BK树或者称为Burkhard-Keller树,是一种基于树的数据结构,被设计于快速查找近似字符串匹配,比方说拼写纠错,或模糊查找,当搜索”aeek”时能返回”seek”和”peek”。

本文首先剖析了基本原理,并在后面给出了Java源码实现。

在定义BK树之前,我们需要预先定义一些操作。为了索引和搜索字典,我们需要一种比较字符串的方法。编辑距离( Levenshtein Distance)是一种标准的方法,它用来表示经过插入、删除和替换操作从一个字符串转换到另外一个字符串的最小操作步数。其它字符串函数也同样可接受(比如将调换作为原子操作),只要能满足以下一些条件。

现在我们观察下编辑距离:构造一个度量空间(Metric Space),该空间内任何关系满足以下三条基本条件:

d(x,y) = 0 x = y (假如x与y的距离为0,则x=y)

d(x,y) = d(y,x) (x到y的距离等同于y到x的距离)

d(x,y) + d(y,z) >= d(x,z)

上述条件中的最后一条被叫做三角不等式(Triangle Inequality)。三角不等式表明x到z的路径不可能长于另一个中间点的任何路径(从x到y再到z)。看下三角形,你不可能从一点到另外一点的两侧再画出一条比它更短的边来。

编辑距离符合基于以上三条所构造的度量空间。请注意,有其它更为普遍的空间,比如欧几里得空间(Euclidian Space),编辑距离不是欧几里得的。既然我们了解了编辑距离(或者其它类似的字符串距离函数)所表达的度量的空间,再来看下Burkhard和Keller所观察到的关键结论。

假设现在我们有两个参数,query表示我们搜索的字符串,n为待查找的字符串与query距离满足要求的最大距离,我们可以拿任意字符串A来跟query进行比较,计算距离为d,因为我们知道三角不等式是成立的,则满足与query距离在n范围内的另一个字符转B,其与A的距离最大为d+n,最小为d-n。

推论如下:

d(query, B) + d(B, A) >= d(query, A),  即 d(query, B) + d(A,B) >= d

-->  d(A,B) >= d - d(query, B) >= d - n

d(A, B) <= d(A,query) + d(query, B),   即 d(A, B) <= d + d(query, B) <= d + n

其实,还可以得到 d(query, A) + d(A,B) >= d(query, B)

--> d(A,B) >= d(query, B) - d(query, A)

--> d(A,B) >= 1 - d >= 0 (query与B不等) 由于 A与B不是同一个字符串,所以d(A,B)>=1

所以, min{1, d - n} <= d(A,B) <= d + n,这是更为完整的结论。

由此,BK树的构造就过程如下:

每个节点有任意个子节点,每条边有个值表示编辑距离。所有子节点到父节点的边上标注n表示编辑距离恰好为n。比如,我们有棵树父节点是”book”和两个子节点”rook”和”nooks”,”book”到”rook”的边标号1,”book”到”nooks”的边上标号2。

从字典里构造好树后,无论何时你想插入新单词时,计算该单词与根节点的编辑距离,并且查找数值为d(neweord, root)的边。递归得与各子节点进行比较,直到没有子节点,你就可以创建新的子节点并将新单词保存在那。比如,插入”boon”到刚才上述例子的树中,我们先检查根节点,查找d(“book”, “boon”) = 1的边,然后检查标号为1的边的子节点,得到单词”rook”。我们再计算距离d(“rook”, “boon”)=2,则将新单词插在”rook”之后,边标号为2。

查询相似词如下:

计算单词与根节点的编辑距离d,然后递归查找每个子节点标号为d-n到d+n(包含)的边。假如被检查的节点与搜索单词的距离d小于n,则返回该节点并继续查询。

BK树是多路查找树,并且是不规则的(但通常是平衡的)。试验表明,1个查询的搜索距离不会超过树的5-8%,并且2个错误查询的搜索距离不会超过树的17-25%,这可比检查每个节点改进了一大步啊!需要注意的是,如果要进行精确查找,也可以非常有效地通过简单地将n设置为0进行。

英文原文:http://blog.notdot.net/2007/4/Damn-Cool-Algorithms-Part-1-BK-Trees

本文给出一个Java源码如下,相当简洁,注释清楚:

BK树的创建、添加、查询:

packageinteldt.todonlp.spellchecker;importjava.util.Collection;importjava.util.HashMap;importjava.util.HashSet;importjava.util.Map;importjava.util.Set;/*** BK树,可以用来进行拼写纠错查询

*

* 1.度量空间。

* 距离度量空间满足三个条件:

* d(x,y) = 0 x = y (假如x与y的距离为0,则x=y)

* d(x,y) = d(y,x) (x到y的距离等同于y到x的距离)

* d(x,y) + d(y,z) >= d(x,z) (三角不等式)

*

* 2、编辑距离( Levenshtein Distance)符合基于以上三条所构造的度量空间

*

* 3、重要的一个结论:假设现在我们有两个参数,query表示我们搜索的字符串(以字符串为例),

* n为待查找的字符串与query最大距离范围,我们可以拿一个字符串A来跟query进行比较,计

* 算距离为d。根据三角不等式是成立的,则满足与query距离在n范围内的另一个字符转B,

* 其余与A的距离最大为d+n,最小为d-n。

*

* 推论如下:

* d(query, B) + d(B, A) >= d(query, A), 即 d(query, B) + d(A,B) >= d --> d(A,B) >= d - d(query, B) >= d - n

* d(A, B) <= d(A,query) + d(query, B), 即 d(query, B) <= d + d(query, B) <= d + n

* 其实,还可以得到 d(query, A) + d(A,B) >= d(query, B)

* --> d(A,B) >= d(query, B) - d(query, A)

* --> d(A,B) >= 1 - d >= 0 (query与B不等) 由于 A与B不是同一个字符串d(A,B)>=1

* 所以, min{1, d - n} <= d(A,B) <= d + n

*

* 利用这一特点,BK树在实现时,子节点到父节点的权值为子节点到父节点的距离(记为d1)。

* 若查找一个元素的相似元素,计算元素与父节点的距离,记为d, 则子节点中能满足要求的

* 相似元素,肯定是权值在d - n <= d1 <= d + n范围内,当然了,在范围内,与查找元素的距离也未必一定符合要求。

* 这相当于在查找时进行了剪枝,然不需要遍历整个树。试验表明,距离为1范围的查询的搜索距离不会超过树的5-8%,

* 并且距离为2的查询的搜索距离不会超过树的17-25%。

* 参见:

*http://blog.notdot.net/2007/4/Damn-Cool-Algorithms-Part-1-BK-Trees(原文)*@authoryifeng

**/

public class BKTree{private final MetricSpacemetricSpace;private Noderoot;public BKTree(MetricSpacemetricSpace) {this.metricSpace =metricSpace;

}/*** 根据某一个集合元素创建BK树

*

*@paramms

*@paramelems

*@return

*/

public static BKTree mkBKTree(MetricSpace ms, Collectionelems) {

BKTree bkTree = new BKTree(ms);for(E elem : elems) {

bkTree.put(elem);

}returnbkTree;

}/*** BK树中添加元素

*

*@paramterm*/

public voidput(T term) {if (root == null) {

root= new Node(term);

}else{

root.add(metricSpace, term);

}

}/*** 查询相似元素

*

*@paramterm

* 待查询的元素

*@paramradius

* 相似的距离范围

*@return* 满足距离范围的所有元素*/

public Set query(T term, doubleradius) {

Set results = new HashSet();if (root != null) {

root.query(metricSpace, term, radius, results);

}returnresults;

}private static final class Node{private finalT value;/*** 用一个map存储子节点*/

private final Map>children;publicNode(T term) {this.value =term;this.children = new HashMap>();

}public void add(MetricSpacems, T value) {//value与父节点的距离

Double distance = ms.distance(this.value, value);//距离为0,表示元素相同,返回

if (distance == 0) {return;

}//从父节点的子节点中查找child,满足距离为distance

Node child =children.get(distance);if (child == null) {//若距离父节点为distance的子节点不存在,则直接添加一个新的子节点

children.put(distance, new Node(value));

}else{//若距离父节点为distance子节点存在,则递归的将value添加到该子节点下

child.add(ms, value);

}

}public void query(MetricSpace ms, T term, double radius, Setresults) {double distance = ms.distance(this.value, term);//与父节点的距离小于阈值,则添加到结果集中,并继续向下寻找

if (distance <=radius) {

results.add(this.value);

}//子节点的距离在最小距离和最大距离之间的。//由度量空间的d(x,y) + d(y,z) >= d(x,z)这一定理,有查找的value与子节点的距离范围如下://min = {1,distance -radius}, max = distance + radius

for (double i = Math.max(distance - radius, 1); i <= distance + radius; ++i) {

Node child =children.get(i);//递归调用

if (child != null) {

child.query(ms, term, radius, results);

}

}

}

}

}

距离度量方法接口:

packageinteldt.todonlp.spellchecker;/*** 度量空间

*

*@authoryifeng

*

*@param*/

public interface MetricSpace{doubledistance(T a, T b);

}

编辑距离:

packageinteldt.todonlp.spellchecker;/*** 编辑距离, 又称Levenshtein距离,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数。

* 该类中许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。

*

* 使用动态规划算法。算法复杂度:m*n。

*

*@authoryifeng

**/

public class LevensteinDistance implements MetricSpace{private double insertCost = 1; //可以写成插入的函数,做更精细化处理

private double deleteCost = 1; //可以写成删除的函数,做更精细化处理

private double substitudeCost = 1.5; //可以写成替换的函数,做更精细化处理。比如使用键盘距离。

public doublecomputeDistance(String target,String source){int n =target.trim().length();int m =source.trim().length();double[][] distance = new double[n+1][m+1];

distance[0][0] = 0;for(int i = 1; i <= m; i++){

distance[0][i] =i;

}for(int j = 1; j <= n; j++){

distance[j][0] =j;

}for(int i = 1; i <= n; i++){for(int j = 1; j <=m; j++){double min = distance[i-1][j] +insertCost;if(target.charAt(i-1) == source.charAt(j-1)){if(min > distance[i-1][j-1])

min= distance[i-1][j-1];

}else{if(min > distance[i-1][j-1] +substitudeCost)

min= distance[i-1][j-1] +substitudeCost;

}if(min > distance[i][j-1] +deleteCost){

min= distance[i][j-1] +deleteCost;

}

distance[i][j]=min;

}

}returndistance[n][m];

}

@Overridepublic doubledistance(String a, String b) {returncomputeDistance(a,b);

}public static voidmain(String[] args) {

LevensteinDistance distance= newLevensteinDistance();

System.out.println(distance.computeDistance("你好","好你"));

}

}

有了以上三个类,下面写一个main函数玩起纠错功能:

packageinteldt.todonlp.spellchecker;importjava.util.Set;/*** 拼写纠错

*

*@authoryifeng

**/

public classSpellChecker {public static voidmain(String args[]) {double radius = 1.5; //编辑距离阈值

String term = "helli"; //待纠错的词//创建BK树

MetricSpace ms = newLevensteinDistance();

BKTree bk = new BKTree(ms);

bk.put("hello");

bk.put("shell");

bk.put("holl");

Set set =bk.query(term, radius);

System.out.println(set.toString());

}

}

输出:[hello]

如果您觉得博文对您有用,请随意打赏。您的鼓励是我前进的动力!

java 拼写纠错_拼写纠错的利器,BK树算法相关推荐

  1. lstm 文本纠错_工业界纠错系统

    本篇文章,主要来唠嗑下工业界的纠错系统怎么设计?包括:基于规则的纠错系统(简单的英文纠错.复杂的中文纠错).基于NN的纠错系统.当然,在成熟的工业界纠错系统中,最好是结合规则&&NN方 ...

  2. python拼写检查_拼写检查 - Python文本处理教程™

    检查拼写是任何文本处理或分析的基本要求. python中的pyspellchecker包提供了这个功能,可以找到可能错误拼写的单词,并建议可能的更正. 首先,我们需要在python环境中使用以下命令安 ...

  3. 拼写检查_拼写检查属性

    拼写检查 Many useful attributes have been provided to web developers recently:  download, placeholder, a ...

  4. 中文拼写纠错_[NLP]中文拼写检纠错

    一.基于统计语言模型的中文拼写纠错 1.流程图 2.实验结果 局部方法的实验结果: 全局方法的实验结果: 3.初步结论 缺点: a.SLM对训练语料的规模和质量敏感. b.错词检测策略灵活,变化较多. ...

  5. 多模型结合的等长拼写纠错 | 全国中文纠错大赛冠军方案

    每天给你送来NLP技术干货! 来自:达观数据 点击这里进群->加入NLP交流群 参与任务  DATAGRAND 中文拼写检查任务是中文自然语言处理中非常具有代表性和挑战性的任务,其本质是找出文本 ...

  6. nlp 中文文本纠错_百度中文纠错技术

    原标题:百度中文纠错技术 分享嘉宾:付志宏 百度资深研发工程师 编辑整理:李润顺 内容来源:Baidu Brain & DataFun AI Talk<百度中文纠错技术> 出品社区 ...

  7. JVM - 结合代码示例彻底搞懂Java内存区域_对象在堆-栈-方法区(元空间)之间的关系

    文章目录 Pre 示例demo 总体关系 代码示例论证 反汇编 Pre JVM - 结合代码示例彻底搞懂Java内存区域_线程栈 | 本地方法栈 | 程序计数器 中我们探讨了线程栈中的内部结构 ,大家 ...

  8. Java计算时间差_传统的SimpleDateFormat类

    Java计算时间差_传统的SimpleDateFormat类 SimpleDateFormat simpleFormat = new SimpleDateFormat("yyyy-MM-dd ...

  9. JAVA异常使用_每个人都曾用过、但未必都用得好

    JAVA异常使用_每个人都曾用过.但未必都用得好 一.抛出异常 vs. 返回错误代码 有人说"Well, an exception is a goto.",但也有人言"m ...

最新文章

  1. JAVA三元运算符排序三个随机数
  2. ASP.NET Core MVC上传、导入、导出知多少
  3. 范例解析:学习Android的IPC主板模式
  4. python可以使用二维元组吗_python中读入二维csv格式的表格方法详解(以元组/列表形式表示)...
  5. 设计模式--六大原则
  6. 【Java2】一维数组,家庭收支界面,/属性和方法,方法重载,变量分类,可变参数,包
  7. zend server 配置问题 ZendEnablerConf.xml
  8. NeurlPS 2019丨微软亚洲研究院 5 篇精选论文解读
  9. Nagios配置监控windows客户端
  10. 【java笔记】继承与多态
  11. R语言︱缺失值处理之多重插补——mice包
  12. java完全自学手册(java完全自学手册pdf洪维恩)
  13. python编程器手机版ios_手机最强Python编程神器,在手机上运行Python
  14. POI Excel设置列宽
  15. 计算机键盘的中心键,电脑键盘上各键的功能及作用
  16. xss靶场练习(一)之xss.haozi.me
  17. idea修改代码仓库地址和用户密码
  18. 网易云音乐python爬虫(Js破解)
  19. JQuery 基础知识学习(详尽版)
  20. JavaSE回顾笔记Ⅲ

热门文章

  1. C#中HttpWebRequest的GetRequestStream执行的效率太低,甚至偶尔死掉
  2. log4j.properties例子
  3. 计算机组成原理重点总结(学习笔记)含计算公式
  4. 1086 就不告诉你 (15 分)
  5. AD中给大电流开窗时把顶层动态铺铜Polygon变成阻焊层静态Region的方法
  6. static 关键字的详解
  7. Qt :圆圈加载进度条(转圈圈)
  8. 期货交易心得 Round 5
  9. totolink 虚拟服务器,TOTOLINK 路由器快速设置指南
  10. 《惢客创业日记》2020.10.26-31(周一)全麻手术会做梦吗?