文本比较算法:编辑距离

编辑距离,又称Levenshtein距离,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。

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

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

问题:找出字符串的编辑距离,即把一个字符串s1最少经过多少步操作变成编程字符串s2,操作有三种,添加一个字符,删除一个字符,修改一个字符。

第一种解决方法,递归迭代。A和B字符距离的比较只有三种情况:A添加字符(B的比较位置+1)、A删除字符(A的比较位置+1)、A替换字符(A和B的比较位置均+1)。程序如下:

/*
 *侯凯,2014-9-15
 *功能:LD距离
 */
#include<iostream>
using namespace std;

int CalTheDistance(int spos1,int spos2,int len1,int len2,char* a,char* b)
{
    if(spos1 >=len1)
    {
        if(spos2>=len2)return 0;
        else return (len2-spos2);
    }
    if(spos2>=len2)
    {
        return len1-spos1;
    }
    if(a[spos1]==b[spos2])
    {
        return CalTheDistance(spos1+1,spos2+1,len1,len2,a,b);
    }
    else
    {
        int d1 = CalTheDistance(spos1+1,spos2,len1,len2,a,b);
        int d2 = CalTheDistance(spos1,spos2+1,len1,len2,a,b);
        int d3 = CalTheDistance(spos1+1,spos2+1,len1,len2,a,b);
        return min(d1,min(d2,d3))+1;
    }
}

int main()
{
    char str1[] = "abe";
    char str2[] = "acb";
    //聚类为2
    int distance = CalTheDistance(0,0,strlen(str1),strlen(str2),str1,str2);
    cout<<distance<<endl;
    system("Pause");
}

采用递归算法,只是理论上有效,便于理解,实际应用中会出现各种限制。一般,堆栈深度程序是设限制的。
第二种方法,动态规划的思想。首先定义这样一个函数——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

示例:比较的两个字符串为“abcd”和“bedf”,阶段与状态(i,j)矩阵:

image

计算下一行得到:

image

进而:

image

可得状态转移方程:若A(i)=B(j),则LD(i,j)=LD(i-1,j-1);否则LD(i,j)=min{LD(i-1,j-1),min(i-1,j),min(i,j-1)}+1。这个关系式可以从题目中直接推到得到。最终得到:

image

 

程序实现如下:

/*
 *侯凯,2014-9-15
 *功能:LD距离
 */
#include<iostream>
using namespace std;

int CalTheDistance(string A,string B)
{
    int **ptr = new int*[ A.size()+ 1];
    for(int i = 0; i < A.size() + 1 ;i++)
    {
        ptr[i] = new int[B.size() + 1];
    }

    for(int i=0;i<A.size()+1;i++)
    {
        ptr[i][0] = i;
    }
    for(int i=0;i<B.size()+1;i++)
    {
        ptr[0][i] = i;
    }
    for(int i=0;i<A.size();i++)
    {
        for(int j=0;j<B.size();j++)
        {
            if(A[i]==B[j])
                ptr[i+1][j+1]=ptr[i][j];
            else
                ptr[i+1][j+1]=min(ptr[i][j],min(ptr[i+1][j],ptr[i][j+1]))+1;
        }
    }
    int result = ptr[A.size()][B.size()];
    for(int i = 0; i < A.size() + 1 ;i++)
    {
        delete [] ptr[i];
        ptr[i] = NULL;
    }
    delete[] ptr;
    ptr = NULL;
    return result;
}

int main()
{
    string str1 = "abcd";
    string str2 = "bedf";
    //聚类为3
    int distance = CalTheDistance(str1,str2);
    cout<<distance<<endl;
    system("Pause");
}

此时时间复杂度为O(mn),空间复杂度亦为O(mn),可对程序进一步改进,使空间复杂度降低为O(m),如下:

/*
 *侯凯,2014-9-15
 *功能:LD距离
 */
#include<iostream>
using namespace std;

int CalTheDistance(string A,string B)
{
    int *ptr = new int[ B.size()+ 1];

    for(int i=0;i<B.size()+1;i++)
    {
        ptr[i] = i;
    }

    for(int i=0;i<A.size();i++)
    {
        int tmp1 = ptr[0];
        ptr[0] += 1;
        for(int j=0;j<B.size();j++)
        {
            int tmp2 = tmp1;
            tmp1 = ptr[j+1];
            if(A[i]==B[j])
                ptr[j+1]=tmp2;
            else
                ptr[j+1]=min(ptr[j],min(tmp2,tmp1))+1;
        }
    }
    int result = ptr[B.size()];
    delete[] ptr;
    ptr = NULL;
    return result;
}

int main()
{
    string str1 = "abcd";
    string str2 = "bedf";
    //聚类为3
    int distance = CalTheDistance(str1,str2);
    cout<<distance<<endl;
    system("Pause");
}

通过二维矩阵,我们不但可以得到两个字符串的编辑距离,也可以回溯得到匹配子串。

以上面为例A=abcd,B=bedf,LD(A,B)=3

他们的匹配为:

A:abcd_

B:_bedf

如上面所示,蓝色表示完全匹配,黑色表示编辑操作,_表示插入字符或者是删除字符操作。如上面所示,黑色字符有3个,表示编辑距离为3。

image

从右下角单元格回溯,若Ai=Bj,则回溯到左上角单元格;若ai≠bj,回溯到左上角、上边、左边中值最小的单元格,若有相同最小值的单元格,优先级按照左上角、上边、左边的顺序。
若回溯到左上角单元格,将Ai添加到匹配字串A,将Bj添加到匹配字串B;若回溯到上边单元格,将Bi添加到匹配字串B,将_添加到匹配字串A;若回溯到左边单元格,将_添加到匹配字串B,将Aj添加到匹配字串A;搜索晚整个匹配路径,匹配字串也就完成了。

在比较长字符串的时候,还有其他性能更好的算法。留待后文详述。

posted @ 2014-09-15 14:09  侯凯  阅读(2908)  评论(0编辑  收藏  举报