编辑距离的分析与实现
做文本分析的时候,我们常常需要计算两篇文本的相关性(相关性除了可以使用相似度还可以使用距离-当然是应该反着来算的),如果你不知道把文本转换为一个向量(如果你知道这个,那遭了,因为那样你就可能认为我这里的很rubbish了),那你可能会考虑我们这里介绍的最朴素的方法-计算字符串间编辑距离的方法。
先给出我遇到的问题(也即定义编辑距离):
设A 和B 是两个字符串。对于字符串可以执行如下操作:
(1) 删除一个字符;
(2)插入一个字符;
(3)将一个字符替换成另外一个字符。
例如将kitten一字转成sitting:
(1). sitten (k→s)
(2). sittin (e→i)
(3). sitting (→g)
利用上面的三种操作可以将字符串A 转换成字符串B。这种转换所需要的最少的字符串操作次数称为字符串A 到B 的编辑距离,记为d(A, B)。请设计一个算法,对任意给定的两个字符串A 和B,计算出他们的编辑距离d(A, B)。
我是从另一面考虑这个问题的!希望计算A变到B的最少步骤,我可以先看看A和B之间有多少是相同的,然后把一些不相同的按照相应操作变就是了。这里能这么做是因为所提供的操作允许我们有这样的假设。
比如,kitten和sitting有ittn是相同的(不必须他们连续),那么这些相同的是我们不需要动的,然后就是要把ke变成sig了,这里应为各个规则权重一样,那么就是3了;如果各个规则权重不一样就没那么简单了。
依据上面的分析,那么我们主要就是要找到两字符串的公共字符串的大小,然后用两字符串的长度的少大值减去它就可以了。也就是:编辑距离=max(字符串1的长度,字符串2的长度)-两字符串的最长公共长度。
好的,现在重点就是要计算两字符串的最长公共长度。我是这样考虑的,如果要计算str1与str2的最长公共长度。我首先从str1的字符从左到右一个个来,假设str1前m个字符与str2前n个字符的最长公共长度为dist[m][n],那么假设str1前m+1个字符与str2前n+1个字符的最长公共长度就为以下两种情况了:(1)str1[m+1]==str2[n+1],这个时候的dist[m+1][n+1]就应该是dist[m][n]+1了;(2) str1[m+1]!=str2[n+1] ,这个时候的dist[m+1][n+1]就应该是dist[m][n+1]和dist[m+1][n]中较大点的那个了。好了,就是这样思想,这是一种动态规划的想法,下面给出动态规划状态转移方程:
dist[m][n]=(1):dist[m-1][n-1],str1[m]==str2[n];(2):max(dist[m-1][n],dist[m][n-1]),else
(注,我不会在这里写数学公式)
好的,下面给出代码,因为听报告的时候电脑没安装C语言开发环境,又没网下不了DEV-C++,所以就直接用Java语言写了(其实这种问题用C语言作为描述代码更好,大家委屈下,因为我也懒的再写一个了)。
代码1:
//求解两字符串的编辑距离
public static int EditDistance(String str1,String str2)
{
char[] strs1=str1.toCharArray();//转换为字符数组
char[] strs2=str2.toCharArray();//转换为字符数组
int[][] dist=new int[strs1.length+1][strs2.length+1];//定义距离二维数组,多定义一维是为了避免边界检测
int i,j,temp;
for(i=0;i<=strs1.length;i++) dist[i][0]=0;//初始化边界值
for(i=0;i<=strs2.length;i++) dist[0][i]=0;//初始化边界值
for(i=1;i<=strs1.length;i++)
{
for(j=1;j<=strs2.length;j++)
{
if(strs1[i-1]==strs2[j-1])//如果两字符相等,则取dist[i-1][j-1]+1
{
temp=dist[i-1][j-1]+1;
}
else//不等,则取dist[i-1][j]与dist[i][j-1]的最大值
{
temp=dist[i][j-1];
if(dist[i-1][j]>temp) temp=dist[i-1][j];
}
dist[i][j]=temp;
}
}
temp=dist[strs1.length][strs2.length];
if(strs1.length>strs2.length)
return strs1.length-temp;
else
return strs2.length-temp;
}
对这个程序作出时空分析的时候发现,时间复杂度为O(mn),空间复杂度也为O(mn),有没有改进的余地呢?其实还是有的。注意到,每次状态转移的时候,我们只需考虑dist[i-1][*]这一维数组和dist[i][*]这一维,所以我们时候只用两个一维数组就可以了呢,然后使用两个数组一次轮倒(轮倒是我创造的词,意思就是轮流的倒来倒去),其实都不用轮倒,只要每次把[i][*]的结果用[i-1][*]保存就好了!
代码二:
//求解两字符串的编辑距离的优化算法,旨在解决其空间复杂度过高(达到0(m*n))的情况,优化后空间复杂度为o(n)
public static int EditDistanceUp(String str1,String str2)
{
char[] strs1=str1.toCharArray();//转换为字符数组
char[] strs2=str2.toCharArray();//转换为字符数组
int[][] dist=new int[2][strs2.length+1];//定义距离二维数组,多定义一维是为了避免边界检测,
//每次都在dist[0][*]与dist[1][*]之间倒
//dist[0][*]存储上次最优结果,也就是dist[i-1][j]
//dist[1][*]存储这次最优结果,也就是dist[i][j]
int i,j,temp;
for(i=0;i<=strs2.length;i++) dist[0][i]=0;//初始化边界值
dist[1][0]=0;
for(i=1;i<=strs1.length;i++)
{
for(j=1;j<=strs2.length;j++)
{
if(strs1[i-1]==strs2[j-1])//如果两字符相等
{
temp=dist[0][j-1]+1;
}
else//不等,则取dist[i-1][j]与dist[i][j-1]的最大值
{
temp=dist[1][j-1];
if(dist[0][j]>temp) temp=dist[0][j];
}
dist[1][j]=temp;
}
for(j=0;j<=strs2.length;j++)
dist[0][j]=dist[1][j];
}
temp=dist[1][strs2.length];
if(strs1.length>strs2.length)
return strs1.length-temp;
else
return strs2.length-temp;
}
暂时就想到这么多了,应该还有改进的余地!如你有好方法解决此类问题,还忘不吝赐教。
PS:祝元旦快乐!一年更比一年好!