字符串编辑距离问题详解
字符串的编辑距离也被称为距Levenshtein距离(Levenshtein Distance),属于经典算法,常用方法使用递归,更好的方法是使用动态规划算法,以避免出现重叠子问题的反复计算,减少系统开销。
《编程之美》一书中3.3节中计算两个字符串的相似度,归根到底也是要求两个字符串的距离,其中问题是这样提出的:
许多程序会大量使用字符串。对于不同的字符串,我们希望能够有办法判断其相似程序。我们定义一套操作方法来把两个不相同的字符串变得相同,具体的操作方法为:
- 修改一个字符(如把"a"替换为"b");
- 增加一个字符(如把"abdd"变为"aebdd");
- 删除一个字符(如把"travelling"变为"traveling");
比如,对于"abcdefg"和"abcdef"两个字符串来说,我们认为可以通过增加/减少一个"g"的方式来达到目的。上面的两种方案,都仅需要一 次 。把这个操作所需要的次数定义为两个字符串的距离,而相似度等于"距离+1"的倒数。也就是说,"abcdefg"和"abcdef"的距离为1,相似度 为1/2=0.5。给定任意两个字符串,你是否能写出一个算法来计算它们的相似度呢?
其实这个问题的关键是要求两个字符串的编辑距离。
例如 将kitten一字转成sitting:
-
sitten (k→s)
-
sittin (e→i)
-
sitting (→g)
俄罗斯科学家Vladimir Levenshtein在1965年提出这个概念。
问题:找出字符串的编辑距离,即把一个字符串s1最少经过多少步操作变成编程字符串s2,操作有三种,添加一个字符,删除一个字符,修改一个字符。
下面我们就针对这个问题来详细阐述一下:
我们假定函数dist(str1, str2)表示字串str1转变到字串str2的编辑距离,那么对于下面3种极端情况,我们很容易给出解答(0表示空串)。
-
dist(0, 0) = 0
-
dist(0, s) = strlen(s)
-
dist(s, 0) = strlen(s)
对于一般的情况,dist(str1, str2)我们应该如何求解呢?
假定我们现在正在求解dist(str1+char1, str2+char2),也就是把"str1+char1"转变成"str2+char2"。在这个转变过称中,我们要分情况讨论:
-
str1可以直接转变成str2。这时我们只要把char1转成char2就可以了(如果char1 != char2)。
-
str1+char1可以直接转变成str2。这时我们处理的方式是插入char2。
-
str1可以直接转成str2+char2。这时的情况是我们需要删除char1。
综合上面三种情况,dist(str1+char1, str2+char2)应该是三者的最小值。
解析:
首先定义这样一个函数——edit(i, j),它表示第一个字符串的长度为i的子串到第二个字符串的长度为j的子串的编辑距离。
显然可以有如下动态规划公式:
-
if i == 0 且 j == 0,edit(i, j) = 0
-
if i == 0 且 j > 0,edit(i, j) = j
-
if i > 0 且j == 0,edit(i, j) = i
-
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。
我们建立以下表格,将两个字符串按照表格1所示的样子进行摆放,规则按照以上公式进行输入,如下所示,我们可以得到每个表格中的值,如下表格2所示:
0 |
a |
b |
c |
d |
e |
f |
|
0 |
|
|
|
|
|
|
|
a |
|
|
|
|
|
|
|
c |
|
|
|
|
|
|
|
e |
|
|
|
|
|
|
|
表格1(字符串摆放表格)
0 |
a |
b |
c |
d |
e |
f |
|
0 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
a |
1 |
|
|
|
|
|
|
c |
2 |
|
|
|
|
|
|
e |
3 |
|
|
|
|
|
|
表格2(按照规则计算i==0 或 j==0的情况)
计算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。依次类推,有如下表格3所示最终的矩阵:
0 |
a |
b |
c |
d |
e |
f |
|
0 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
a |
1 |
0 |
1 |
2 |
3 |
4 |
5 |
c |
2 |
1 |
1 |
1 |
2 |
3 |
4 |
e |
3 |
2 |
2 |
2 |
2 |
2 |
3 |
表格3(最终计算得到的字符串相对距离)
此时右下角即为我们所需要的两个字符串的编辑距离。即字符串 "abcdef"和"ace"的编辑距离为3.
有了以上的步骤,相信大家已经很清楚了,使用动态规划算法的时候,需要建立子问题的表格,以上的表格就是。而且我们能够很容易的使用二维数组建立。代码实现也就易如反掌了!
以下是我的实现过程,希望对大家有用,如果有什么可以优化或者错误的地方,希望能够得到批评指正。
1 #include <iostream> 2 #include <string> 3 4 using namespace std; 5 6 int min3Value(int a, int b, int c) 7 { 8 int tmp = (a <= b? a:b); 9 return (tmp<=c? tmp: c); 10 } 11 12 13 int Get2StringEditDis(string strA, string strB) 14 { 15 int nLenA = strA.length(); 16 int nLenB = strB.length(); 17 int **matrix = new int *[nLenA + 1]; 18 for (int i = 0; i != nLenA +1; i++) 19 { 20 matrix[i] = new int[nLenB + 1]; 21 } 22 // 动态规划 计算 23 // 初始化数组 24 matrix[0][0] = 0; 25 int p,q; 26 // j = 0; edit(i, j) = i 27 for (p = 1; p!= nLenA+1; p++) 28 { 29 matrix[p][0] = p; 30 } 31 // i = 0; edit(i,j) = j 32 for (q=1; q != nLenB+1; q++) 33 { 34 matrix[0][q] = q; 35 } 36 // i>0, j>0 37 for (int j = 1; j != nLenA+1; j++) 38 { 39 for (int k = 1; k != nLenB+1; k++) 40 { 41 int Fjk = 0; 42 if (strA[j-1] != strB[k-1]) 43 { 44 Fjk = 1; 45 } 46 matrix[j][k] = min3Value(matrix[j-1][k]+1,matrix[j][k-1]+1,matrix[j-1][k-1]+Fjk); 47 } 48 } 49 50 51 52 53 // 输出距离矩阵 54 // 第一行输出字符串b 55 // 第一列输出字符串A 56 cout<<"*****************************"<<endl; 57 cout<<"字符串编辑距离矩阵如下:\n"; 58 for (p = -1; p!= nLenA +1; p++) 59 { 60 for (q = -1; q !=nLenB+1; q++) 61 { 62 //cout.width(3),cout<<matrix[p][q]; 63 cout.width(3); 64 if (p ==-1 && q == -1) 65 { 66 cout<<" "; 67 } 68 else if (p + q == -1) 69 { 70 cout<<"NUL"; 71 } 72 else if (p == -1 && q >0) 73 { 74 cout<<strB[q-1]; 75 } 76 else if(q == -1 && p > 0) 77 { 78 cout<<strA[p-1]; 79 } 80 else 81 { 82 cout<<matrix[p][q]; 83 } 84 } 85 cout<<endl; 86 } 87 cout<<"*****************************"<<endl; 88 // 89 int nEditDis = matrix[nLenA][nLenB]; 90 for (int m = 0; m!=nLenA + 1; m++) 91 { 92 delete[] matrix[m]; 93 } 94 delete[] matrix; 95 96 97 return nEditDis; 98 } 99 100 101 int main() 102 { 103 string strA("abcdefgh"); 104 string strB("adgcf"); 105 106 int nDist = Get2StringEditDis(strA,strB); 107 cout<<"The edit dis is "<<nDist<<endl; 108 109 return 0; 110 }
结果如图1所示:
其中对于另一篇随笔中有亚马逊今年的在线笔试题中,有一道该类型题目的变种~~~ 大家可以翻阅下!地址: http://www.cnblogs.com/jiabei521/p/3352935.html