zoukankan      html  css  js  c++  java
  • 字符串算法专题之:字符串相似度和字符串编辑距离

    字符串编辑距离,又称Levenshtein距离(也叫做Edit Distance),是指两个字串之间,由一个转成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符和删除一个字符。我们认为三种编辑操作的任何一种执行一次则编辑次数加1.

    例如将kitten一字转成sitting:
    sitten (k→s)
    sittin (e→i)
    sitting (→g)

    俄罗斯科学家Vladimir Levenshtein在1965年提出这个概念。

    字符串相似度,参阅《编程之美》第3.3节给出的定义:两个字符串的相似度等于“字符串编辑距离+1”的倒数。可见求字符串相似度的关键还是求字符串编辑距离。

    此问题不容易直接下手,因为有三种允许的操作类型,我们很难判断哪个字符该删除,哪个字符该修改才能获得最小的编辑距离。这时候就需要考虑该问题繁杂的表象下存在的的一些规律和不变量,寻找问题的子问题以及原问题和子问题的关系。

    1、编辑距离的上界是较长字符串的长度,下界是两个字符串长度之差;

    2、如果一个字符串长度为0,则编辑距离为另一字符串的长度;

    进一步挖掘该问题和子问题的关系,会发现:

    第一个递归算法

    如果有两个串A=xabcdae和B=xfdfa,它们的第一个字符是 相同的,只要计算A[2,...,7]=abcdae和B[2,...,5]=fdfa的距离就可以了。但是如果两个串的第一个字符不相同,那么可以进行 如下的操作(lenA和lenB分别是A串和B串的长度)。

        1.删除A串的第一个字符,然后计算A[2,...,lenA]和B[1,...,lenB]的距离。

        2.删除B串的第一个字符,然后计算A[1,...,lenA]和B[2,...,lenB]的距离。

      3.修改A串的第一个字符为B串的第一个字符,然后计算A[2,...,lenA]和B[2,...,lenB]的距离。

      4.修改B串的第一个字符为A串的第一个字符,然后计算A[2,...,lenA]和B[2,...,lenB]的距离。

      5.增加B串的第一个字符到A串的第一个字符之前,然后计算A[1,...,lenA]和B[2,...,lenB]的距离。

      6.增加A串的第一个字符到B串的第一个字符之前,然后计算A[2,...,lenA]和B[1,...,lenB]的距离。

    在这个题目中,我们并不在乎两个字符串变得相等之后的字符串是怎样的。所以,可以将上面的6个操作合并为:

      1.一步操作之后,再将A[2,...,lenA]和B[1,...,lenB]变成相字符串。

      2.一步操作之后,再将A[2,...,lenA]和B[2,...,lenB]变成相字符串。

      3.一步操作之后,再将A[1,...,lenA]和B[2,...,lenB]变成相字符串。

    整理可得如下关系:

    设字符串a的子串起始索引为ABgein,结束索引为AEnd,字符串b的子串起始索引为BBegin,结束索引为BEnd。leva,b(0,|a|,0,|b|)表示a,b的编辑距离,则:

    COST =  (a[ABgein] == b[BBegin])? 0 : 1)

    根据以上分析,可以具体代码实现如下:

    #include<iostream>
    #include<cstdlib>
    #include<string>
    using namespace std;
    
    int minValue(int v1, int v2, int v3) {
        return min(v1, min(v2,v3));
    }
    
    int calculateStringDistance(string strA, int pABegin, int pAEnd, string strB, int pBBegin, int pBEnd)
    {
        if(pABegin>pAEnd)
        {
            if(pBBegin > pBEnd)
                return 0;
            else
                return pBEnd - pBBegin + 1;
        }
    
        if(pBBegin > pBEnd)
        {
            if(pABegin > pAEnd)
                return 0;
            else
                return pAEnd - pABegin + 1;
        }
    
        if(strA[pABegin] == strB[pBBegin])
        {
            return calculateStringDistance(strA, pABegin+1, pAEnd, strB, pBBegin+1, pBEnd);
        }
        else
        {
            int t1 = calculateStringDistance(strA, pABegin, pAEnd, strB, pBBegin+1, pBEnd);
            int t2 = calculateStringDistance(strA, pABegin+1, pAEnd, strB, pBBegin, pBEnd);
            int t3 = calculateStringDistance(strA, pABegin+1, pAEnd, strB, pBBegin+1, pBEnd);
            return minValue(t1, t2, t3) + 1;
        }
    }
    
     int main() {
        string a = "kitten";
        string b = "sitting";
        cout << "Edit Distance is : " <<calculateStringDistance(a, 0, a.length()-1, b, 0, b.length()-1);
    
        return 0;
     }

    此方法为编程之美提供的。

    第二个递归算法

    字符串a,b的编辑距离定义为leva,b(|a|,|b|),则:

    其中[ai ≠ bj]表示当两者不等时值为1,两者相等时值为0.

    这种方法来自维基百科,从思路上和编程之美递归方法是一致的,区别在于编程之美的方法是从前往后分析,维基百科方法是从后往前分析。

    给出具体代码:

    int LevenshteinDistance(string s, string t)
    {
      int len_s = s.length();
      int len_t = t.length();
      int cost;
    
      /* test for degenerate cases of empty strings */
      if (len_s == 0) return len_t;
      if (len_t == 0) return len_s;
    
      /* test if last characters of the strings match */
      if (s[len_s-1] == t[len_t-1]) cost = 0;
      else                          cost = 1;
    
      /* return minimum of delete char from s, delete char from t, and delete char from both */
      return minValue(LevenshteinDistance(s.substr(0,len_s-1), t) + 1,
                     LevenshteinDistance(s, t.substr(0,len_t-1)) + 1,
                     LevenshteinDistance(s.substr(0,len_s-1), t.substr(0,len_t-1)) + cost);
    }

    递归算法的问题

    递归算法思考容易,代码编写简单,在这个问题里,上述两种算法都存在严重的性能问题。通过分析代码和思考递归运行过程,不难发现存在大量的子问题重复计算,这些无用的计算白白浪费了计算时间和程序栈,当字符串较长递归层次较深的时候会出现内存溢出。对于重复子问题的情况常见的解决方法就是制作备忘录,首次遇到该子问题是进行计算并将计算结果保存,再次遇到该子问题时直接取出保持的结果。

    自底向上矩阵表示法

    递归是一种自顶向下的解决方法,如果想采用备忘录记录中间结果往往采用自底向上的方法,使用矩阵作为记录媒介,设字符串a的长度为la,b的长度为lb,则使用d[lb+1][la+1]的二维数组来记录信息,d[i][j]表示a[0..j-1]子串和b[0..i-1]子串的编辑距离。根据上述分析可知。d[i][0] = i, d[0][j] = j,其中一个字符串长度为0是,编辑距离为另一个字符串的长度。其他情况下d[i][j] = min(d[i-1][j]+1, d[i][j-1]+1, d[i-1][j-1]+cost),cost=0(a[j-1] == b[i-1])或者1(不等)。

    矩阵表示如下:

                                           

    图1表示初始情况,设置d[i][0] = i, d[0][j] = j,

    图2表示程序执行两次循环之后的结果;

    图3是程序执行结束后的矩阵,右下角的值即为编辑距离,可见kitten和sitting的编辑距离为3.

    代码如下:

    int LevenshteinDistanceMatrix(string s, string t)
    {
      // for all i and j, d[i,j] will hold the Levenshtein distance between
      // the first i characters of s and the first j characters of t;
      // note that d has (m+1)*(n+1) values
      int len_s = s.length();
      int len_t = t.length();
      int cost;
      int d[len_t+1][len_s+1];
    
      //The init is not necessary
      for (int i=0; i<len_t+1; i++)
      {
        for (int j=0; j<len_s+1; j++)
            d[i][j] = 0;
      }
    
      // source prefixes can be transformed into empty string by
      // dropping all characters
      for (int i=0; i<len_t+1; i++)
      {
          d[i][0] = i;
      }
    
      // target prefixes can be reached from empty source prefix
      // by inserting every characters
      for (int j=0; j<len_s+1; j++)
      {
          d[0][j] = j;
      }
    
      for (int i=1; i<len_t+1; i++)
        {
          for (int j=1; j<len_s+1; j++)
            {
    
              if (t[i-1] == s[j-1])
                    cost = 0;
              else
                    cost = 1;
              d[i][j] = minValue
                        (
                          d[i-1][j] + 1,  // a deletion
                          d[i][j-1] + 1,  // an insertion
                          d[i-1][j-1] + cost // a substitution
                        );
    
            }
        }
    
        for (int j=0; j<len_s+1; j++)
        {
            if ( j == 0)
                cout << "      ";
            else
                cout << s[j-1] <<  "  ";
        }
        cout << endl;
        for (int i=0; i<len_t+1; i++)
        {
          if ( i == 0)
                cout << "   ";
            else
                cout << t[i-1] <<  "  ";
          for (int j=0; j<len_s+1; j++)
    
            {
                cout << d[i][j] << "  ";
            }
            cout << endl;
        }
        cout << endl;
    
      return d[len_t][len_s];
    }

    进一步优化

    分析矩阵法的执行过程,会发现每次计算的时候只需要本行和上一行的结果,而和再之前的行无关,如果第i行已经完成计算,则第i-1行就不再需要了,所以可以只采用两行的矩阵来实现,当字符串长度大的时候可以有效的节省存储空间。读者可自行修改上述代码实现。

    题目变化

    此时的编辑距离把插入一个字符、删除一个字符以及替换一个字符都当作一次操作,实际上替换操作可以看作是先删除旧字符再插入新字符,这样替换操作变成了两次操作,此时如何实现呢。

    如果真正理解了该问题的实现思路,题目的这个变化很容易满足,只需要改动上述代码里的一个值,没错,就是cost,把cost = 1改为cost = 2即可。

     

     

     
  • 相关阅读:
    CGContextRef使用简要教程
    使用JSONObject 深度序列化和反序列化
    使用yum方式在centOS上安装mysql
    安全驾驶技巧
    java -jar xxx.jar
    [转帖]鲍鹏山:我们培养了很多高学历的野蛮人
    perl的几个小tips
    上传项目至svn服务器,从svn上获取项目
    UE把环境变量Path改了
    成就连自己都惊讶的未来
  • 原文地址:https://www.cnblogs.com/zhaoshuai1215/p/3176655.html
Copyright © 2011-2022 走看看