求两个字符串的最短编辑距离

   

      1.最小编辑距离的概念


        编辑距离是一种字符串之间相似程度的计算方法。按照Damerau给出的定义,即两个字符串之间的编辑距离等于使一个字符串变成另外一个字符串而进行如下操作的最少次数。用edit来表示编辑距离。共四种操作,分别是:a插入,b删除,c替换,d相邻字符交换位置。
  比如:edit("happy", "hpapy") == 1(需要交换两个相邻字符"a"和"p"的位置);edit("sailn", "failing") == 3(需要将"s"换成"f",在字母"l"后边插入"i","n"后面插入"g")。有的时候,变换并不唯一,重要的是要求出这一些变换路径中最短的数量。

        2.两字符串最小编辑距离的算法实现


        关于编辑距离的求法,普遍的采用的是动态规划方法。动态规划其实就是把一个复杂的最优解问题分解成一系列较为简单的最优解问题,再将较为简单的最优解问题一步步分解,直到能够一眼看出为止。下面以"sailn"和"failing"这两个字符串作例子进行说明。定义这样一个函数——edit(i, j),它表示字符串x的长度为i的子串到字符串y的长度为j的子串的编辑距离。


        算法开始要先初始化edit(0, j) = j(字符串1子串长度为0,字符串2子串有多少个字符,就作多少次增加操作;于是同理,edit(i, 0) = i)。这里,要注意到对于操作(d),即交换相邻字符串的操作,要把某个字符通过这个操作到另一个位置上,最多只能执行一次操作,即只能移动到邻位上。原因是什么呢?这是因为,移动两次的话,就没有优势了,它的操作等于两次替换操作的操作数。大于2次的时候,移动操作会更差。所以,操作(d),只发生在相邻的字符之间。


        通过分析可以得出如下的动态规划公式:
        (1)如果 i == 0 且 j == 0,edit(i, j) = 0(两个空串无所谓编辑距离,定义为0)
        (2)如果 i == 0 且 j > 0,edit(i, j) = j(前面已说明)
        (3)如果 i > 0 且 j == 0,edit(i, j) = i(前面已说明)
        (4)如果0 < i ≤ 1  且 0 < j ≤ 1 ,edit(i, j) == min{ edit(i-1, j) + 1, edit(i, j-1) + 1, edit(i-1, j-1) + f(i, j) },这里当字符串1的第i个字符不等于字符串2的第j个字符时,f(i, j) = 1;否则,f(i, j) = 0。
        (5)如果i > 1且 j > 1时,这个时候可能出现操作(d),由之前的推导,我们只能交换一次,否则就没有意义。这个时候在比较最小值中可能加入edit(i-2, j-2) +1,什么时候加入呢?假设i-2长度的字符串1子串和j-2长度的字符串2子串已经得出最优解,这个时候如果s1[i-1] == s2[j] 并且s1[i] == s2[j-1],这时就在比较值中加入edit(i-2, j-2) + 1(这个1是交换一次的操作)
    
        通过上述公式可以就可以写出算法了,算法的实现有两种形式,一种是递归实现,一种是非递归实现。不管实现形式如何,二者都需要用到一个辅助数组,用来存放计算过程中所有的edit值,最后一行,最后一列的那个值就是最小编辑距离。正是由于这个数组的存在,算法才能够高效的运行。因为后面的edit值和前面的edit值有关,将他们都存储起来,后面计算时就会高效很多。
  另外对于非递归实现的算法而言,辅助数组的大小还可以压缩。因为程序是一行一行对edit值进行计算的,任何一个时刻的edit计算都只和当前行前面的元素以及前面两行的元素相关。因此,可以只使用一个三行的数组,交替使用,便可以达到节省空间开销的目的。但是,从后面的对实际问题进行测试的结果发现,优化后运行时间比之前有所增长。实际问题中要根据具体的要求决定具体的实现方式。

        3.算法的正确性证明


        (1)在第一行与第一列肯定是正确的,这也很好理解,例如将kitten转换为空字符串,我们需要进行的操作数为kitten的长度(所进行的操作为将kitten所有的字符丢弃)。
        (2)对字符可能进行的操作有四种:
           1).如果可以使用k次操作把s[1…i]转换为t[1…j-1],接着只需要把t[j]加在最后面就能将s[1…i]转换为t[1…j],总共所需操作次数为k+1
           2).如果可以使用k次操作把s[1…i-1]转换为t[1…j],接着只需要把s[i]从最后删除就可以完成转换,总共所需操作次数为k+1
           3).如果可以使用k次操作把s[1…i-1]转换为t[1…j-1],接着只需要在需要的情况下(s[i] != t[j])把s[i]替换为t[j],总共所需的操作次数为k+cost(cost代表是否需要转换,如果s[i]==t[j],则cost为0,否则为1)。
           4).如果可以使用k次操作把s[1…i-1]转换为t[1…j],并且s[i]=t[j-1],s[i-1]=t[j],接着只需要把s[i]和s[i-1]交换即可以完成转换,总共所需操作次数为k+1
        (3)将s[1…n]转换为t[1…m]当然需要将所有的s转换为所有的t,所以,edit[n,m](辅助数组右下角)就是我们所需的结果。

        (4)这个证明过程只能证明我们可以得到结果,但并没有证明结果是最小的(即我们得到的是最少的转换步骤)。所以算法中引进了一个函数min(),即edit[i,j]保存的是上述三种操作中操作数最小的一种。这就保证了我们获得的结果是最小的操作次数。


        4.算法的时间复杂度分析


        算法的主要任务是计算辅助数组各个元素的值。数组中共有(m+1)* (n+1)个元素,计算他们的值时间复杂度为O(m*n)。因此算法的时间复杂度为O(m*n),递归实现和非递归实现都是如此,但是递归过程涉及函数创建、堆栈的分配等过程,性能上会有所降低。实际的测试也说明了这一点。另外空间复杂度优化后的非递归实现也包含了一些其他的开销,所以性能比优化前也有所降低。下面是对随机产生的字符串进行测试所得的结果:

字符串X长度

字符串Y长度

最短编辑距离

递归时间(ms

非递归时间(ms

非递归(空间优化)时间(ms

20

30

25

82

0

1

200

300

248

13335

3

5

500

1000

813

116793

29

35

2000

1000

1633

483133

135

159


        5.完整源代码

#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

const int X = 20, Y = 30;

//1.递归实现
int edit_length_1(string x, string y, int edit[][Y+1], int xlen, int ylen, int i, int j){
	if(i >= 0 && j >= 0 && i <=xlen && j <= ylen){
		if(i == 0 || j == 0 || edit[i][j] != 100000)
			return edit[i][j];
		else{
			if(x[i-1] == y[j-1]){
				return edit[i][j] = min(min(edit_length_1(x,y,edit,xlen,ylen,i,j-1) + 1,
							    edit_length_1(x,y,edit,xlen,ylen,i-1,j) + 1),
							edit_length_1(x,y,edit,xlen,ylen,i-1,j-1));
			}else{
				if(i >= 2 && j >= 2 && x[i-2] == y[j-1] && x[i-1] == y[j-2]){
					return edit[i][j] = min(min(edit_length_1(x,y,edit,xlen,ylen,i,j-1) + 1,
								    edit_length_1(x,y,edit,xlen,ylen,i-1,j) + 1),
                                                         	min(edit_length_1(x,y,edit,xlen,ylen,i-1,j-1) + 1,
								    edit_length_1(x,y,edit,xlen,ylen,i-2,j-2) +1));
				}else{
					return edit[i][j] = min(min(edit_length_1(x,y,edit,xlen,ylen,i,j-1) + 1,
                                                             	    edit_length_1(x,y,edit,xlen,ylen,i-1,j) + 1),
                                                                edit_length_1(x,y,edit,xlen,ylen,i-1,j-1) + 1);

				}
			}
		}
	}else{
		return 0;
	}	
}

//2.非递归实现
int edit_length_2(string x, string y, int edit[][Y+1], int xlen, int ylen){
	int i = 0, j = 0;
	for(i = 0; i <= xlen; i++){
		edit[i][0] = i;
	}
	for(j = 0; j <= ylen; j++){
		edit[0][j] = j;
	}
	for(i = 1; i <= xlen; i++ ){
		for(j = 1; j <= ylen; j++){
			if(x[i-1] == y[j-1]){
				edit[i][j] = min(min(edit[i][j-1] + 1, edit[i-1][j] + 1), edit[i-1][j-1]);
			}else{
				if(i >= 2 && j >= 2 && x[i-2] == y[j-1] && x[i-1] == y[j-2]){
					edit[i][j] = min(min(edit[i][j-1] + 1, edit[i-1][j] + 1), min(edit[i-1][j-1] + 1, edit[i-2][j-2] + 1));
				}else{
			
					edit[i][j] = min(min(edit[i][j-1] + 1, edit[i-1][j] + 1), edit[i-1][j-1] + 1);
				}
			}
		}
	}
	return edit[xlen][ylen];
}

//3.非递归实现,空间优化
int edit_length_3(string x, string y, int edit[][Y+1], int xlen, int ylen){
	int i = 0, j = 0;
	//首行元素初始化
	for(j = 0; j <= ylen; j++){
		edit[0][j] = j;
	}
	for(i = 1; i <= xlen; i++ ){
		edit[i%3][0] = edit[(i-1)%3][0] + 1;
		for(j = 1; j <= ylen; j++){
			if(x[i-1] == y[j-1]){
				edit[i%3][j] = min(min(edit[i%3][j-1] + 1, edit[(i-1)%3][j] + 1), edit[(i-1)%3][j-1]);
			}else{
				if(i >= 2 && j >= 2 && x[i-2] == y[j-1] && x[i-1] == y[j-2]){
					edit[i%3][j] = min(min(edit[i%3][j-1] + 1, edit[(i-1)%3][j] + 1), min(edit[(i-1)%3][j-1] + 1, edit[(i-2)%3][j-2] + 1));
				}else{
			
					edit[i%3][j] = min(min(edit[i%3][j-1] + 1, edit[(i-1)%3][j] + 1), edit[(i-1)%3][j-1] + 1);
				}
			}
			//cout << edit[i%3][j] << " ";
		}	
		//cout << endl;
	}
	return edit[(i-1)%3][j-1];
}

void test_1(string x, string y){
	int xlen = x.length();
	int ylen = y.length();
	int edit[X+1][Y+1] = {100};
	for(int i = 0; i <= xlen; i++){
		for(int j = 0; j <= ylen; j++){
			edit[i][j] = 100000;
		}	
	}
	for(int i = 0; i <= xlen; i++){
		edit[i][0] = i;
	}
	for(int j = 0; j <= ylen; j++){
		edit[0][j] = j;
	}
	/*
	cout << "分析前的数组状态" << endl;
	for(int i = 0; i <= xlen; i++){
		for(int j = 0; j <= ylen; j++){
			cout << edit[i][j] << " ";
		}	
		cout << endl;
	}
	cout << endl;
	*/
	int max_len = edit_length_1(x, y, edit, xlen, ylen, xlen, ylen);
	/*
	cout << "分析后的数组状态:" << endl;
	for(int i = 0; i <= xlen; i++){
		for(int j = 0; j <= ylen; j++){
			cout << edit[i][j] << " ";
		}	
		cout << endl;
	}
	cout << endl;
	*/
	cout << "1.递归分析这两个字符串的最短编辑距离为:" << max_len << endl;
}

void test_2(string x, string y){
	int xlen = x.length();
	int ylen = y.length();
	int edit[X+1][Y+1];
	memset(edit, 0, sizeof(edit));
	//分析前的数组状态
	/*
	cout << "分析前的数组状态:" << endl;
	for(int i = 0; i <= xlen; i++){
		for(int j = 0; j <= ylen; j++){
			cout << edit[i][j] << " ";
		}	
		cout << endl;
	}
	cout << endl;
	*/
	int max_len = edit_length_2(x, y, edit, xlen, ylen);
	/*
	cout << "分析后的数组状态:" << endl;
	for(int i = 0; i <= xlen; i++){
		for(int j = 0; j <= ylen; j++){
			cout << edit[i][j] << " ";
		}	
		cout << endl;
	}
	cout << endl;
	*/
	cout << "2.非递归分析这两个字符串的最短编辑距离为:" << max_len << endl;
}

void test_3(string x, string y){
	int xlen = x.length();
	int ylen = y.length();
	int edit[3][Y+1];
	memset(edit, 0, sizeof(edit));
	//分析前的数组状态
	/*
	cout << "分析前的数组状态:" << endl;
	for(int i = 0; i <= 2; i++){
		for(int j = 0; j <= ylen; j++){
			cout << edit[i][j] << " ";
		}	
		cout << endl;
	}
	cout << endl;
	*/
	int max_len = edit_length_3(x, y, edit, xlen, ylen);
	/*
	cout << "分析后的数组状态:" << endl;
	for(int i = 0; i <= 2; i++){
		for(int j = 0; j <= ylen; j++){
			cout << edit[i][j] << " ";
		}	
		cout << endl;
	}
	cout << endl;
	*/
	cout << "3.非递归(空间优化)分析这两个字符串的最短编辑距离为:" << max_len << endl;
}

char* rand_string(char *str, int len){
	int i = 0;
	srand((unsigned)time(NULL));
	for(i = 0; i < len; i++)
		str[i] = rand()%26 + 'a';
	str[i] = '\0';
	return str;
}

long getCurrentTime(){
	struct timeval tv;
	gettimeofday(&tv, NULL);
	return tv.tv_sec * 1000 + tv.tv_usec / 1000;
}

int main(){
	
	char str_1[X+1];
	rand_string(str_1, X);
	string x(str_1);
	sleep(1);
	char str_2[Y+1];
	rand_string(str_2, Y);
	string y(str_2);

	cout << "string x = " << x << endl;
	cout << "  字符串长度为:" << x.length() << endl;
	cout << "string y = " << y << endl;
	cout << "  字符串长度为:" << y.length() << endl;

	long time_1 = getCurrentTime();
	test_1(x, y);
	long time_2 = getCurrentTime();
	cout << "  用时:";
	cout << time_2 - time_1 << " ms;" << endl;
	
	long time_3 = getCurrentTime();
	test_2(x, y);
	long time_4 = getCurrentTime();
	cout << "  用时:";
	cout << time_4 - time_3 << " ms;" << endl;

	long time_5 = getCurrentTime();
	test_3(x, y);
	long time_6 = getCurrentTime();
	cout << "  用时:";
	cout << time_6 - time_5 << " ms;" << endl;
	
}



你可能感兴趣的:(算法学习)