拼写纠错的利器,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工具包,特别是在文本处 ...
随机推荐
- SQL 表结构操作
数据库知识总结(表结构操作) 1.创建表Scores 1 create table Scores --表名 2 (Id int identity(1,1) primary key,--设置主键,并且行 ...
- linux下开机不自动挂载指定分区
我的debian装好后,有保留windows,但是却不想在debian启动后桌面上,文件管理器中显示windows分区,留个记录在这里,需要的时候方便查看 使用mount 的 noauto参数: 创建 ...
- Java IO(IO流)-1
IO流 第一部分 (outputStream/InputStream Writer/Redaer) IO流对象中输入和输出是相辅相成的,输出什么,就可以输入什么. IO的命名方式为前半部分能干什么,后 ...
- 【转】浅谈UML的概念和模型之UML九种图
原文地址:浅谈UML的概念和模型之UML九种图 目录: UML的视图 UML的九种图 UML中类间的关系 上文我们介绍了,UML的视图,在每一种视图中都包含一个或多种图.本文我们重点讲解UML每种图的 ...
- JAVA基础知识总结:一
一.软件开发的常识 1.什么是软件? 一系列按照特定顺序组织起来的计算机数据或者指令 常见的软件: 系统软件:Windows\Mac OS \Linux 应用软件:QQ,一系列的播放器(爱奇艺,乐视, ...
- LeetCode 533. Lonely Pixel II (孤独的像素之二) $
Given a picture consisting of black and white pixels, and a positive integer N, find the number of b ...
- PhiloGL学习(4)——三维对象、加载皮肤
前言 上一篇文章中介绍了如何响应鼠标和键盘事件,本文介绍如何加载三维对象并实现给三维对象添加一个漂亮的皮肤. 一. 原理分析 我对三维的理解为:所谓三维对象无非是多个二维对象拼接到一起,贴图就更简单了 ...
- iOS之 Category 属性 的理解
在 Objective-C 中可以通过 Category 给一个现有的类添加属性,但是却不能添加实例变量 反正读第一遍的时候我是有点晕的,可以添加“属性”,然后又说“添加实例变量”,第一感觉就好像 有 ...
- .5-Vue源码之AST(1)
讲完了数据劫持原理和一堆初始化 现在是DOM相关的代码了 上一节是从这个函数开始的: // Line-3924 Vue.prototype._init = function(options) { // ...
- H5新特性汇总
H5新特性: 新增选择器 document.querySelector.document.querySelectorAll 拖拽释放(Drag and drop) API 媒体播放的 video 和 ...