基於海量詞庫的單詞拼寫檢查、推薦究竟是咋作的?

前言

在咱們平常應用中,應該遇到很多相似的情況:git

  • 寫文檔時,單詞拼寫錯誤後,工具自動推薦一個類似且正確的拼寫形式;
  • 使用搜狗輸入法時,敲錯某個字的拼音照樣可以打出咱們想要的漢字;
  • 利用搜索引擎進行搜索時,下拉框中自動列出與輸入相近的詞語。
  • 等等,不一一列舉。

這種功能是如何實現的呢?裏面用到了哪些算法呢?本文就來介紹一個可以完成這個任務的算法。github

問題描述

其實,這幾個問題都可以轉換成同一個問題:即對於給定的輸入字符串T,在預先準備好的模式串集合Q中找到與輸入串類似的模式串子集。算法

那麼如何獲得準備好的這些模式串集合呢?咱們能夠經過數據挖掘等一些機制來獲得。express

那麼接下來的問題就是如何快速的從這個集合中找到與輸入串類似的字符串?一般咱們用最小編輯距離來表示兩個字符串的類似程度。app

例如,對於輸入串T,咱們限制錯誤數小於等於2,即在預先準備好的模式集合中找全部與輸入串編輯距離小於等於2的字符串。函數

有什麼算法可以快速完成這個任務呢?工具

暴力算法

遍歷集合Q中的每一個模式串P,分別計算其與輸入串T的最小編輯距離,若是編輯距離小於指定的錯誤容忍度x,則輸出這個模式串。優化

  • 時間複雜度:O(|Q| * n * m),當|Q|很大時,速度將會很慢。

那麼這個算法能夠優化麼?能夠!搜索引擎

好比,第一個字不多有人輸入錯,因此咱們能夠在模式串集合Q中只對第一個字與輸入串第一個字相同的那些字符串進行類似度計算,這樣就可以減小至關多的算量,是一個可行方法。設計

可是這也有問題,倘若少部分人確實第一個字輸入錯了,那麼這個算法找到的全部串也是錯的,不能達到糾錯的效果。

因此,針對首字符過濾的優化算法有必定的侷限性。

步步優化

咱們仔細思考這個問題,因爲模式串Q是一個集合,那麼其中一定有大量的模式串有共同的前綴。可否利用這個前綴進行優化呢?

優化1: 利用兩個詞的相同前綴進行優化

好比:字符串 explore和explain,他們有公共的前綴,這就意味着他們與字符串explode的編輯矩陣的前幾列值是相同的,不用重複計算,以下圖紅色部分所示。

explore與explain不管與任何字符串計算編輯距離,編輯矩陣的前4列確定如出一轍。因此,若是咱們已經計算過explore與某個串的編輯距離後,那麼當計算該串與explain的編輯距離時,前4列能夠複用,直接從第五列開始計算。

到此,咱們獲得一個新的算法計算多模式的編輯距離:把模式串集合創建成一棵字典樹,深度優先遍歷這棵樹,在遍歷的過程當中,不斷更新編輯矩陣的某一個列,若是到達的節點是一個終結符,而且T與P(路徑上的字符造成的字符串)的編輯距離小於指定的容忍度,則找到一個符合條件的串。

優化2:剪枝

雖然咱們利用詞前綴優化了算法,可以避免擁有相同前綴模式串的編輯矩陣的重複計算,可是必需要遍歷全部節點。有沒有什麼辦法可以在計算到某一深度後,根據一些限制條件可以剪去該子樹其它剩餘節點的計算呢?在搜索算法中,這種優化叫作剪枝。接下來咱們討論一下該如何設計一個剪枝函數。

從新審視咱們的編輯距離定義,其實能夠當作是把字符串P和T分別拆分紅兩段,而後計算對應的段的編輯距離之和,以下圖所示。

字符串P和T分別拆分紅兩段,紅色和綠色。紅色部分之間的編輯距離與綠色部分之間的編輯距離之和即爲字符串P和T的編輯距離。

舉個例子,更形象:

  • 例子1
ed("explore", "express") = ed("explo", "exp") + ed("re", "ress")
  • 例子2
ed("explore", "express") = ed("exp", "exp") + ed("lore", "ress")
  • 例子3
    可是,並非每種劃分都是正確的,好比下面圖所示:
ed("ex","exp") + ed("plore", "ress") = 1 + 4 = 5

因此,最小編輯距離問題又至關於一個最優拆分,即對於字符串P中位置爲i的字符,找到在T中的一個最優位置j,使得

ed(P.prefix(i), T.prefix(j)) + ed(P.suffix(i+1), T.suffix(j+1))

最小。

回到咱們這個問題中來,若是咱們限制P和T的最小編輯距離小於等於x,

咱們讓 p[i]分別匹配t[i-x],t[i-x+1],......,t[i],t[i+1],......t[i+x],並找到其中前半段匹配的最小的編輯距離ed1=ed(p[1~i],t[1~j]),若是ed1大於x,咱們則能推斷出ed(p,t)也終將大於x(ed=ed1+ed2>x)。

爲何p[i]不匹配t[i-x-1]以及以前的位置呢?那是由於ed(p.prefix(i), t.prefix(i-x-1)) > x,由於必須至少在t.prefix(i-x-1)中插入x+1個字符才能保證字符串長度相等;同理p[i]也不能匹配t[i+x+1]及其以後的位置。因此,根據分段原則,最優匹配確定出如今t[i-x] ~ t[i+x]之間,若是這個區間的最小編輯距離都大於x,那麼咱們無需對p[i+1]及其以後的字符進行匹配計算。

例如:當遍歷到藍色節點l時,路徑造成的字符串expl與T=exist知足剪枝條件,則後序節點不須要遍歷,由於後面不可能有任何一個字符串知足與T的編輯距離小於2。

至此,咱們獲得了剪枝優化:深度遍歷到達字典樹的某個節點,其路徑上的字符組成字符串P,計算其與T.prefix(i-x), T.prefix(i-x+1),......T.prefix(i+x)的最小編輯距離,若是其中的最小值大於x,則中止遍歷這棵子樹上的後面的節點。

其實,這個最終版本的優化算法出自論文:《Error-tolerant finite-state recognition with applications to morphological analysis and spelling correction》.K Oflazer:1996

代碼實現與效果對比

代碼實現需須要很強的技巧性,由於不管是剪枝函數仍是進行最終確認函數均可以複用同一個編輯矩陣,貼一個很醜陋的代碼:https://github.com/haolujun/Algorithm/tree/master/muti-edit-distance

這個算法在錯誤容忍度很是小的狀況下效率很是高,我隨機生成了10萬個長度5~10的模式串,再隨機生成100個輸入串T(長度5 ~ 10),字符集大小爲10,x最小編輯距離限制,計算多模式編輯距離,處理總時間以下,單位ms:

算法 x = 1 x = 2 x = 3 x = 4 x = 5 x = 6
暴力算法 21990 21990 21990 21990 21990 21990
優化算法 97 922 4248 11361 20097 28000

當容忍度很小時,優化算法完勝暴力算法,而且實際應用中x通常取值都很是小,正好適合優化算法。

當x值增大,優化算法效率逐漸降低,而且最後慢於暴力算法,這是由於優化算法實現複雜致使(遞歸+更復雜的判斷)。

因此,最終應用時,咱們根據x值選擇不一樣的算法。

相關文章
相關標籤/搜索