编辑距离概念描述:
编辑距离,又称Levenshtein距离,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。
例如将kitten一字转成sitting:
sitten (k→s)
sittin (e→i)
sitting (→g)
俄罗斯科学家Vladimir Levenshtein在1965年提出这个概念。
问题:找出字符串的编辑距离,即把一个字符串s1最少经过多少步操作变成编程字符串s2,操作有三种,添加一个字符,删除一个字符,修改一个字符
解析:
首先定义这样一个函数——edit(i, j),它表示第一个字符串的长度为i的子串到第二个字符串的长度为j的子串的编辑距离。
显然可以有如下动态规划公式:
<!--[if !supportLists]-->if i == 0 且 j == 0,edit(i, j) = 0
<!--[if !supportLists]-->if i == 0 且 j > 0,edit(i, j) = j
<!--[if !supportLists]-->if i > 0 且j == 0,edit(i, j) = i
<!--[if !supportLists]-->if i ≥ 1 且 j ≥ 1 ,edit(i, j) == min{ edit(i-1, j) + 1, edit(i, j-1) + 1, edit(i-1, j-1) + f(i, j) },当第一个字符串的第i个字符不等于第二个字符串的第j个字符时,f(i, j) = 1;否则,f(i, j) = 0。
| 0 | f | a | i | l | i | n | g |
0 |
|
|
|
|
|
|
|
|
s |
|
|
|
|
|
|
|
|
a |
|
|
|
|
|
|
|
|
i |
|
|
|
|
|
|
|
|
l |
|
|
|
|
|
|
|
|
n |
|
|
|
|
|
|
|
|
| 0 | f | a | i | l | i | n | g |
0 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
s | 1 |
|
|
|
|
|
|
|
a | 2 |
|
|
|
|
|
|
|
i | 3 |
|
|
|
|
|
|
|
l | 4 |
|
|
|
|
|
|
|
n | 5 |
|
|
|
|
|
|
|
计算edit(1, 1),edit(0, 1) + 1 == 2,edit(1, 0) + 1 == 2,edit(0, 0) + f(1, 1) == 0 + 1 == 1,min(edit(0, 1),edit(1, 0),edit(0, 0) + f(1, 1))==1,因此edit(1, 1) == 1。 依次类推:
| 0 | f | a | i | l | i | n | g |
0 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
s | 1 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
a | 2 | 2 |
|
|
|
|
|
|
i | 3 |
|
|
|
|
|
|
|
l | 4 |
|
|
|
|
|
|
|
n | 5 |
|
|
|
|
|
|
|
edit(2, 1) + 1 == 3,edit(1, 2) + 1 == 3,edit(1, 1) + f(2, 2) == 1 + 0 == 1,其中s1[2] == 'a' 而 s2[1] == 'f'‘,两者不相同,所以交换相邻字符的操作不计入比较最小数中计算。以此计算,得出最后矩阵为:
| 0 | f | a | i | l | i | n | g |
0 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
s | 1 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
a | 2 | 2 | 1 | 2 | 3 | 4 | 5 | 6 |
i | 3 | 3 | 2 | 1 | 2 | 3 | 4 | 5 |
l | 4 | 4 | 3 | 2 | 1 | 2 | 3 | 4 |
n | 5 | 5 | 4 | 3 | 2 | 2 | 2 | 3 |
Lucene里的实现:
//*****************************
// Compute Levenshtein distance: see org.apache.commons.lang.StringUtils#getLevenshteinDistance(String, String)
//*****************************
public float getDistance (String target, String other) {
char[] sa;
int n;
int p[]; //'previous' cost array, horizontally
int d[]; // cost array, horizontally
int _d[]; //placeholder to assist in swapping p and d
/*
* 这里的实现是仅仅使用一个临时数组做交换空间,避免直接创建
* 一个二维数组可能出现的oom
*/
sa = target.toCharArray();
n = sa.length;
// 初始化目标字符串
p = new int[n+1];
d = new int[n+1];
final int m = other.length();
if (n == 0 || m == 0) {
if (n == m) {
return 1;
}
else {
return 0;
}
}
// indexes into strings s and t
int i; // iterates through s
int j; // iterates through t
char t_j; // jth character of t
int cost; // cost
// 横向字符串,初始化编辑距离 0-n, 第一个横排的值
for (i = 0; i<=n; i++) {
p[i] = i;
}
for (j = 1; j<=m; j++) { // 竖向字符串,从第二个横排开始
// 注意,这里j-1是由于数组的第一个位置是0,字符串位置从1开始的,实际取字符时,要位置-1得到下标
t_j = other.charAt(j-1);
// 横向编辑距离的第一个值的值初始值(1-m)
d[0] = j;
for (i=1; i<=n; i++) {
cost = sa[i-1]==t_j ? 0 : 1;
// edit(i, j) == min{ edit(i-1, j) + 1, edit(i, j-1) + 1, edit(i-1, j-1) + f(i, j) }
// minimum of cell to the left+1, to the top+1, diagonally left and up +cost
d[i] = Math.min(Math.min(d[i-1]+1, p[i]+1), p[i-1]+cost);
}
// copy current distance counts to 'previous row' distance counts
_d = p;
// 记录上一个横排的值
p = d;
// 下一个马上处理的横排
d = _d;
}
// our last action in the above loop was to switch d and p, so p now
// actually has the most recent cost counts
return 1.0f - ((float) p[n] / Math.max(other.length(), sa.length));
}