BK樹 拼寫檢查器

2021-09-29 19:38:59 字數 4249 閱讀 4831

bk樹或者稱為burkhard-keller樹,是一種基於樹的資料結構,被設計於快速查詢近似字串匹配,比方說拼寫檢查器,或模糊查詢,當搜尋」aeek」時能返回」seek」和」peek」。為何bk-trees這麼酷,因為除了窮舉搜尋,沒有其他顯而易見的解決方法,並且它能以簡單和優雅的方法大幅度提公升搜尋速度。

除了字串匹配、查詢回文串、查詢重複子串等經典問題以外,日常生活中我們還會遇到其它一些怪異的字串問題。比如,有時我們需要知道給定的兩個字串「有多像」,換句話說兩個字串的相似度是多少。2023年,**科學家vladimir levenshtein給字串相似度做出了乙個明確的定義叫做levenshtein距離,我們通常叫它「編輯距離」。字串a到b的編輯距離是指,只用插入、刪除和替換三種操作,最少需要多少步可以把a變成b。例如,從fame到gate需要兩步(兩次替換),從game到acm則需要三步(刪除g和e再新增c)。levenshtein給出了編輯距離的一般求法,就是大家都非常熟悉的經典動態規劃問題。

在自然語言處理中,這個概念非常重要,例如我們可以根據這個定義開發出一套半自動的校對系統:查詢出一篇文章裡所有不在字典裡的單詞,然後對於每個單詞,列出字典裡與它的levenshtein距離小於某個數n的單詞,讓使用者選擇正確的那乙個。n通常取到2或者3,或者更好地,取該單詞長度的1/4等等。這個想法倒不錯,但演算法的效率成了新的難題:查字典好辦,建乙個trie樹即可;但怎樣才能快速在字典裡找出最相近的單詞呢?這個問題難就難在,levenshtein的定義可以是單詞任意位置上的操作,似乎不遍歷字典是不可能完成的。現在很多軟體都有拼寫檢查的功能,提出更正建議的速度是很快的。它們到底是怎麼做的呢?2023年,burkhard和keller提出的bk樹有效地解決了這個問題。這個資料結構強就強在,它初步解決了乙個看似不可能的問題,而其原理非常簡單。

首先,我們觀察levenshtein距離的性質。令d(x,y)表示字串x到y的levenshtein距離,那麼顯然:

1. d(x,y) = 0 當且僅當 x=y  (levenshtein距離為0 <==> 字串相等)

2. d(x,y) = d(y,x)     (從x變到y的最少步數就是從y變到x的最少步數)

3. d(x,y) + d(y,z) >= d(x,z)  (從x變到z所需的步數不會超過x先變成y再變成z的步數)

最後這乙個性質叫做三角形不等式。就好像乙個三角形一樣,兩邊之和必然大於第三邊。給某個集合內的元素定義乙個二元的「距離函式」,如果這個距離函式同時滿足上面說的三個性質,我們就稱它為「度量空間」。我們的三維空間就是乙個典型的度量空間,它的距離函式就是點對的直線距離。度量空間還有很多,比如manhattan距離,圖論中的最短路,當然還有這裡提到的levenshtein距離。就好像並查集對所有等價關係都適用一樣,bk樹可以用於任何乙個度量空間。

建樹的過程有些類似於trie。首先我們隨便找乙個單詞作為根(比如game)。以後插入乙個單詞時首先計算單詞與根的levenshtein距離:如果這個距離值是該節點處頭一次出現,建立乙個新的兒子節點;否則沿著對應的邊遞迴下去。例如,我們插入單詞fame,它與game的距離為1,於是新建乙個兒子,連一條標號為1的邊;下一次插入gain,算得它與game的距離為2,於是放在編號為2的邊下。再下次我們插入gate,它與game距離為1,於是沿著那條編號為1的邊下去,遞迴地插入到fame所在子樹;gate與fame的距離為2,於是把gate放在fame節點下,邊的編號為2。

查詢操作異常方便。如果我們需要返回與錯誤單詞距離不超過n的單詞,這個錯誤單詞與樹根所對應的單詞距離為d,那麼接下來我們只需要遞迴地考慮編號在d-n到d+n範圍內的邊所連線的子樹。由於n通常很小,因此每次與某個節點進行比較時都可以排除很多子樹。

舉個例子,假如我們輸入乙個gaie,程式發現它不在字典中。現在,我們想返回字典中所有與gaie距離為1的單詞。我們首先將gaie與樹根進行比較,得到的距離d=1。由於levenshtein距離滿足三角形不等式,因此現在所有離game距離超過2的單詞全部可以排除了。比如,以aim為根的子樹到game的距離都是3,而game和gaie之間的距離是1,那麼aim及其子樹到gaie的距離至少都是2。於是,現在程式只需要沿著標號範圍在1-1到1+1裡的邊繼續走下去。我們繼續計算gaie和fame的距離,發現它為2,於是繼續沿標號在1和3之間的邊前進。遍歷結束後回到game的第二個節點,發現gaie和gain距離為1,輸出gain並繼續沿編號為1或2的邊遞迴下去(那條編號為4的邊連線的子樹又被排除掉了)……

實踐表明,一次查詢所遍歷的節點不會超過所有節點的5%到8%,兩次查詢則一般不會17-25%,效率遠遠超過暴力列舉。適當進行快取,減小levenshtein距離常數n可以使演算法效率更高。

求算兩個字串之間的編輯距離

簡述設a和b是兩個字串,要用最少的字元操作將字串a轉換為字串b

字串操作包括,

1)刪除乙個字元

2)插入乙個字元

3)將乙個字元改為另乙個字元

演算法:模擬構造乙個(m + 1)行,(n+1)列的**

每一次都是在前一次的計算結果下,得到當前的值

首先是三個特殊情況 用srcstr表示源字串,dststr 表示目標字串

1)    兩個空字串的編輯距離d(srcstr, dststr) = 0

2)   如果srcstr為空,dststr不為空,則d(srcstr, dststr) = dststr.length(), 即在原空字串上新增字元,形成dststr

3)   如果dststr為空,srcstr不為空,則d(srcstr, dststr) = srcstr.length(), 及在源字串上刪除其所有字元,直至為空

例子:下面實際解決一下從srcstr = "bd"  到 dststr = "abcd"的過程,

上面這三種情況分別是初始化的時候要做的

首先用一維陣列表示兩位陣列

縱向 i = 0 -> m+1 , d[i * (n + 1)] = i 

橫向 i = 0 -> n+1, d[i] = i

即:如下圖是初始化之後的**資訊,縱向是b,d   橫向是a,b,c,d

步驟:for(i = 1 -> 2)     //  2為「bd"的長度

for( j = 1 -> 4 )   // 4 為」abcd"的長度

為了確定d[ i ][ j ]的大小, 需要比較

a)  從d[ i - 1 ][j - 1] 修改字元srcstr[i - 1], 使之變為dststr[j - 1], 如果srcstr[i - 1] == dststr[j - 1] 則這一步可以免去

b)  從d[ i - 1 ][ j ]  在srcstr的[ i - 1]處新增乙個字元,使字元srcstr[ i - 1 ]變為dststr[ j - 1 ]

c)  從d[ i ][ j - 1 ] 在dststr的[ j - 1 ]處刪除乙個字元, 使字元dststr[ j - 1 ]變為srcstr[ i - 1]

三者之間的最小值賦給d[ i ][ j ]

例如:s=「eeba」   t="abac" 

我們發現當s只有乙個字元e、t只有乙個字元a的時候,我們馬上就能得到s和t的編輯距離edit(0,0)=1(將e替換成a)。那麼如果s中有1個

字元e、t中有兩個字元ab的時候,我們是不是可以這樣分解:edit(0,1)=edit(0,0)+1(將e替換成a後,在新增乙個b)。如果s中有

兩個字元ee,t中有兩個字元ab的時候,我們是不是可以分解成:edit(1,1)=min(edit(0,1)+1, edit(1,0)+1, 

edit(0,0)+f(1,1)). 這樣我們可以得到這樣一些動態規劃公式:       

如果i=0且j=0        edit(0, 0)=1

如果i=0且j>0        edit(0, j )=edit(0, j-1)+1

如果i>0且j=0        edit( i, 0 )=edit(i-1, 0)+1

如果i>0且j>0        edit(i, j)=min(edit(i-1, j)+1, edit(i,j-1)+1, edit(i-1,j-1)+f(i , j) )

小注:edit(i,j)表示s中[0.... i]的子串si到t中[0....j]的子串tj的編輯距離。

f(i,j)表示s中第i個字元s(i)轉換到t中第j個字元s(j)所需要的操作次數,

如果s(i)==s(j),則不需要任何操作f(i, j)=0; 否則,需要替換操作,f(i, j)=1

實際效果比之前寫的多執行緒暴力慢多了.......

2.基數估計

3.噴泉碼

4.同型雜湊

5.levenshtein自動機

6.用四叉樹和希爾伯特曲線做空間索引

7.日誌結構化儲存

8.分組密碼與安全排列

9.字謎樹

拼寫檢查器

class spellcheck object 拼寫糾錯 def init self,file name content open file name read self.words re.findall a za z content self.alphabet abcdefghijklmnopqr...

python 拼寫檢查

無意中刷微博看到這篇文章作者用很簡短的語句寫了乙個拼寫檢查的python程式。看完之後發現原來拼寫檢查的原理是這樣的,之前感覺應該是很高深的東西。但是由於對python中lambda表示式的不怎麼理解於是,我就又憑著自己的理解簡單的寫了一遍。沒有原文中作者的優化部分,只是簡單的實現了功能。而且是只有...

單詞拼寫檢查

給定n個單詞 n 10000 給定m個查詢 輸出無法查詢見的單詞的個數 分析 貌似是雜湊表的入門題?我覺得好難。做雜湊表需要一些技巧的,大概就是對單詞的首 中 尾的字元順序碼加權,方便插入也方便查詢。遇到be和bee這種單詞可能費一些時間,不過總的來說還是很快的。單詞拼寫檢查 include inc...