BK Tree

BK Tree或Burkhard Keller Tree是一種數據結構,用於根據編輯距離(Levenshtein距離)概念執行拼寫檢查。 BK樹也用於近似字符串匹配。基於該數據結構,可以實現許多軟件中的各種自動校正特徵。

假設我們有一個單詞字典,然後我們有一些其他的單詞要在字典中檢查拼寫錯誤。我們需要收集字典中與給定單詞非常接近的所有單詞。例如,如果我們檢查一個單詞“ruk”,我們將有{“truck”,“buck”,“duck”,......}。因此,拼寫錯誤可以通過刪除單詞中的字符或在單詞中添加新字符或通過將單詞中的字符替換爲適當的字符來更正。因此,我們將使用編輯距離作爲衡量錯誤拼寫單詞與字典中單詞的正確性和匹配的度量。

現在,讓我們看看我們的BK樹的結構。與所有其他樹一樣,BK樹由節點和邊組成。 BK樹中的節點將代表我們字典中的單個單詞,並且節點的數量與字典中單詞的數量完全相同。邊將包含一些整數權重,它將告訴我們從一個節點到另一個節點的編輯距離。假設我們有一個從節點u到節點v的邊緣有一些邊緣權重w,那麼w是將字符串u轉換爲v所需的編輯距離。

考慮字典集合:{“help”,“hell”,“hello”}。因此,對於這一字典集合,我們的BK樹將如下所示。

 

image.png

BK樹中的每個節點都只有一個具有相同編輯距離的子節點。如果我們在插入時遇到編輯距離的某些衝突,我們將向子集傳播插入過程,直到找到字符串節點的適當父節點。

BK樹中的每個插入都將從根節點開始。根節點可以是字典中的任何單詞。

例如,在上面的字典中添加另一個單詞“shell”。現在Dict [] = {“help”,“hell”,“hello”,“shell”}。現在顯而易見的是,“shell”具有與“hello”相同的編輯距離來自根節點“help”,即2.因此,我們遇到了碰撞。因此,我們通過在預先存在的衝突節點上遞歸地執行此插入過程來處理此衝突。

因此,現在我們不是在根節點“help”處插入“shell”,而是將它插入到碰撞節點“hello”。現在新的節點“shell”被添加到樹中,它的節點“hello”作爲其父節點,edge-weigth爲2(編輯距離)。下圖描述了插入後的BK樹。

 

image.png

所以,到現在爲止,我們已經瞭解瞭如何構建我們的BK樹。現在,問題是如何找到拼寫錯誤的單詞最接近的正確單詞?首先,我們需要設置容差值。此容差值只是從拼寫錯誤的單詞到字典中正確單詞的最大編輯距離。因此,要在容差限度內找到符合條件的正確單詞,Naive方法將迭代字典中的所有單詞並收集容差限制內的單詞。但是這種方法具有O(n * m * n)時間複雜度(n是dict []中的單詞數,m是正確單詞的平均大小,n是拼寫錯誤單詞的長度),對於較大的字典大小超時。

因此,可以用BK樹解決這一問題。我們知道BK樹中的每個節點都是根據其父節點的編輯距離度量構建的。因此,我們將直接從根節點到達容差限制內的特定節點。讓我們說,我們的容差限制是TOL,當前節點與拼寫錯誤的單詞的編輯距離是dist。因此,現在不是迭代它的所有子節點,而是迭代它在範圍內具有編輯距離的子節點 [dist-TOL,dist + TOL]。這將在很大程度上降低我們的複雜性。我們將在時間複雜度分析中討論這個問題。

考慮下面構造的BK樹。

 

image.png

假設我們有一個拼寫錯誤的單詞“oop”,容差限制爲2.現在,我們將看到我們將如何收集給定拼寫錯誤單詞的預期正確值。

  • 迭代1:我們將開始檢查根節點的編輯距離。 D(“oop” - >“help”)= 3.現在我們將迭代其編輯距離範圍[D-TOL,D + TOL],即[1,5]
  • 迭代2:讓我們從最高可能的編輯距離中開始迭代,即節點“循環”與編輯距離4.現在我們將再次從拼寫錯誤的單詞中找到它的編輯距離。 D(“oop”,“loop”)= 1。
    這裏D = 1,即D <= TOL,所以我們將“循環”添加到預期的正確單詞列表並處理其子節點的編輯距離在[D-TOL,D + TOL]中,即[1,3]
  • 迭代3:現在,我們處於節點“troop”。我們將再一次檢查拼寫錯誤單詞的編輯距離。 D(“oop”,“troop”)= 2.再次D <= TOL,因此我們再次將“troop”添加到預期的正確單詞列表中。

對於從根節點開始直到最底部葉節點的[D-TOL,D + TOL]範圍內的所有單詞,我們將繼續相同。這類似於樹上的DFS遍歷,有選擇地訪問邊緣權重在某個給定範圍內的子節點。

因此,最後我們將只留下拼寫錯誤的單詞“oop”的2個預期單詞,即{“loop”,“troop”}

// C++ program to demonstrate working of BK-Tree 
#include "bits/stdc++.h" 
using namespace std; 

// maximum number of words in dict[] 
#define MAXN 100 

// defines the tolerence value 
#define TOL 2 

// defines maximum length of a word 
#define LEN 10 

struct Node 
{ 
    // stores the word of the current Node 
    string word; 

    // links to other Node in the tree 
    int next[2*LEN]; 

    // constructors 
    Node(string x):word(x) 
    { 
        // initializing next[i] = 0 
        for(int i=0; i<2*LEN; i++) 
            next[i] = 0; 
    } 
    Node() {} 
}; 

// stores the root Node 
Node RT; 

// stores every Node of the tree 
Node tree[MAXN]; 

// index for current Node of tree 
int ptr; 

int min(int a, int b, int c) 
{ 
    return min(a, min(b, c)); 
} 

// Edit Distance 
// Dynamic-Approach O(m*n) 
int editDistance(string& a,string& b) 
{ 
    int m = a.length(), n = b.length(); 
    int dp[m+1][n+1]; 

    // filling base cases 
    for (int i=0; i<=m; i++) 
        dp[i][0] = i; 
    for (int j=0; j<=n; j++) 
        dp[0][j] = j; 

    // populating matrix using dp-approach 
    for (int i=1; i<=m; i++) 
    { 
        for (int j=1; j<=n; j++) 
        { 
            if (a[i-1] != b[j-1]) 
            { 
                dp[i][j] = min( 1 + dp[i-1][j], // deletion 
                                1 + dp[i][j-1], // insertion 
                                1 + dp[i-1][j-1] // replacement 
                            ); 
            } 
            else
                dp[i][j] = dp[i-1][j-1]; 
        } 
    } 
    return dp[m][n]; 
} 

// adds curr Node to the tree 
void add(Node& root,Node& curr) 
{ 
    if (root.word == "" ) 
    { 
        // if it is the first Node 
        // then make it the root Node 
        root = curr; 
        return; 
    } 

    // get its editDist from the Root Node 
    int dist = editDistance(curr.word,root.word); 

    if (tree[root.next[dist]].word == "") 
    { 
        /* if no Node exists at this dist from root 
        * make it child of root Node*/

        // incrementing the pointer for curr Node 
        ptr++; 

        // adding curr Node to the tree 
        tree[ptr] = curr; 

        // curr as child of root Node 
        root.next[dist] = ptr; 
    } 
    else
    { 
        // recursively find the parent for curr Node 
        add(tree[root.next[dist]],curr); 
    } 
} 

vector <string> getSimilarWords(Node& root,string& s) 
{ 
    vector < string > ret; 
    if (root.word == "") 
    return ret; 

    // calculating editdistance of s from root 
    int dist = editDistance(root.word,s); 

    // if dist is less than tolerance value 
    // add it to similar words 
    if (dist <= TOL) ret.push_back(root.word); 

    // iterate over the string havinng tolerane 
    // in range (dist-TOL , dist+TOL) 
    int start = dist - TOL; 
    if (start < 0) 
    start = 1; 

    while (start < dist + TOL) 
    { 
        vector <string> tmp = 
            getSimilarWords(tree[root.next[start]],s); 
        for (auto i : tmp) 
            ret.push_back(i); 
        start++; 
    } 
    return ret; 
} 

// driver program to run above functions 
int main(int argc, char const *argv[]) 
{ 
    // dictionary words 
    string dictionary[] = {"hell","help","shel","smell", 
                        "fell","felt","oops","pop","oouch","halt"
                        }; 
    ptr = 0; 
    int sz = sizeof(dictionary)/sizeof(string); 

    // adding dict[] words on to tree 
    for(int i=0; i<sz; i++) 
    { 
        Node tmp = Node(dictionary[i]); 
        add(RT,tmp); 
    } 

    string w1 = "ops"; 
    string w2 = "helt"; 
    vector < string > match = getSimilarWords(RT,w1); 
    cout << "similar words in dictionary for : " << w1 << ":\n"; 
    for (auto x : match) 
        cout << x << endl; 

    match = getSimilarWords(RT,w2); 
    cout << "Correct words in dictionary for " << w2 << ":\n"; 
    for (auto x : match) 
        cout << x << endl; 

    return 0; 
} 



作者:SeanC52111
鏈接:https://www.jianshu.com/p/cedbd94f4f45
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章