BK Tree或Burkhard Keller Tree是一种数据结构,用于根据编辑距离(Levenshtein距离)概念执行拼写检查。 BK树也用于近似字符串匹配。基于该数据结构,可以实现许多软件中的各种自动校正特征。
假设我们有一个单词字典,然后我们有一些其他的单词要在字典中检查拼写错误。我们需要收集字典中与给定单词非常接近的所有单词。例如,如果我们检查一个单词“ruk”,我们将有{“truck”,“buck”,“duck”,......}。因此,拼写错误可以通过删除单词中的字符或在单词中添加新字符或通过将单词中的字符替换为适当的字符来更正。因此,我们将使用编辑距离作为衡量错误拼写单词与字典中单词的正确性和匹配的度量。
现在,让我们看看我们的BK树的结构。与所有其他树一样,BK树由节点和边组成。 BK树中的节点将代表我们字典中的单个单词,并且节点的数量与字典中单词的数量完全相同。边将包含一些整数权重,它将告诉我们从一个节点到另一个节点的编辑距离。假设我们有一个从节点u到节点v的边缘有一些边缘权重w,那么w是将字符串u转换为v所需的编辑距离。
考虑字典集合:{“help”,“hell”,“hello”}。因此,对于这一字典集合,我们的BK树将如下所示。
BK树中的每个节点都只有一个具有相同编辑距离的子节点。如果我们在插入时遇到编辑距离的某些冲突,我们将向子集传播插入过程,直到找到字符串节点的适当父节点。
BK树中的每个插入都将从根节点开始。根节点可以是字典中的任何单词。
例如,在上面的字典中添加另一个单词“shell”。现在Dict [] = {“help”,“hell”,“hello”,“shell”}。现在显而易见的是,“shell”具有与“hello”相同的编辑距离来自根节点“help”,即2.因此,我们遇到了碰撞。因此,我们通过在预先存在的冲突节点上递归地执行此插入过程来处理此冲突。
因此,现在我们不是在根节点“help”处插入“shell”,而是将它插入到碰撞节点“hello”。现在新的节点“shell”被添加到树中,它的节点“hello”作为其父节点,edge-weigth为2(编辑距离)。下图描述了插入后的BK树。
所以,到现在为止,我们已经了解了如何构建我们的BK树。现在,问题是如何找到拼写错误的单词最接近的正确单词?首先,我们需要设置容差值。此容差值只是从拼写错误的单词到字典中正确单词的最大编辑距离。因此,要在容差限度内找到符合条件的正确单词,Naive方法将迭代字典中的所有单词并收集容差限制内的单词。但是这种方法具有O(n * m * n)时间复杂度(n是dict []中的单词数,m是正确单词的平均大小,n是拼写错误单词的长度),对于较大的字典大小超时。
因此,可以用BK树解决这一问题。我们知道BK树中的每个节点都是根据其父节点的编辑距离度量构建的。因此,我们将直接从根节点到达容差限制内的特定节点。让我们说,我们的容差限制是TOL,当前节点与拼写错误的单词的编辑距离是dist。因此,现在不是迭代它的所有子节点,而是迭代它在范围内具有编辑距离的子节点 [dist-TOL,dist + TOL]。这将在很大程度上降低我们的复杂性。我们将在时间复杂度分析中讨论这个问题。
考虑下面构造的BK树。
假设我们有一个拼写错误的单词“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 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 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 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;
}