拼写纠错的利器,BK树算法
BK树或者称为Burkhard-Keller树,是一种基于树的数据结构,被设计于快速查找近似字符串匹配,比方说拼写纠错,或模糊查找,当搜索”aeek”时能返回”seek”和”peek”。
本文首先剖析了基本原理,并在后面给出了Java源码实现。
BK树在1973年由Burkhard和Keller第一次提出,论文在这《Some approaches to best match file searching》。这是网上唯一的ACM存档,需要订阅。更细节的内容,可以阅读这篇论文《Fast Approximate String Matching in a Dictionary》。
在定义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树的创建、添加、查询:
package inteldt.todonlp.spellchecker; import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.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(原文)
* @author yifeng
*
*/
public class BKTree<T>{
private final MetricSpace<T> metricSpace; private Node<T> root; public BKTree(MetricSpace<T> metricSpace) {
this.metricSpace = metricSpace;
} /**
* 根据某一个集合元素创建BK树
*
* @param ms
* @param elems
* @return
*/
public static <E> BKTree<E> mkBKTree(MetricSpace<E> ms, Collection<E> elems) { BKTree<E> bkTree = new BKTree<E>(ms); for (E elem : elems) {
bkTree.put(elem);
} return bkTree;
} /**
* BK树中添加元素
*
* @param term
*/
public void put(T term) {
if (root == null) {
root = new Node<T>(term);
} else {
root.add(metricSpace, term);
}
} /**
* 查询相似元素
*
* @param term
* 待查询的元素
* @param radius
* 相似的距离范围
* @return
* 满足距离范围的所有元素
*/
public Set<T> query(T term, double radius) { Set<T> results = new HashSet<T>(); if (root != null) {
root.query(metricSpace, term, radius, results);
} return results;
} private static final class Node<T> { private final T value; /**
* 用一个map存储子节点
*/
private final Map<Double, Node<T>> children; public Node(T term) {
this.value = term;
this.children = new HashMap<Double, BKTree.Node<T>>();
} public void add(MetricSpace<T> ms, T value) {
// value与父节点的距离
Double distance = ms.distance(this.value, value); // 距离为0,表示元素相同,返回
if (distance == 0) {
return;
} // 从父节点的子节点中查找child,满足距离为distance
Node<T> child = children.get(distance); if (child == null) {
// 若距离父节点为distance的子节点不存在,则直接添加一个新的子节点
children.put(distance, new Node<T>(value));
} else {
// 若距离父节点为distance子节点存在,则递归的将value添加到该子节点下
child.add(ms, value);
}
} public void query(MetricSpace<T> ms, T term, double radius, Set<T> results) { 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<T> child = children.get(i); // 递归调用
if (child != null) {
child.query(ms, term, radius, results);
} }
}
}
}
距离度量方法接口:
package inteldt.todonlp.spellchecker; /**
* 度量空间
*
* @author yifeng
*
* @param <T>
*/
public interface MetricSpace<T> { double distance(T a, T b); }
编辑距离:
package inteldt.todonlp.spellchecker; /**
* 编辑距离, 又称Levenshtein距离,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数。
* 该类中许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。
*
* 使用动态规划算法。算法复杂度:m*n。
*
* @author yifeng
*
*/
public class LevensteinDistance implements MetricSpace<String>{
private double insertCost = 1; // 可以写成插入的函数,做更精细化处理
private double deleteCost = 1; // 可以写成删除的函数,做更精细化处理
private double substitudeCost = 1.5; // 可以写成替换的函数,做更精细化处理。比如使用键盘距离。 public double computeDistance(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;
}
} return distance[n][m];
} @Override
public double distance(String a, String b) {
return computeDistance(a,b);
} public static void main(String[] args) {
LevensteinDistance distance = new LevensteinDistance();
System.out.println(distance.computeDistance("你好","好你"));
}
}
有了以上三个类,下面写一个main函数玩起纠错功能:
package inteldt.todonlp.spellchecker; import java.util.Set; /**
* 拼写纠错
*
* @author yifeng
*
*/
public class SpellChecker {
public static void main(String args[]) {
double radius = 1.5; // 编辑距离阈值
String term = "helli"; // 待纠错的词 // 创建BK树
MetricSpace<String> ms = new LevensteinDistance();
BKTree<String> bk = new BKTree<String>(ms); bk.put("hello");
bk.put("shell");
bk.put("holl"); Set<String> set = bk.query(term, radius);
System.out.println(set.toString()); }
} 输出:[hello]
如果您觉得博文对您有用,请随意打赏。您的鼓励是我前进的动力!
拼写纠错的利器,BK树算法的更多相关文章
- elasticsearch拼写纠错之Term Suggester
一.什么是拼写纠错 拼写纠错就是搜索引擎可以智能的感知用户输入关键字的错误,并使用纠正过的关键字进行搜索展示给用户:拼写纠错是一种改善用户体验的功能: elasticsearch提供了以下不同类型的s ...
- (转载)搜索引擎的Query自动纠错技术和架构详解
from http://www.52nlp.cn/%E8%BE%BE%E8%A7%82%E6%95%B0%E6%8D%AE%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E%E7 ...
- NLP系列(1)_从破译外星人文字浅谈自然语言处理基础
作者:龙心尘 &&寒小阳 时间:2016年1月. 出处: http://blog.csdn.net/longxinchen_ml/article/details/50543337 ht ...
- 面试小结之Elasticsearch篇(转)
最近面试一些公司,被问到的关于Elasticsearch和搜索引擎相关的问题,以及自己总结的回答. Elasticsearch是如何实现Master选举的? Elasticsearch的选主是ZenD ...
- python就业班-淘宝-目录.txt
卷 TOSHIBA EXT 的文件夹 PATH 列表卷序列号为 AE86-8E8DF:.│ python就业班-淘宝-目录.txt│ ├─01 网络编程│ ├─01-基本概念│ │ 01-网络通信概述 ...
- Elasticsearch 疑难解惑
Elasticsearch是如何实现Master选举的? Elasticsearch的选主是ZenDiscovery模块负责的,主要包含Ping(节点之间通过这个RPC来发现彼此)和Unicast(单 ...
- NLP系列(1)_从破译外星人文字浅谈自然语言处理的基础
作者:龙心尘 &&寒小阳 时间:2016年1月. 出处: http://blog.csdn.net/longxinchen_ml/article/details/50543337, h ...
- Levenshtein Distance算法(编辑距离算法)
编辑距离 编辑距离(Edit Distance),又称Levenshtein距离,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数.许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符, ...
- 常用python机器学习库总结
开始学习Python,之后渐渐成为我学习工作中的第一辅助脚本语言,虽然开发语言是Java,但平时的很多文本数据处理任务都交给了Python.这些年来,接触和使用了很多Python工具包,特别是在文本处 ...
随机推荐
- SAP 月结F.19与GR/IR
http://blog.sina.com.cn/s/blog_3eeba40101008v75.html 为什么要做月结?月结究竟都结些啥? 月结的目的和手段都不知道,只知道一部分.月结,为了出资产负 ...
- Quartz格式设置说明
这些星号由左到右按顺序代表 : * * * * * * * 格式 ...
- windows消息简单应用实例
//基本定义 internal class MyMessager : IMessageFilter { public bool PreFilterMessage(ref Message m) { // ...
- Charles从入门到放弃
Charles版本:4.0.2 一.开始 连接方式 方法一:电脑和手机连接同一个wifi 方法二:电脑使用网线连接网络,手机通过USB连接电脑 二.过滤网络请求 1.简单过滤 在Sequence模式下 ...
- 二、Tomcat配置以及IDEA运行第一个Jsp项目——JavaWeb点滴
一.Tomcat配置环境变量 tomcat从官网下载最新的即可,本人下载的是安装版本.在安装过程中需要设置用户名和密码以及选择相应的JDK的安装目录.这些都比较简单直接下一步即可,安装完成之后就是配置 ...
- mysql开启慢查询日志以及查看(转载自网络)
转载自http://database.51cto.com/art/201309/410314_1.htm
- 如何用java创建一个jdbc程序
第一个jdbc程序 JDBC简介 Java数据库连接(Java Database Connectivity,JDBC),是一种用于执行SQL语句的Java API,它由一组用Java编程语言编写的类和 ...
- QT信号和槽
QT信号和槽 ============ 信号和槽是一种高级接口,应用于对象之间的通信,它是 QT 的核心特性.要正确的处理信号和槽,必须借助一个称为 moc(Meta Object Compiler) ...
- Muduo阅读笔记---入门(一)
第一步:下载源码和文档 下载muduo项目的源码.<muduo-manual.pdf>文档,以及<Linux多线程服务端编程:使用muduo C++网络库.pdf>,这些是前期 ...
- linux中的权限
第1章 显示或设置网络相关信息 1.1 ip address 与ifconfig 类似 [root@znix ~]# ip address 1: lo: <LOOPBACK,UP,LOWER_U ...