首页 > 代码库 > 编辑距离算法
编辑距离算法
定义
给定两个字符串s1和s2,两者的编辑距离定义为将s1转换为s2的最小编辑操作数(等价于将s2转换为s1的最小编辑操作数)。
编辑操作有3种:插入一个字符、删除一个字符、替换一个字符。
例如:cat和cbt的编辑距离是1(将a替换为b);cat到ca的编辑距离是1(删除t);ct到cat的编辑距离是1(插入a);xcat到caty的编辑距离是2(删除x,插入y)。
求解方法
知道了编辑距离的定义,那么如何求最小编辑距离呢?这里用到了动态规划的思想。
用例子来说明,假如我们要求解 jary和jerry的最小编辑距离,那么首先要创建如下矩阵:
j | a | r | y | ||
0 | 1 | 2 | 3 | 4 | |
j | 1 | ||||
e | 2 | ||||
r | 3 | ||||
r | 4 | ||||
y | 5 |
这个矩阵什么意思呢?第一行是字符串jary,第一列是字符串jerry,每个标有数字的单元格代表了两个字符串的子串的最小编辑距离。第二行第二列的0代表两个字符串都取空子串的时候,编辑距离就是0(子串相等);第二行第三列的1代表当jerry的子串取空,jary的子串取j时,两个子串的最小编辑距离是1(给jerry的子串插入j)。其他的依次类推,可以很容易得出当前矩阵中的第二行和第二列的数字。
而我们最终要求的两个字符串的最小编辑距离对应的就是矩阵右下角的那个单元格,它代表当jary子串取jary,jerry子串取jerry时,两个子串的最小编辑距离,也就是两个字符串的最小编辑距离。
这里我先直接说怎么求,然后再解释原理。看下面的矩阵,我在中心空白的位置标上了从x1到x20,这里x后面的数字代表我们求解时的顺序。
j | a | r | y | ||
0 | 1 | 2 | 3 | 4 | |
j | 1 | x1 | x6 | x11 | x16 |
e | 2 | x2 | x7 | x12 | x17 |
r | 3 | x3 | x8 | x13 | x18 |
r | 4 | x4 | x9 | x14 | x19 |
y | 5 | x5 | x10 | x15 | x20 |
如果按顺序求解的话,那么在求解每一个值的时候,它的左、上、左上三个位置的单元格值肯定都是已知的,将这三个单元格里的值分别定义为left、top、leftTop,则要求解的单元格的值v为:
cost=若单元格横向对应的字符和纵向对应的字符相等则为0否则为1
min(left+1,top+1,leftTop+cost)
按照求解方法求解后的矩阵:
j | a | r | y | ||
0 | 1 | 2 | 3 | 4 | |
j | 1 | 0 | 1 | 2 | 3 |
e | 2 | 1 | 1 | 2 | 3 |
r | 3 | 2 | 2 | 1 | 2 |
r | 4 | 3 | 3 | 2 | 2 |
y | 5 | 4 | 4 | 3 | 2 |
取右下角的值,因此jary和jerry的编辑距离是2(替换a为e,插入一个r)。
求解原理
通过上面的介绍我们可以用矩阵求两个字符串的最小编辑距离了,但是这么做的原理是什么呢?其实很简单,当我们要求字符串s[1...i]到t[1...j]的编辑距离的时候:
- 如果我们知道可以在k1个操作内将s[1...i-1]转换为t[1...j],那么用k1+1次操作一定能将s[1...i]转化为t[1...j],因为只需要先做一次移除操作移除s[i]将s[1...i]转化为s[1...i-1],然后再做k1个操作就可以转换为t[1...j]。
- 如果我们知道可以在k2个操作内将s[1...i]转换为t[1...j-1],那么用k2+1次操作一定能将s[1...i]转化为t[1...j],因为我们可以先用k2次操作将s[1...i]转化为t[1...j-1],然后再执行一次插入操作在末尾插入t[j]即可将s[1...i]转化为t[1...j]
- 如果我们知道可以在k3个操作内将s[1...i-1]转化为t[1...j-1],那么如果s[i]==t[j],则将s[1...i]转换为t[1...j]只需要k3次操作,如果s[i]!=t[j],则需要做一次替换操作将s[i]替换为t[j],这种情况下需要k3+1次操作。
而上面我们讨论的3中情况下的k1、k2、k3就对应着矩阵里一个单元格的左、上、左上的单元格里的值。
上述结论可以表述为如下公式:
实现代码
明白了原理之后,写代码很简单,就是用代码模拟计算矩阵的过程(java实现):
package common; import org.junit.Assert; public class LevenshteinDistance { public static int getDistance(String src, String des) { int[][] m=new int[des.length()+1][]; for (int i = 0; i < m.length; i++) { m[i]=new int[src.length()+1]; } for(int i=0;i<src.length()+1;i++){ m[0][i]=i; } for(int i=0;i<des.length()+1;i++){ m[i][0]=i; } for(int i=1;i<des.length()+1;i++){ for (int j = 1; j < src.length()+1; j++) { int rcost=des.charAt(i-1)==src.charAt(j-1)?0:1; m[i][j]=Math.min(Math.min(m[i-1][j]+1, m[i-1][j-1]+rcost), m[i][j-1]+1); } } return m[des.length()][src.length()]; } public static void main(String[] args) { Assert.assertEquals(getDistance("cat", "dog"), 3); //replace Assert.assertEquals(getDistance("cat", "cbt"), 1); //replace Assert.assertEquals(getDistance("cat", "ca"), 1); //delete Assert.assertEquals(getDistance("catx", "cat"), 1); //delete Assert.assertEquals(getDistance("ct", "cat"), 1); //insert Assert.assertEquals(getDistance("xcat", "caty"), 2); //delete and insert Assert.assertEquals(getDistance("fast", "cats"), 3); Assert.assertEquals(getDistance("cats", "fast"), 3); Assert.assertEquals(getDistance("kitten", "sitting"), 3); Assert.assertEquals(getDistance("sitting", "kitten"), 3); Assert.assertEquals(getDistance("jary", "jerry"), 2); Assert.assertEquals(getDistance("jerry", "jary"), 2); } }
总结
要学会编辑距离算法主要就是掌握两点,第一是要会通过矩阵手算两个字符串的编辑距离,第二就是明白为什么可以这么计算。掌握了这两点写程序就很简单了。
编辑距离算法