在搞驗證碼識別的時候須要比較字符代碼的類似度用到「編輯距離算法」,關於原理和C#實現作個記錄。html
據百度百科介紹:正則表達式
編輯距離,又稱Levenshtein距離(也叫作Edit Distance),是指兩個字串之間,由一個轉成另外一個所需的最少編輯操做次數,若是它們的距離越大,說明它們越是不一樣。許可的編輯操做包括將一個字符替換成另外一個字符,插入一個字符,刪除一個字符。算法
例如將kitten一字轉成sitting:數組
sitten (k→s)app
sittin (e→i)框架
sitting (→g)搜索引擎
俄羅斯科學家Vladimir Levenshtein在1965年提出這個概念。所以也叫Levenshtein Distance。spa
例如設計
DNA分析code
拼字檢查
語音辨識
抄襲偵測
補充內容:
感謝評論區刀是什麼樣的刀的熱心分享,有興趣的朋友能夠參考一下stackoverflow的這篇博文:How to Strike a Match
整理了下stackoverflow的代碼,代碼以下:
1 /// <summary> 2 /// This class implements string comparison algorithm 3 /// based on character pair similarity 4 /// Source: http://www.catalysoft.com/articles/StrikeAMatch.html 5 /// </summary> 6 public class SimilarityTool 7 { 8 /// <summary> 9 /// Compares the two strings based on letter pair matches 10 /// </summary> 11 /// <param name="str1"></param> 12 /// <param name="str2"></param> 13 /// <returns>The percentage match from 0.0 to 1.0 where 1.0 is 100%</returns> 14 public double CompareStrings(string str1, string str2) 15 { 16 List<string> pairs1 = WordLetterPairs(str1.ToUpper()); 17 List<string> pairs2 = WordLetterPairs(str2.ToUpper()); 18 19 int intersection = 0; 20 int union = pairs1.Count + pairs2.Count; 21 22 for (int i = 0; i < pairs1.Count; i++) 23 { 24 for (int j = 0; j < pairs2.Count; j++) 25 { 26 if (pairs1[i] == pairs2[j]) 27 { 28 intersection++; 29 pairs2.RemoveAt(j);//Must remove the match to prevent "GGGG" from appearing to match "GG" with 100% success 30 31 break; 32 } 33 } 34 } 35 return (2.0 * intersection) / union; 36 } 37 38 /// <summary> 39 /// Gets all letter pairs for each 40 /// individual word in the string 41 /// </summary> 42 /// <param name="str"></param> 43 /// <returns></returns> 44 private List<string> WordLetterPairs(string str) 45 { 46 List<string> AllPairs = new List<string>(); 47 48 // Tokenize the string and put the tokens/words into an array 49 string[] Words = Regex.Split(str, @"\s"); 50 51 // For each word 52 for (int w = 0; w < Words.Length; w++) 53 { 54 if (!string.IsNullOrEmpty(Words[w])) 55 { 56 // Find the pairs of characters 57 String[] PairsInWord = LetterPairs(Words[w]); 58 59 for (int p = 0; p < PairsInWord.Length; p++) 60 { 61 AllPairs.Add(PairsInWord[p]); 62 } 63 } 64 } 65 return AllPairs; 66 } 67 68 /// <summary> 69 /// Generates an array containing every 70 /// two consecutive letters in the input string 71 /// </summary> 72 /// <param name="str"></param> 73 /// <returns></returns> 74 private string[] LetterPairs(string str) 75 { 76 int numPairs = str.Length - 1; 77 78 string[] pairs = new string[numPairs]; 79 80 for (int i = 0; i < numPairs; i++) 81 { 82 pairs[i] = str.Substring(i, 2); 83 } 84 return pairs; 85 } 86 }
算法過程
計算類似度公式:1-它們的距離/兩個字符串長度的最大值。
爲了直觀表現,我將兩個字符串分別寫到行和列中,實際計算中不須要。咱們用字符串「ivan1」和「ivan2」舉例來看看矩陣中值的情況:
一、第一行和第一列的值從0開始增加
i | v | a | n | 1 | ||
0 | 1 | 2 | 3 | 4 | 5 | |
i | 1 | |||||
v | 2 | |||||
a | 3 | |||||
n | 4 | |||||
2 | 5 |
二、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 |
三、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#實現:
1 public class LevenshteinDistance 2 { 3 /// <summary> 4 /// 取最小的一位數 5 /// </summary> 6 /// <param name="first"></param> 7 /// <param name="second"></param> 8 /// <param name="third"></param> 9 /// <returns></returns> 10 private int LowerOfThree(int first, int second, int third) 11 { 12 int min = Math.Min(first, second); 13 return Math.Min(min, third); 14 } 15 16 private int Levenshtein_Distance(string str1, string str2) 17 { 18 int[,] Matrix; 19 int n = str1.Length; 20 int m = str2.Length; 21 22 int temp = 0; 23 char ch1; 24 char ch2; 25 int i = 0; 26 int j = 0; 27 if (n == 0) 28 { 29 return m; 30 } 31 if (m == 0) 32 { 33 34 return n; 35 } 36 Matrix = new int[n + 1, m + 1]; 37 38 for (i = 0; i <= n; i++) 39 { 40 //初始化第一列 41 Matrix[i, 0] = i; 42 } 43 44 for (j = 0; j <= m; j++) 45 { 46 //初始化第一行 47 Matrix[0, j] = j; 48 } 49 50 for (i = 1; i <= n; i++) 51 { 52 ch1 = str1[i - 1]; 53 for (j = 1; j <= m; j++) 54 { 55 ch2 = str2[j - 1]; 56 if (ch1.Equals(ch2)) 57 { 58 temp = 0; 59 } 60 else 61 { 62 temp = 1; 63 } 64 Matrix[i, j] = LowerOfThree(Matrix[i - 1, j] + 1, Matrix[i, j - 1] + 1, Matrix[i - 1, j - 1] + temp); 65 } 66 } 67 for (i = 0; i <= n; i++) 68 { 69 for (j = 0; j <= m; j++) 70 { 71 Console.Write(" {0} ", Matrix[i, j]); 72 } 73 Console.WriteLine(""); 74 } 75 76 return Matrix[n, m]; 77 } 78 79 /// <summary> 80 /// 計算字符串類似度 81 /// </summary> 82 /// <param name="str1"></param> 83 /// <param name="str2"></param> 84 /// <returns></returns> 85 public decimal LevenshteinDistancePercent(string str1, string str2) 86 { 87 //int maxLenth = str1.Length > str2.Length ? str1.Length : str2.Length; 88 int val = Levenshtein_Distance(str1, str2); 89 return 1 - (decimal)val / Math.Max(str1.Length, str2.Length); 90 } 91 }
調用:
1 static void Main(string[] args) 2 { 3 string str1 = "ivan1"; 4 string str2 = "ivan2"; 5 Console.WriteLine("字符串1 {0}", str1); 6 7 Console.WriteLine("字符串2 {0}", str2); 8 9 Console.WriteLine("類似度 {0} %", new LevenshteinDistance().LevenshteinDistancePercent(str1, str2) * 100); 10 Console.ReadLine(); 11 }
結果:
拓展與補充:
小規模的字符串近似搜索,需求相似於搜索引擎中輸入關鍵字,出現相似的結果列表。
來源:.Net.NewLife。
需求:假設在某系統存儲了許多地址,例如:「北京市海淀區中關村大街1號海龍大廈」。用戶輸入「北京 海龍大廈」便可查詢到這條結果。另外還須要有容錯設計,例如輸入「廣西 京島風景區」可以搜索到"廣西壯族自治區京島風景名勝區"。最終的需求是:能夠根據用戶輸入,匹配若干條近似結果共用戶選擇。
目的:避免用戶輸入相似地址致使數據出現重複項。例如,已經存在「北京市中關村」,就不該該再容許存在「北京中關村」。
舉例:
此類技術在搜索引擎中早已普遍使用,例如「查詢預測」功能。
要實現此算法,首先須要明確「字符串近似」的概念。
計算字符串類似度一般使用的是動態規劃(DP)算法。
經常使用的算法是 Levenshtein Distance。用這個算法能夠直接計算出兩個字符串的「編輯距離」。所謂編輯距離,是指一個字符串,每次只能經過插入一個字符、刪除一個字符或者修改一個字符的方法,變成另一個字符串的最少操做次數。這就引出了第一種方法:計算兩個字符串之間的編輯距離。稍加思考以後發現,不能用輸入的關鍵字直接與句子作匹配。你必須從句子中選取合適的長度後再作匹配。把結果按照距離升序排序。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 6 namespace BestString 7 { 8 public static class SearchHelper 9 { 10 public static string[] Search(string param, string[] datas) 11 { 12 if (string.IsNullOrWhiteSpace(param)) 13 return new string[0]; 14 15 string[] words = param.Split(new char[] { ' ', ' ' }, StringSplitOptions.RemoveEmptyEntries); 16 17 foreach (string word in words) 18 { 19 int maxDist = (word.Length - 1) / 2; 20 21 var q = from str in datas 22 where word.Length <= str.Length 23 && Enumerable.Range(0, maxDist + 1) 24 .Any(dist => 25 { 26 return Enumerable.Range(0, Math.Max(str.Length - word.Length - dist + 1, 0)) 27 .Any(f => 28 { 29 return Distance(word, str.Substring(f, word.Length + dist)) <= maxDist; 30 }); 31 }) 32 orderby str 33 select str; 34 datas = q.ToArray(); 35 } 36 37 return datas; 38 } 39 40 static int Distance(string str1, string str2) 41 { 42 int n = str1.Length; 43 int m = str2.Length; 44 int[,] C = new int[n + 1, m + 1]; 45 int i, j, x, y, z; 46 for (i = 0; i <= n; i++) 47 C[i, 0] = i; 48 for (i = 1; i <= m; i++) 49 C[0, i] = i; 50 for (i = 0; i < n; i++) 51 for (j = 0; j < m; j++) 52 { 53 x = C[i, j + 1] + 1; 54 y = C[i + 1, j] + 1; 55 if (str1[i] == str2[j]) 56 z = C[i, j]; 57 else 58 z = C[i, j] + 1; 59 C[i + 1, j + 1] = Math.Min(Math.Min(x, y), z); 60 } 61 return C[n, m]; 62 } 63 } 64 }
分析這個方法後發現,每次對一個句子進行相關度比較的時候,都要把把句子從頭至尾掃描一次,每次掃描還須要以最大偏差做長度控制。這樣一來,對每一個句子的計算次數大大增長。達到了二次方的規模(忽略距離計算時間)。
因此咱們須要更高效的計算策略。在紙上寫出一個句子,再寫出幾個關鍵字。一個一個塗畫以後,偶然發現另外一種字符串相關的算法徹底能夠適用。那就是 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)/句子長度。
最後獲得咱們最終的搜索方法
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Diagnostics; 6 7 namespace BestString 8 { 9 public static class SearchHelper 10 { 11 public static string[] Search(string param, string[] items) 12 { 13 if (string.IsNullOrWhiteSpace(param) || items == null || items.Length == 0) 14 return new string[0]; 15 16 string[] words = param 17 .Split(new char[] { ' ', '\u3000' }, StringSplitOptions.RemoveEmptyEntries) 18 .OrderBy(s => s.Length) 19 .ToArray(); 20 21 var q = from sentence in items.AsParallel() 22 let MLL = Mul_LnCS_Length(sentence, words) 23 where MLL >= 0 24 orderby (MLL + 0.5) / sentence.Length, sentence 25 select sentence; 26 27 return q.ToArray(); 28 } 29 30 //static int[,] C = new int[100, 100]; 31 32 /// <summary> 33 /// 34 /// </summary> 35 /// <param name="sentence"></param> 36 /// <param name="words">多個關鍵字。長度必須大於0,必須按照字符串長度升序排列。</param> 37 /// <returns></returns> 38 static int Mul_LnCS_Length(string sentence, string[] words) 39 { 40 int sLength = sentence.Length; 41 int result = sLength; 42 bool[] flags = new bool[sLength]; 43 int[,] C = new int[sLength + 1, words[words.Length - 1].Length + 1]; 44 //int[,] C = new int[sLength + 1, words.Select(s => s.Length).Max() + 1]; 45 foreach (string word in words) 46 { 47 int wLength = word.Length; 48 int first = 0, last = 0; 49 int i = 0, j = 0, LCS_L; 50 //foreach 速度會有所提高,還能夠加剪枝 51 for (i = 0; i < sLength; i++) 52 for (j = 0; j < wLength; j++) 53 if (sentence[i] == word[j]) 54 { 55 C[i + 1, j + 1] = C[i, j] + 1; 56 if (first < C[i, j]) 57 { 58 last = i; 59 first = C[i, j]; 60 } 61 } 62 else 63 C[i + 1, j + 1] = Math.Max(C[i, j + 1], C[i + 1, j]); 64 65 LCS_L = C[i, j]; 66 if (LCS_L <= wLength >> 1) 67 return -1; 68 69 while (i > 0 && j > 0) 70 { 71 if (C[i - 1, j - 1] + 1 == C[i, j]) 72 { 73 i--; 74 j--; 75 if (!flags[i]) 76 { 77 flags[i] = true; 78 result--; 79 } 80 first = i; 81 } 82 else if (C[i - 1, j] == C[i, j]) 83 i--; 84 else// if (C[i, j - 1] == C[i, j]) 85 j--; 86 } 87 88 if (LCS_L <= (last - first + 1) >> 1) 89 return -1; 90 } 91 92 return result; 93 } 94 } 95 }
對於此類問題,要想獲得更快速的實現,必需要用到分詞+索引的方案。在此不作探討。