【算法】字符串近似搜索(转)
來源:.Net.NewLife。需求:假設在某系統存儲了許多地址,例如:“北京市海淀區中關村大街1號海龍大廈”。用戶輸入“北京 海龍大廈”即可查詢到這條結果。另外還需要有容錯設計,例如輸入“廣西 京島風景區”能夠搜索到"廣西壯族自治區京島風景名勝區"。最終的需求是:可以根據用戶輸入,匹配若干條近似結果共用戶選擇。目的:避免用戶輸入類似地址導致數據出現重復項。例如,已經存在“北京市中關村”,就不應該再允許存在“北京中關村”。
????舉例:
??? 此類技術在搜索引擎中早已廣泛使用,例如“查詢預測”功能。
??? 要實現此算法,首先需要明確“字符串近似”的概念。
??? 計算字符串相似度通常使用的是動態規劃(DP)算法。
??? 常用的算法是?Levenshtein Distance。用這個算法可以直接計算出兩個字符串的“編輯距離”。所謂編輯距離,是指一個字符串,每次只能通過插入一個字符、刪除一個字符或者修改一個字符的方法,變成另外一個字符串的最少操作次數。這就引出了第一種方法:計算兩個字符串之間的編輯距離。稍加思考之后發現,不能用輸入的關鍵字直接與句子做匹配。你必須從句子中選取合適的長度后再做匹配。把結果按照距離升序排序。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | 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[0]; ????????????string[] words = param.Split(new?char[] { ' ', ' '?}, StringSplitOptions.RemoveEmptyEntries); ????????????foreach?(string?word in?words) ????????????{ ????????????????int?maxDist = (word.Length - 1) / 2; ????????????????var?q = from?str in?datas ????????????????????????where?word.Length <= str.Length ????????????????????????????&& Enumerable.Range(0, maxDist + 1) ????????????????????????????.Any(dist => ????????????????????????????{ ????????????????????????????????return?Enumerable.Range(0, Math.Max(str.Length - word.Length - dist + 1, 0)) ????????????????????????????????????.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 + 1, m + 1]; ????????????int?i, j, x, y, z; ????????????for?(i = 0; i <= n; i++) ????????????????C[i, 0] = i; ????????????for?(i = 1; i <= m; i++) ????????????????C[0, i] = i; ????????????for?(i = 0; i < n; i++) ????????????????for?(j = 0; j < m; j++) ????????????????{ ????????????????????x = C[i, j + 1] + 1; ????????????????????y = C[i + 1, j] + 1; ????????????????????if?(str1[i] == str2[j]) ????????????????????????z = C[i, j]; ????????????????????else ????????????????????????z = C[i, j] + 1; ????????????????????C[i + 1, j + 1] = 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)/句子長度。
??? 最后得到我們最終的搜索方法
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | 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 == 0) ????????????????return?new?string[0]; ????????????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 >= 0 ????????????????????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 + 1, words[words.Length - 1].Length + 1]; ????????????//int[,] C = new int[sLength + 1, words.Select(s => s.Length).Max() + 1]; ????????????foreach?(string?word in?words) ????????????{ ????????????????int?wLength = word.Length; ????????????????int?first = 0, last = 0; ????????????????int?i = 0, j = 0, LCS_L; ????????????????//foreach 速度會有所提升,還可以加剪枝 ????????????????for?(i = 0; i < sLength; i++) ????????????????????for?(j = 0; j < wLength; j++) ????????????????????????if?(sentence[i] == word[j]) ????????????????????????{ ????????????????????????????C[i + 1, j + 1] = C[i, j] + 1; ????????????????????????????if?(first < C[i, j]) ????????????????????????????{ ????????????????????????????????last = i; ????????????????????????????????first = C[i, j]; ????????????????????????????} ????????????????????????} ????????????????????????else ????????????????????????????C[i + 1, j + 1] = Math.Max(C[i, j + 1], C[i + 1, j]); ????????????????LCS_L = C[i, j]; ????????????????if?(LCS_L <= wLength >> 1) ????????????????????return?-1; ????????????????while?(i > 0 && j > 0) ????????????????{ ????????????????????if?(C[i - 1, j - 1] + 1 == C[i, j]) ????????????????????{ ????????????????????????i--; ????????????????????????j--; ????????????????????????if?(!flags[i]) ????????????????????????{ ????????????????????????????flags[i] = true; ????????????????????????????result--; ????????????????????????} ????????????????????????first = i; ????????????????????} ????????????????????else?if?(C[i - 1, j] == C[i, j]) ????????????????????????i--; ????????????????????else// if (C[i, j - 1] == C[i, j]) ????????????????????????j--; ????????????????} ????????????????if?(LCS_L <= (last - first + 1) >> 1) ????????????????????return?-1; ????????????} ????????????return?result; ????????} ????} } |
??? 對于此類問題,要想得到更快速的實現,必須要用到分詞+索引的方案。在此不做探討。
代碼打包下載:http://files.cnblogs.com/Aimeast/BestString.zip
http://www.cnblogs.com/Aimeast/archive/2011/09/05/2167844.html
總結
以上是生活随笔為你收集整理的【算法】字符串近似搜索(转)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 煎黄花鱼取什么菜名好听?
- 下一篇: 使用ajax跨域withCredenti