在搞验证码识别的时候需要比较字符代码的相似度用到“编辑距离算法”,关于原理和C#实现做个记录。

据百度百科介绍:

编辑距离,又称Levenshtein距离(也叫做Edit Distance),是指两个字串之间,由一个转成另一个所需的最少编辑操作次数,如果它们的距离越大,说明它们越是不同。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。

  例如将kitten一字转成sitting:

  sitten (k→s)

  sittin (e→i)

  sitting (→g)

  俄罗斯科学家Vladimir Levenshtein在1965年提出这个概念。因此也叫Levenshtein Distance。

例如

  • 如果str1="ivan",str2="ivan",那么经过计算后等于 0。没有经过转换。相似度=1-0/Math.Max(str1.length,str2.length)=1
  • 如果str1="ivan1",str2="ivan2",那么经过计算后等于1。str1的"1"转换"2",转换了一个字符,所以距离是1,相似度=1-1/Math.Max(str1.length,str2.length)=0.8

应用

  DNA分析

  拼字检查

  语音辨识

  抄袭侦测

补充内容:

  感谢评论区刀是什么样的刀的热心分享,有兴趣的朋友可以参考一下stackoverflow的这篇博文:How to Strike a Match

整理了下stackoverflow的代码,代码如下:

 /// <summary>
/// This class implements string comparison algorithm
/// based on character pair similarity
/// Source: http://www.catalysoft.com/articles/StrikeAMatch.html
/// </summary>
public class SimilarityTool
{
/// <summary>
/// Compares the two strings based on letter pair matches
/// </summary>
/// <param name="str1"></param>
/// <param name="str2"></param>
/// <returns>The percentage match from 0.0 to 1.0 where 1.0 is 100%</returns>
public double CompareStrings(string str1, string str2)
{
List<string> pairs1 = WordLetterPairs(str1.ToUpper());
List<string> pairs2 = WordLetterPairs(str2.ToUpper()); int intersection = ;
int union = pairs1.Count + pairs2.Count; for (int i = ; i < pairs1.Count; i++)
{
for (int j = ; j < pairs2.Count; j++)
{
if (pairs1[i] == pairs2[j])
{
intersection++;
pairs2.RemoveAt(j);//Must remove the match to prevent "GGGG" from appearing to match "GG" with 100% success break;
}
}
}
return (2.0 * intersection) / union;
} /// <summary>
/// Gets all letter pairs for each
/// individual word in the string
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
private List<string> WordLetterPairs(string str)
{
List<string> AllPairs = new List<string>(); // Tokenize the string and put the tokens/words into an array
string[] Words = Regex.Split(str, @"\s"); // For each word
for (int w = ; w < Words.Length; w++)
{
if (!string.IsNullOrEmpty(Words[w]))
{
// Find the pairs of characters
String[] PairsInWord = LetterPairs(Words[w]); for (int p = ; p < PairsInWord.Length; p++)
{
AllPairs.Add(PairsInWord[p]);
}
}
}
return AllPairs;
} /// <summary>
/// Generates an array containing every
/// two consecutive letters in the input string
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
private string[] LetterPairs(string str)
{
int numPairs = str.Length - ; string[] pairs = new string[numPairs]; for (int i = ; i < numPairs; i++)
{
pairs[i] = str.Substring(i, );
}
return pairs;
}
}

算法过程

  1. str1或str2的长度为0返回另一个字符串的长度。 if(str1.length==0) return str2.length; if(str2.length==0) return str1.length;
  2. 初始化(n+1)*(m+1)的矩阵d,并让第一行和列的值从0开始增长。
  3. 扫描两字符串(n*m级的),如果:str1[i] == str2[j],用temp记录它,为0。否则temp记为1。然后在矩阵d[i,j]赋于d[i-1,j]+1 、d[i,j-1]+1、d[i-1,j-1]+temp三者的最小值。
  4. 扫描完后,返回矩阵的最后一个值d[n][m]即是它们的距离。

计算相似度公式:1-它们的距离/两个字符串长度的最大值。
为了直观表现,我将两个字符串分别写到行和列中,实际计算中不需要。我们用字符串“ivan1”和“ivan2”举例来看看矩阵中值的状况:

1、第一行和第一列的值从0开始增长

    i v a n 1
 
i          
v          
a          
n          
2          

2、i列值的产生 Matrix[i - 1, j] + 1 ; Matrix[i, j - 1] + 1   ;    Matrix[i - 1, j - 1] + t

    i v a n 1
  0+t=0 1+1=2 2 3 4 5
i 1+1=2 取三者最小值=0        
v 2 依次类推:1        
a 3 2        
n 4 3        
2 5 4        

3、V列值的产生

    i v a n 1
  0 1 2      
i 1 0 1      
v 2 1 0      
a 3 2 1      
n 4 3 2      
2 5 4 3      

依次类推直到矩阵全部生成

    i v a n 1
  0 1 2 3 4 5
i 1 0 1 2 3 4
v 2 1 0 1 2 3
a 3 2 1 0 1 2
n 4 3 2 1 0 1
2 5 4 3 2 1 1

最后得到它们的距离=1

相似度:1-1/Math.Max(“ivan1”.length,“ivan2”.length) =0.8

算法用C#实现:

 public class LevenshteinDistance
{
/// <summary>
/// 取最小的一位数
/// </summary>
/// <param name="first"></param>
/// <param name="second"></param>
/// <param name="third"></param>
/// <returns></returns>
private int LowerOfThree(int first, int second, int third)
{
int min = Math.Min(first, second);
return Math.Min(min, third);
} private int Levenshtein_Distance(string str1, string str2)
{
int[,] Matrix;
int n = str1.Length;
int m = str2.Length; int temp = ;
char ch1;
char ch2;
int i = ;
int j = ;
if (n == )
{
return m;
}
if (m == )
{ return n;
}
Matrix = new int[n + , m + ]; for (i = ; i <= n; i++)
{
//初始化第一列
Matrix[i, ] = i;
} for (j = ; j <= m; j++)
{
//初始化第一行
Matrix[, j] = j;
} for (i = ; i <= n; i++)
{
ch1 = str1[i - ];
for (j = ; j <= m; j++)
{
ch2 = str2[j - ];
if (ch1.Equals(ch2))
{
temp = ;
}
else
{
temp = ;
}
Matrix[i, j] = LowerOfThree(Matrix[i - , j] + , Matrix[i, j - ] + , Matrix[i - , j - ] + temp);
}
}
for (i = ; i <= n; i++)
{
for (j = ; j <= m; j++)
{
Console.Write(" {0} ", Matrix[i, j]);
}
Console.WriteLine("");
} return Matrix[n, m];
} /// <summary>
/// 计算字符串相似度
/// </summary>
/// <param name="str1"></param>
/// <param name="str2"></param>
/// <returns></returns>
public decimal LevenshteinDistancePercent(string str1, string str2)
{
//int maxLenth = str1.Length > str2.Length ? str1.Length : str2.Length;
int val = Levenshtein_Distance(str1, str2);
return - (decimal)val / Math.Max(str1.Length, str2.Length);
}
}

 调用:

 static void Main(string[] args)
{
string str1 = "ivan1";
string str2 = "ivan2";
Console.WriteLine("字符串1 {0}", str1); Console.WriteLine("字符串2 {0}", str2); Console.WriteLine("相似度 {0} %", new LevenshteinDistance().LevenshteinDistancePercent(str1, str2) * );
Console.ReadLine();
}

结果:

拓展与补充:

 小规模的字符串近似搜索,需求类似于搜索引擎中输入关键字,出现类似的结果列表。

来源:.Net.NewLife。
    需求:假设在某系统存储了许多地址,例如:“北京市海淀区中关村大街1号海龙大厦”。用户输入“北京 海龙大厦”即可查询到这条结果。另外还需要有容错设计,例如输入“广西 京岛风景区”能够搜索到"广西壮族自治区京岛风景名胜区"。最终的需求是:可以根据用户输入,匹配若干条近似结果共用户选择
    目的:避免用户输入类似地址导致数据出现重复项。例如,已经存在“北京市中关村”,就不应该再允许存在“北京中关村”。

举例

此类技术在搜索引擎中早已广泛使用,例如“查询预测”功能。

要实现此算法,首先需要明确“字符串近似”的概念。

计算字符串相似度通常使用的是动态规划(DP)算法。

常用的算法是 Levenshtein Distance。用这个算法可以直接计算出两个字符串的“编辑距离”。所谓编辑距离,是指一个字符串,每次只能通过插入一个字符、删除一个字符或者修改一个字符的方法,变成另外一个字符串的最少操作次数。这就引出了第一种方法:计算两个字符串之间的编辑距离。稍加思考之后发现,不能用输入的关键字直接与句子做匹配。你必须从句子中选取合适的长度后再做匹配。把结果按照距离升序排序。

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text; namespace BestString
{
public static class SearchHelper
{
public static string[] Search(string param, string[] datas)
{
if (string.IsNullOrWhiteSpace(param))
return new string[]; string[] words = param.Split(new char[] { ' ', ' ' }, StringSplitOptions.RemoveEmptyEntries); foreach (string word in words)
{
int maxDist = (word.Length - ) / ; var q = from str in datas
where word.Length <= str.Length
&& Enumerable.Range(, maxDist + )
.Any(dist =>
{
return Enumerable.Range(, Math.Max(str.Length - word.Length - dist + , ))
.Any(f =>
{
return Distance(word, str.Substring(f, word.Length + dist)) <= maxDist;
});
})
orderby str
select str;
datas = q.ToArray();
} return datas;
} static int Distance(string str1, string str2)
{
int n = str1.Length;
int m = str2.Length;
int[,] C = new int[n + , m + ];
int i, j, x, y, z;
for (i = ; i <= n; i++)
C[i, ] = i;
for (i = ; i <= m; i++)
C[, i] = i;
for (i = ; i < n; i++)
for (j = ; j < m; j++)
{
x = C[i, j + ] + ;
y = C[i + , j] + ;
if (str1[i] == str2[j])
z = C[i, j];
else
z = C[i, j] + ;
C[i + , j + ] = Math.Min(Math.Min(x, y), z);
}
return C[n, m];
}
}
}

分析这个方法后发现,每次对一个句子进行相关度比较的时候,都要把把句子从头到尾扫描一次,每次扫描还需要以最大误差作长度控制。这样一来,对每个句子的计算次数大大增加。达到了二次方的规模(忽略距离计算时间)。

所以我们需要更高效的计算策略。在纸上写出一个句子,再写出几个关键字。一个一个涂画之后,偶然发现另一种字符串相关的算法完全可以适用。那就是 Longest common subsequence(LCS,最长公共字串)。为什么这个算法可以用来计算两个字符串的相关度?先看一个例子:

关键字:少年时代的神话播下了浪漫注意

句子:就是少年时代大量神话传说在其心田里播下了浪漫主义这颗难以磨灭的种子

这里用了两个关键字进行搜索。可以看出来两个关键字都有部分匹配了句子中的若干部分。这样可以单独为两个关键字计算 LCS,LCS之和就是简单的相关度。看到这里,你若是已经理解了核心思想,已经可以实现出基本框架了。但是,请看下面这个例子:

关键字:东土大唐,唐三藏

句子:我本是东土大唐钦差御弟唐三藏大徒弟孙悟空行者

看出来问题了吗?下面还是使用同样的关键字和句子。

关键字:东土大(唐唐)三藏

句子: 我本是东土大唐钦差御弟唐三藏大徒弟孙悟空行者

举这个例子为了说明,在进行 LCS 计算的过程中,得到的结果并不能保证就是我们期望的结果。为了①保证所匹配的结果中不存在交集,并且②在句子中的匹配结果尽可能的短,需要采取两个补救措施。(为什么需要满足这样的条件,读者自行思考)

第一:可以在单次计算 LCS 之后,用贪心策略向前(向后)找到最先能够完成匹配的位置,再用相同的策略向后(向前)扫描。这样可以满足第二个条件找到句子中最短的匹配。如果你对 LCS 算法有深入了解,完全可以在计算 LCS 的过程中找到最短匹配的结束位置,然后只需要进行一次向前扫描就可以完成。这样节约了一次扫描过程。

第二:增加一个标记数组,记录句子中的字符是否被匹配过。

最后标记数组中标记过的位置就是匹配结果。

相信你看到这里一定非常头晕,下面用一个例子解释:(句子)

关键字:   ABCD

句子:     XAYABZCBXCDDYZ

句子分解: X Y  Z  X   YZ

A   B C   D

A   B C D

你可能会匹配成 AYABZCBXCDD,AYABZCBXCD,ABZCBXCDD,ABZCBXCD。我们实际需要的只是ABZCBXCD。

使用LCS匹配之后,得到的很可能是 XAYABZCBXCDDYZ;

用贪心策略向前处理后,得到结果为 XAYABZCBXCDDYZ;

用贪心策略向后处理后,得到结果为 XAYABZCBXCDDYZ。

这样处理的目的是为了避免得到较长的匹配结果(类似正则表达式的贪婪、懒惰模式)。

以上只是描述了怎么计算两个字符串的相似程度。除此之外还需要:①剔除相似度较低的结果;②对结果进行排序。

剔除相似度较低的结果,这里设定了一个阈值:差错比例不能超过匹配结果长度的一半。

对结果进行排序,不能够直接使用相似度进行排序。因为相似度并没有考虑到句子的长度。按照使用习惯,通常会把匹配度高,并且句子长度短的放在前面。这就得到了排序因子:(不匹配度+0.5)/句子长度。

最后得到我们最终的搜索方法

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics; namespace BestString
{
public static class SearchHelper
{
public static string[] Search(string param, string[] items)
{
if (string.IsNullOrWhiteSpace(param) || items == null || items.Length == )
return new string[]; string[] words = param
.Split(new char[] { ' ', '\u3000' }, StringSplitOptions.RemoveEmptyEntries)
.OrderBy(s => s.Length)
.ToArray(); var q = from sentence in items.AsParallel()
let MLL = Mul_LnCS_Length(sentence, words)
where MLL >=
orderby (MLL + 0.5) / sentence.Length, sentence
select sentence; return q.ToArray();
} //static int[,] C = new int[100, 100]; /// <summary>
///
/// </summary>
/// <param name="sentence"></param>
/// <param name="words">多个关键字。长度必须大于0,必须按照字符串长度升序排列。</param>
/// <returns></returns>
static int Mul_LnCS_Length(string sentence, string[] words)
{
int sLength = sentence.Length;
int result = sLength;
bool[] flags = new bool[sLength];
int[,] C = new int[sLength + , words[words.Length - ].Length + ];
//int[,] C = new int[sLength + 1, words.Select(s => s.Length).Max() + 1];
foreach (string word in words)
{
int wLength = word.Length;
int first = , last = ;
int i = , j = , LCS_L;
//foreach 速度会有所提升,还可以加剪枝
for (i = ; i < sLength; i++)
for (j = ; j < wLength; j++)
if (sentence[i] == word[j])
{
C[i + , j + ] = C[i, j] + ;
if (first < C[i, j])
{
last = i;
first = C[i, j];
}
}
else
C[i + , j + ] = Math.Max(C[i, j + ], C[i + , j]); LCS_L = C[i, j];
if (LCS_L <= wLength >> )
return -; while (i > && j > )
{
if (C[i - , j - ] + == C[i, j])
{
i--;
j--;
if (!flags[i])
{
flags[i] = true;
result--;
}
first = i;
}
else if (C[i - , j] == C[i, j])
i--;
else// if (C[i, j - 1] == C[i, j])
j--;
} if (LCS_L <= (last - first + ) >> )
return -;
} return result;
}
}
}

对于此类问题,要想得到更快速的实现,必须要用到分词+索引的方案。在此不做探讨。

代码打包下载:http://files.cnblogs.com/Aimeast/BestString.zip

用C#实现字符串相似度算法(编辑距离算法 Levenshtein Distance)的更多相关文章

  1. [Irving]字符串相似度-字符编辑距离算法(c#实现)

    编辑距离(Edit Distance),又称Levenshtein距离,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数.许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字 ...

  2. 扒一扒编辑距离(Levenshtein Distance)算法

    最近由于工作需要,接触了编辑距离(Levenshtein Distance)算法.赶脚很有意思.最初百度了一些文章,但讲的都不是很好,读起来感觉似懂非懂.最后还是用google找到了一些资料才慢慢理解 ...

  3. Java 比较两个字符串的相似度算法(Levenshtein Distance)

    转载自: https://blog.csdn.net/JavaReact/article/details/82144732 算法简介: Levenshtein Distance,又称编辑距离,指的是两 ...

  4. 编辑距离算法(Levenshtein)

    编辑距离定义: 编辑距离,又称Levenshtein距离,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数. 许可的编辑操作包括:将一个字符替换成另一个字符,插入一个字符,删除一个字符. 例如 ...

  5. Go 实现字符串相似度计算函数 Levenshtein 和 SimilarText

    [转]http://www.syyong.com/Go/Go-implements-the-string-similarity-calculation-function-Levenshtein-and ...

  6. 字符串相似度算法(编辑距离算法 Levenshtein Distance)(转)

    在搞验证码识别的时候需要比较字符代码的相似度用到“编辑距离算法”,关于原理和C#实现做个记录. 据百度百科介绍: 编辑距离,又称Levenshtein距离(也叫做Edit Distance),是指两个 ...

  7. 字符串相似度算法(编辑距离算法 Levenshtein Distance)

    在搞验证码识别的时候需要比较字符代码的相似度用到“编辑距离算法”,关于原理和C#实现做个记录.据百度百科介绍:编辑距离,又称Levenshtein距离(也叫做Edit Distance),是指两个字串 ...

  8. [转]字符串相似度算法(编辑距离算法 Levenshtein Distance)

    转自:http://www.sigvc.org/bbs/forum.php?mod=viewthread&tid=981 http://www.cnblogs.com/ivanyb/archi ...

  9. 计算字符串相似度算法——Levenshtein

    转自:http://wdhdmx.iteye.com/blog/1343856 0.这个算法实现起来很简单 1.百度百科介绍: Levenshtein 距离,又称编辑距离,指的是两个字符串之间,由一个 ...

随机推荐

  1. yum的初步了解与使用

    什么是yum Yum(Yellow dog Updater,Modified)是一个基于RPM包管理的字符前端软件包管理器.能够从指定的服务器自动下载RPM包并且安装,可解决软件包相关依赖性,并且一次 ...

  2. 【beta】阶段 第六次 Scrum Meeting

    每日任务 1.本次会议为第六次 Meeting会议: 2.本次会议在周六上午大课间,在陆大楼召开,召开本次会议为15分钟. 一.今日站立式会议照片 二.每个人的工作 (有work item 的ID) ...

  3. 团队作业8——Beta项目(冲刺计划)

    Beta阶段冲刺计划 经过几周的努力我们完成了Alpha的开发,进过一段时间的调整与重组我们继续向Beta版进发. 1. 新成员介绍 林乔桦(201421123074):掌握c语言,JavaScrip ...

  4. 201521123098 《Java程序设计》第7周学习总结

    1. 本周学习总结 以你喜欢的方式(思维导图或其他)归纳总结集合相关内容. 2. 书面作业 1. ArrayList代码分析 1.1 解释ArrayList的contains源代码 该方法调用了ind ...

  5. 201521123062 《Java程序设计》第14周学习总结

    1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多数据库相关内容. 2. 书面作业 1. MySQL数据库基本操作 建立数据库,将自己的姓名.学号作为一条记录插入.(截图,需出现自 ...

  6. 201521123023《Java程序设计》第9周学习总结

    1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结异常相关内容. 2. 书面作业 本次PTA作业题集异常 1.常用异常 题目5-1 1.1 截图你的提交结果(出现学号) 1.2 自己 ...

  7. C#设计模式(6)-原型模式

    引言 上一篇介绍了设计模式中的抽象工厂模式-C#设计模式(3)-建造者模式,本篇将介绍原型模式: 点击这里查看全部设计模式系列文章导航 原型模式简介 原型模式:用原型实例指定创建对象的种类,并且通过拷 ...

  8. returned a response status of 403 OR 409

    当我们使用jersy把图片上传到我们的图片服务器中[tomcat],我们可能会有以下的错误: returned a response status of 403 OR 409 403和409我都遇到过 ...

  9. SpringMVC第三篇【收集参数、字符串转日期、结果重定向、返回JSON】

    业务方法收集参数 我们在Struts2中收集web端带过来的参数是在控制器中定义成员变量,该成员变量的名字与web端带过来的名称是要一致的-并且,给出该成员变量的set方法,那么Struts2的拦截器 ...

  10. java基础知识3--如何获取资源文件(Java中获取资源文件的url)

    java开发中,常见的resource文件有:.xml,.properties,.txt文件等,后台开发中经常用到读取资源文件,处理业务逻辑,然后返回结果. 获取资源文件的方法说明getResourc ...