字符串编辑距离

1,116 阅读2分钟

编辑距离(Edit Distance),这里指的是Levenshtein距离,也就是字符串S1通过插入、修改、删除三种操作最少能变换成字符串S2的次数。接下来介绍利用动态规划来求解字符串的编辑距离。

定义:s1s_1s2s_2表示两字符串,dist(i,j)dist(i, j)表示字符串s1s_1的前ii个字符串和s2s_2的前jj个字符串的编辑距离,s1(i)s_1(i)s2(j)s_2(j)分别表示s1s1的第ii个字符和s2s2的第jj字符。

  • s1(i)=s2(j)s_1(i) = s_2(j),则dist(i,j)dist(i, j)就等于s1s_1的前i1i-1个字符串和s2s_2的前j1j-1个字符串的编辑距离即 dist(i,j)=dist(i1,j1)dist(i, j) = dist(i-1,j-1)
  • s1(i)s2(j)s_1(i) \neq s_2(j),则为了使s1(i)=s2(j)s_1(i) = s_2(j),可以通过在s1s_1的第ii个字符处插入s2s_2的第jj个字符,或替换s1s_1的第ii个字符为s2s_2的第jj个字符,或删除s1(s2)s_1(s_2)的第i(j)i(j)个字符(即使删除之后还是会出现不同),但是上述都是在进行了一次字符的操作之后,将转化为字问题求解,如上述的替换使s1(i)=s2(j)s_1(i) = s_2(j),则 dist(i,j)=dist(i1,j1)+1dist(i, j) = dist(i-1,j-1) + 1 ,插入和删除使 dist(i,j)=dist(i1,j)+1dist(i, j) = dist(i-1,j) +1或者 dist(i,j)=dist(i,j1)+1dist(i, j) = dist(i,j-1) +1

基于上述的情况可以得出递推公式:

dist(i,j)={i,j=0j,i=0dist(i1,j1),s1(i)=s2(j)min(d(i1,j),d(i,j1),d(i1,j1)),s1(i)s2(j)dist(i,j) = \begin{equation} \left\{ \begin{array}{lr} i, & j = 0 \\ j, & i = 0 \\ dist(i-1,j-1), & s_1(i) = s_2(j) \\ min(d(i-1,j), d(i,j-1),d(i-1,j-1)), & s_1(i) \neq s_2(j) \\ \end{array} \right. \end{equation}

例如:s1="abcd"s_1 = "abcd",s2="abfce"s_2 = "abfce",则利用动态规划求解的矩阵为

abfce
012345
a101234
b210123
c321112
d432222

代码实现

public static int levenshtein(String s1, String s2){
    if(s1 == null){
        return s2 == null? 0:s2.length();
    }
    if(s2 == null){
        return s1 == null? 0:s1.length();
    }
    int n = s1.length();
    int m = s2.length();
    int[][] matrix = new int[n+1][m+1];

    for(int i = 0;i <= n;i++){
        matrix[i][0] = i;
    }
    for(int j = 0;j <= m;j++){
        matrix[0][j] = j;
    }

    for(int i = 1;i <= n;i++){
        for(int j = 1;j <= m;j++){
            if(s1.charAt(i-1) == s2.charAt(j-1)){
                matrix[i][j] = matrix[i-1][j-1];
            }else {
                matrix[i][j] = Math.min(Math.min(matrix[i-1][j], matrix[i][j-1]), matrix[i-1][j-1]) + 1;
            }
        }
    }
    return matrix[n][m];
}

上述的方法其中有一个不足之处是当两个字符串太长时,则对应申请的数组占用的空间也变大。而上述的算法中,在更新dist(i,j)dist(i,j)的过程中只用到了dist(i1,j)dist(i-1,j)dist(i,j1)dist(i,j-1)dist(i1,j1)dist(i-1,j-1)这三个数,因此我们可以只存储更新的上一行的数据,这样就可以得到dist(i1,j)dist(i-1,j)dist(i1,j1)dist(i-1,j-1)这两个数,而d(i,0)=id(i,0) = i,可以在此基础上根据上一行的d(i1,0)d(i-1,0)d(i1,1)d(i-1,1)推出d(i1,1)d(i-1,1),在结合上一行的d(i1,1)d(i-1,1)d(i1,2)d(i-1,2)推出d(i1,2)d(i-1,2)…以此类推得到第ii行的数据。接下来是改进后的代码实现

public static int levenshteinImprove(String s1, String s2){
    if(s1 == null)
        return s2 == null? 0:s2.length();
    if(s2 == null)
        return s1 == null? 0:s1.length();
    int n = s1.length(),m = s2.length();
    int[] matrix = new int[m+1];
    for(int i = 0;i <= m;i++){
        matrix[i] = i;
    }

    for(int i = 1;i <= n;i++){
        int pre = matrix[0];
        matrix[0] = i;
        for(int j = 1;j <= m;j++){
            int tmp = matrix[j];
            if(s1.charAt(i-1) == s2.charAt(j-1)){
                matrix[j] = pre;
            }else {
                matrix[j] = Math.min(Math.min(pre, matrix[j-1]),  matrix[j]) + 1;
            }
            pre = tmp;
        }
    }
    return matrix[m];
}

上述代码还可以改进一个小细节,当比较的字符串长短一个很长,一个很短时,我们可以比较其长短,取较短的字符串长度作为开辟的数组的长度。