Levenshtein Distance(编辑距离)算法与使用场景
前提
已经很久没深入研究过算法相关的东西,毕竟日常少用,就算死记硬背也是没有实施场景导致容易淡忘。最近在做一个脱敏数据和明文数据匹配的需求的时候,用到了一个算法叫Levenshtein Distance Algorithm
,本文对此算法原理做简单的分析,并且用此算法解决几个常见的场景。
什么是Levenshtein Distance
Levenshtein Distance
,一般称为编辑距离(Edit Distance
,Levenshtein Distance
只是编辑距离的其中一种)或者莱文斯坦距离,算法概念是俄罗斯科学家弗拉基米尔·莱文斯坦(Levenshtein · Vladimir I)在1965年提出。此算法的概念很简单:Levenshtein Distance
指两个字串之间,由一个转换成另一个所需的最少编辑操作次数,允许的编辑操作包括:
- 将其中一个字符替换成另一个字符(
Substitutions
)。 - 插入一个字符(
Insertions
)。 - 删除一个字符(
Deletions
)。
下文开始简称
Levenshtein Distance
为LD
Levenshtein Distance公式定义
这个数学公式最终得出的数值就是LD
的值。举个例子:
将kitten
这个单词转成sitting
的LD
值为3:
- kitten → sitten (k→s)
- sitten → sittin (e→i)
- sittin → sitting (insert a 'g')
Levenshtein Distance动态规划方法
可以使用动态规划的方法去测量LD
的值,步骤大致如下:
- 初始化一个
LD
矩阵(M,N)
,M
和N
分别是两个输入字符串的长度。 - 矩阵可以从左上角到右下角进行填充,每个水平或垂直跳转分别对应于一个插入或一个删除。
- 通过定义每个操作的成本为1,如果两个字符串不匹配,则对角跳转的代价为1,否则为0,简单来说就是:
- 如果
[i][j]
位置的两个字符串相等,则从[i][j]
位置左加1,上加1,左上加0,然后从这三个数中取出最小的值填充到[i][j]
。 - 如果
[i][j]
位置的两个字符串不相等,则从[i][j]
位置左、左上、上三个位置的值中取最小值,这个最小值加1(或者说这三个值都加1然后取最小值),然后填充到[i][j]
。
- 如果
- 按照上面规则
LD
矩阵(M,N)
填充完毕后,最终矩阵右下角的数字就是两个字符串的LD
值。
这里不打算证明上面动态规划的结论(也就是默认这个动态规划的结果是正确的),直接举两个例子说明这个问题:
- 例子一(两个等长字符串):
son
和sun
。 - 例子二(两个非等长字符串):
doge
和dog
。
例子一:
初始化LD
矩阵(3,3)
:
s |
o |
n |
||
---|---|---|---|---|
0 |
1 |
2 |
3 |
|
s |
1 |
|||
u |
2 |
|||
n |
3 |
计算[0][0]
的位置的值,因为's' = 's'
,所以[0][0]的值 = min(1+1, 1+1, 0+0) = 0
。
s |
o |
n |
||
---|---|---|---|---|
0 |
1 |
2 |
3 |
|
s |
1 |
0 | ||
u |
2 |
|||
n |
3 |
按照这个规则计算其他位置的值,填充完毕后的LD
矩阵`如下:
s |
o |
n |
||
---|---|---|---|---|
0 |
1 |
2 |
3 |
|
s |
1 |
0 | 1 | 2 |
u |
2 |
1 | 1 | 2 |
n |
3 |
2 | 2 | 1 |
那么son
和sun
的LD
值为1。
例子二:
初始化LD
矩阵(4,3)
:
d |
o |
g |
||
---|---|---|---|---|
0 |
1 |
2 |
3 |
|
d |
1 |
|||
o |
2 |
|||
g |
3 |
|||
e |
4 |
接着填充矩阵:
d |
o |
g |
||
---|---|---|---|---|
0 |
1 |
2 |
3 |
|
d |
1 |
0 |
1 |
2 |
o |
2 |
1 |
0 |
1 |
g |
3 |
2 |
1 |
0 |
e |
4 |
3 |
2 |
1 |
那么doge
和dog
的LD
值为1。
Levenshtein Distance算法实现
依据前面提到的动态规划方法,可以相对简单地实现LD
的算法,这里选用Java
语言进行实现:
public enum LevenshteinDistance {
// 单例
X;
/**
* 计算Levenshtein Distance
*/
public int ld(String source, String target) {
Optional.ofNullable(source).orElseThrow(() -> new IllegalArgumentException("source"));
Optional.ofNullable(target).orElseThrow(() -> new IllegalArgumentException("target"));
int sl = source.length();
int tl = target.length();
// 定义矩阵,行列都要加1
int[][] matrix = new int[sl + 1][tl + 1];
// 首行首列赋值
for (int k = 0; k <= sl; k++) {
matrix[k][0] = k;
}
for (int k = 0; k <= tl; k++) {
matrix[0][k] = k;
}
// 定义临时的编辑消耗
int cost;
for (int i = 1; i <= sl; i++) {
for (int j = 1; j <= tl; j++) {
if (source.charAt(i - 1) == target.charAt(j - 1)) {
cost = 0;
} else {
cost = 1;
}
matrix[i][j] = min(
// 左上
matrix[i - 1][j - 1] + cost,
// 右上
matrix[i][j - 1] + 1,
// 左边
matrix[i - 1][j] + 1
);
}
}
return matrix[sl][tl];
}
private int min(int x, int y, int z) {
return Math.min(x, Math.min(y, z));
}
/**
* 计算匹配度match rate
*/
public BigDecimal mr(String source, String target) {
int ld = ld(source, target);
// 1 - ld / max(len1,len2)
return BigDecimal.ONE.subtract(BigDecimal.valueOf(ld)
.divide(BigDecimal.valueOf(Math.max(source.length(), target.length())), 2, BigDecimal.ROUND_HALF_UP));
}
}
算法的复杂度为O(N * M)
,其中N
和M
分别是两个输入字符串的长度。这里的算法实现完全参照前面的动态规划方法推论过程,实际上不一定需要定义二维数组(矩阵),使用两个一维的数组即可,可以参看一下java-string-similarity中Levenshtein算法的实现。以前面的例子运行一下:
public static void main(String[] args) throws Exception {
String s = "doge";
String t = "dog";
System.out.println("Levenshtein Distance:" +LevenshteinDistance.X.ld(s, t));
System.out.println("Match Rate:" +LevenshteinDistance.X.mr(s, t));
}
// 输出
Levenshtein Distance:1
Match Rate:0.75
Levenshtein Distance算法一些使用场景
LD
算法主要的应用场景有:
DNA
分析。- 拼写检查。
- 语音识别。
- 抄袭侦测。
- 等等......
其实主要就是"字符串"匹配场景,这里基于实际遇到的场景举例。
脱敏数据和明文数据匹配
最近有场景做脱敏数据和明文数据匹配,有时候第三方导出的文件是脱敏文件,格式如下:
姓名 | 手机号 | 身份证 |
---|---|---|
张*狗 |
123****8910 |
123456****8765**** |
己方有明文数据如下:
姓名 | 手机号 | 身份证 |
---|---|---|
张大狗 |
12345678910 |
123456789987654321 |
要把两份数据进行匹配,得出上面两条数据对应的是同一个人的数据,原理就是:当且仅当两条数据中手机号的LD
值为4,身份证的LD
值为8,姓名的LD
值为1,则两条数据完全匹配。
使用前面写过的算法:
public static void main(String[] args) throws Exception {
String sourceName = "张*狗";
String sourcePhone = "123****8910";
String sourceIdentityNo = "123456****8765****";
String targetName = "张大狗";
String targetPhone = "12345678910";
String targetIdentityNo = "123456789987654321";
boolean match = LevenshteinDistance.X.ld(sourceName, targetName) == 1 &&
LevenshteinDistance.X.ld(sourcePhone, targetPhone) == 4 &&
LevenshteinDistance.X.ld(sourceIdentityNo, targetIdentityNo) == 8;
System.out.println("是否匹配:" + match);
targetName = "张大doge";
match = LevenshteinDistance.X.ld(sourceName, targetName) == 1 &&
LevenshteinDistance.X.ld(sourcePhone, targetPhone) == 4 &&
LevenshteinDistance.X.ld(sourceIdentityNo, targetIdentityNo) == 8;
System.out.println("是否匹配:" + match);
}
// 输出结果
是否匹配:true
是否匹配:false
拼写检查
这个场景看起来比较贴近生活,也就是词典应用的拼写提示,例如输入了throwab
,就能提示出throwable
,笔者认为一个简单实现就是遍历t
开头的单词库,寻找匹配度比较高(LD
值比较小)的单词进行提示(实际上为了满足效率有可能并不是这样实现的)。举个例子:
public static void main(String[] args) throws Exception {
String target = "throwab";
// 模拟一个单词库
List<String> words = Lists.newArrayList();
words.add("throwable");
words.add("their");
words.add("the");
Map<String, BigDecimal> result = Maps.newHashMap();
words.forEach(x -> result.put(x, LevenshteinDistance.X.mr(x, target)));
System.out.println("输入值为:" + target);
result.forEach((k, v) -> System.out.println(String.format("候选值:%s,匹配度:%s", k, v)));
}
// 输出结果
输入值为:throwab
候选值:the,匹配度:0.29
候选值:throwable,匹配度:0.78
候选值:their,匹配度:0.29
这样子就可以基于输入的throwab
选取匹配度最高的throwable
。
抄袭侦测
抄袭侦测的本质也是字符串的匹配,可以简单认为匹配度高于某一个阈值就是属于抄袭。例如《我是一只小小鸟》里面的一句歌词是:
我是一只小小小小鸟,想要飞呀飞却飞也飞不高
假设笔者创作了一句歌词:
我是一条小小小小狗,想要睡呀睡却睡也睡不够
我们可以尝试找出两句词的匹配度:
System.out.println(LevenshteinDistance.X.mr("我是一只小小小小鸟,想要飞呀飞却飞也飞不高", "我是一条小小小小狗,想要睡呀睡却睡也睡不够"));
// 输出如下
0.67
可以认为笔者创作的歌词是完全抄袭的。当然,对于大文本的抄袭侦测(如论文查重等等)需要考虑执行效率的问题,解决的思路应该是类似的,但是需要考虑如何分词、大小写等等各种的问题。
小结
本文仅仅对Levenshtein Distance
做了一点皮毛上的分析并且列举了一些简单的场景,其实此算法在日常生活中是十分常见的,笔者猜测词典应用的单词拼写检查、论文查重(抄袭判别)都可能和此算法相关。算法虽然学习曲线比较陡峭,但是它确实是一把解决问题的利刃。
参考资料:
Levenshtein Distance(编辑距离)算法与使用场景的更多相关文章
- Levenshtein distance 编辑距离算法
这几天再看 virtrual-dom,关于两个列表的对比,讲到了 Levenshtein distance 距离,周末抽空做一下总结. Levenshtein Distance 介绍 在信息理论和计算 ...
- Levenshtein Distance (编辑距离) 算法详解
编辑距离即从一个字符串变换到另一个字符串所需要的最少变化操作步骤(以字符为单位,如son到sun,s不用变,将o->s,n不用变,故操作步骤为1). 为了得到编辑距离,我们画一张二维表来理解,以 ...
- 扒一扒编辑距离(Levenshtein Distance)算法
最近由于工作需要,接触了编辑距离(Levenshtein Distance)算法.赶脚很有意思.最初百度了一些文章,但讲的都不是很好,读起来感觉似懂非懂.最后还是用google找到了一些资料才慢慢理解 ...
- 自然语言处理(5)之Levenshtein最小编辑距离算法
自然语言处理(5)之Levenshtein最小编辑距离算法 题记:之前在公司使用Levenshtein最小编辑距离算法来实现相似车牌的计算的特性开发,正好本节来总结下Levenshtein最小编辑距离 ...
- Lucene的FuzzyQuery中用到的Levenshtein Distance(LD)算法
2019独角兽企业重金招聘Python工程师标准>>> Lucene的FuzzyQuery中用到的Levenshtein Distance(LD)算法 博客分类: java 搜索引擎 ...
- 利用Levenshtein Distance (编辑距离)实现文档相似度计算
1.首先将word文档解压缩为zip /** * 修改后缀名 */ public static String reName(String path){ File file=new File(path) ...
- Levenshtein distance 编辑距离
编辑距离,又称Levenshtein距离,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数.许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符 实现方案: 1. 找出最长 ...
- Levenshtein Distance + LCS 算法计算两个字符串的相似度
//LD最短编辑路径算法 public static int LevenshteinDistance(string source, string target) { int cell = source ...
- C#实现Levenshtein distance最小编辑距离算法
Levenshtein distance,中文名为最小编辑距离,其目的是找出两个字符串之间需要改动多少个字符后变成一致.该算法使用了动态规划的算法策略,该问题具备最优子结构,最小编辑距离包含子最小编辑 ...
随机推荐
- linux进程(一)
回顾:CentOS6的启动过程开机自检->找硬盘->操作系统->内核->进程->登录 Systemd借鉴了很多launchd的思想,他的重要特性如下:1.同SysVini ...
- Python的lambda学习
lambda可以简化简单循环,如下: def fc1(x): return x + 10 print "fc1(23) = ", fc1(23) y = lambda x: x+1 ...
- 创想变现:斯坦福设计创新课堂ME310分享(上篇)
编者按:今年6月,微软亚洲研究院人机交互组研究员顾嘉唯,在美国斯坦福大学担任了d-School的ME310设计课程的项目评审.该课程是斯坦福大学的全球联合新产品设计创新课程,学习方式以小组为单位,每个 ...
- Arcpy处理修改shapefile FeatureClass 线要素坐标
需求:在开发的webgis系统中需要将道路矢量数据与谷歌地图瓦片叠加,谷歌地图瓦片在国家测绘局的要求是进行了偏移处理的,人称“火星坐标系GCJ_02”,道路数据是WGS-84坐标系下的经纬度坐标,现在 ...
- 1、简述在java网络编程中,服务端程序与客户端程序的具体开发步骤?
网络编程分为UDP通信和TCP通信 UDP协议: 发送端:1.创建DatagramSocket对象.2.创建DatagramPacket对象,并封装数据.3.发送数据.4.释放 资源. 接收端:1.创 ...
- Leetcode14._最长公共前缀
题目 编写一个函数来查找字符串数组中的最长公共前缀. 如果不存在公共前缀,返回空字符串 "". 示例 1: 输入: ["flower","flow&q ...
- XGBoost使用篇(未完成)
1.截止到本文(20191104)sklearn没有集成xgboost算法,需要单独安装xgboost库,然后导入使用 xgboost官网安装说明 Pre-built binary wheel for ...
- 之前工作过程中自定义的代码生成器模版,codesimit
动软代码生成器 和codesmith 5年前的东西,或许有些过时 动软的功能有限,改的也比较简单,已弃. codesmith可定制性强,当时自已改的,提高了团队的整体工作效率. codesmith代码 ...
- discount the possibility|pessimistic|bankrupt|
Nor can we discount the possibility that some factor in the diet itself has harmful effects. ADJ-GRA ...
- 《你不知道的Javascript》学习笔记
简介 众所周知,JavaScript 既是一门充满吸引力.简单易用的语言,又是一门具有许多复杂微妙技术的语言,即使是经验丰富的JavaScript 开发者,如果没有认真学习的话也无法真正理解它们. 如 ...