字符串编辑距离
指的是字符串A向字符串B转换的最小步数。比如字符串“ABC”转换成“A”最少需要删除“B”,“C”两个字符。字符串操作有三种,一个是新增插入,一个是删除,一个是替换。该算法最早由 levenshtein提出。
从A字符串向B字符串转换,最重要的是考虑不要重复操作,比如 “abd”转换成“abcd”只需要插入一个“c”,而不用删去“d”再插入“cd”。这就要看Levenshtein Distance算法的思想了。
算法基本原理:假设我们可以使用d[ i , j ]个步骤,表示将串s[ 1…i ] 转换为串t [ 1…j ]所需要的最少步骤个数,在最基本的情况下,即在i等于0时,也就是说串s为空,那么对应的d[0,j] 就是至少增加j个字符,使得s转化为t。在j等于0时,也就是说串t为空,那么对应的d[i,0] 就是减少i个字符,使得s转化为t。
原理看起来复杂,其实就是规定了一个二维数组 d。d[i][j]表示长度为i的字符串S向长度为j的字符串T的编辑距离。那么我们知道,比如说字符串S =“abceh“要编辑成字符串T=“bbeh”,为了下标对应,我们可以设置一个二维数组d[5+1][4+1]。根据我们的设定,d[0][0]表示的就是长度为0的S(即为空)转换成长度为0的T(也为空),需要0步操作。表格如下:
黄色表格就是d[0][0]的值,橙色的是d[0][1]的值,表示的就是长度为0的S(即为空)转换成长度为1的T(即“b”),我们初始化的时候可以轻易算出表格上方,左方的所有步数。我们现在开始求d[1][1](表格中?问号)的值,它表示的是长度为1的字符串S(即“a”)编辑成长度为1的字符串T(即“b“)的距离。我们可以轻易知道这个值最小是1,操作是替换,既不是新增插入,也不是删除。于是表格便成了:
我们的继续求下一个问号所在的步数d[1][2]。我们为什么要求这个从“a”转换成“bb”的步数呢?原因就是为了比较字符编辑时候,插入、删除、替换哪个操作是最优操作。比如这里,”a”转换成“bb”,刚刚我们已经求出了“a”转换成“b”的最短编辑距离是1(也就是d[1][1]),因此现在变成了求“b”到“bb”的最短编辑距离,因为字符串“a”在d[1][1]的时候已经被我们转换成“b”了。因此d[1][2]的值应该为d[1][1]+1。我们以此类推添加表格的第一行。
求第二行d[2][1](也就是“ab”=>“b“)的时候,这里有一个注意的地方,就是对于“ab”=>“b”的情况,Levenshtein 是使用替换而不是删除操作,也就是把“a”替换成空字符。所以这里我们对字符“ab”的最优操作是把“a”替换成空字符,再拿“ b”与“b”比较。也就是d[2][1] = d[1][0]+(S[2-1]==T[1-1]?0:1) 这里S,T的下标记得-1,因为是取数组下标。所以我们得出结论:
- d[m][n]步如果是新增,那么d[m][n] = d[m][n-1]+1。
- d[m][n]步如果是删除,那么d[m][n] = d[m-1][n]+1。
- d[m][n]步如果是替换,那么d[m][n] = d[m-1][n-1]+ (S[m -1] == T[n -1]?0:1)。
- d[i][j]的最优解就是它上一步的最优解加上上面三步的最小值。
Levenshtein Distance算法的用最优解累加的方法求出最优编辑距离。这也是一种穷举的办法。PHP中就有levenshtein函数。我们现在自己实现一下这个算法的代码:
function countDistance($s,$t){
$lens = strlen($s);
$lent = strlen($t);
if($lens == 0 ){
return $lent;
}
if($lent == 0){
return $lens;
}
$dp = array();
for($i = 0; $i <= $lens; $i++){
//初始化第0列
$dp[$i][0] = $i;
}
for($j = 0;$j<= $lent;$j++){
//初始化第0行
$dp[0][$j] = $j;
}
for($i = 1;$i<= $lens;$i++){
for($j =1;$j <= $lent;$j ++){
//插入
$insert = $dp[$i][$j-1] + 1;
//删除
$del = $dp[$i-1][$j]+1;
//替换
$modi = $dp[$i-1][$j-1] + ($s[$i -1] == $t[$j -1]?0:1);
$dp[$i][$j] = min($insert,$del,$modi);
}
}
return $dp[$lens][$lent];
}
我们上面简单的例子基本上实现了Levenshtein算法,当然还有两个缺点:
- 中文字符等操作的情况。
- 二维数组很占空间,比如2个1000字符的字符串,所需要的二维数组很庞大。需要改写。