算法笔记:编辑距离
原题:
给出两个单词word1和word2,计算出将word1 转换为word2的最少操作次数。
你总共三种操作方法:
- 插入一个字符
- 删除一个字符
- 替换一个字符
给出 work1=”mart” 和 work2=”karma”
返回 3
下面是两种思路实现,自顶向上方法和自顶向下方法
1.自底向上方法实现
不如将问题简化下,化成规模比较小的问题。
首先,我们先考虑两个空串,那么当然就会出现0次。
若其中有一个为空串,另外一个为单字符的话,若A为空的话,只要执行一次插入操作,就能达到目的。反之只要执行一次删除次数操作。
对于非空串来说,我们先考虑只有单字符的A和B,不相等的话就只需要执行一次替换操作,就可以到达目的。
我们将上面操作视为单字符操作
对于双字符的情况,它可以视作单字符完成后再进行单字符操作的情况。对于有三个字符的,同样视作双字符操作完成后再执行单字符判断操作的情况。以此类推,那么对于任意长度n的字符串,都可以看成n-1字符串完成后再进行单字符操作。
这是一种动态规划的算法。于是我们就可以使用一个result[i][j]数组来表示字符串A的从起始到i的子串到匹配字符串B从起始到j的子串所需要进行的操作数。
那么问题来了,我们应该如何表示单字符操作呢?
正如上面定义所说的。当A为空的话,那么就执行插入。那什么时候会出现这么一种情况?
当A的长度比B要小的时候,那么我们就需要进行插入操作。
当我们用result[i][j]形式表示该操作的时候,就表示为result[i][j] = result[i][j-1]+1
(为什么这里是result[i][j-1] 而不是result[i - 1][j]?
因为我们result[i][j-1]表示A的前i个和B的前j-1的已经匹配好的了,所以当j-1加1后,i就比较小了。所以就需要进行插入操作。
)
同理,当A的长度比B要大的时候,我们就要需要删除操作
表示为result[i][j] = result[i-1][j] + 1;
那么替换呢?
很简单,两个一样长,如果不相等就替换。
表示为result[i][j] = result[i-1][j-1] + x(x = 0 or 1)
我们可以用下图来表示他们之间的关系
迭代算法
public class Solution {
public int minDistance(String word1, String word2) {
int[][] result = new int[word1.length()+1][word2.length()+1];
//初始化数组
for(int i = 0 ; i <= word1.length() ; i++){
result[i][0] = i;
}
for(int j = 0 ; j <= word2.length() ; j++){
result[0][j] = j ;
}
for(int i = 1 ; i <= word1.length() ; i++){
for(int j = 1; j <= word2.length() ;j++){
// 单字符操作
if(word1.charAt(i-1) == word2.charAt(j-1)){
result[i][j] = result[i-1][j-1];
} else {
result[i][j] = min(result[i-1][j-1],result[i-1][j],
result[i][j-1]) + 1;
}
}
}
return result[word1.length()][word2.length()];
}
public int min(int t1,int t2,int t3){
int min = t1 > t2? t2 : t1;
min = min > t3? t3 : min;
return min;
}
}
2.自顶向下方法实现
当然,不止那么一种算法。我们刚才是通过从后往前推的方式推导出结果的。
我们也可以换一种思路,从前往后退的方式进行推导。
对于空串的处理,我们和上面的处理是一样的
那么就对于不为空串的字符串就有
- 删除A第一个字符后,将A从2到尾的子串和B从1到尾的子串变成相同字符串
- 删除B第一个字符后,将A从1到尾的子串和B从2到尾的子串变成相同字符串
- 替换A或B第一个字符后,将A从2到尾的子串和B从2到尾的子串变成相同字符串
- 增加B第一个字符到A第一个字符后,将A从1到尾的子串和B从2到尾的子串变成相同字符串
- 增加A第一个字符到B第一个字符后,将A从2到尾的子串和B从1到尾的子串变成相同字符串
综上,我们可以得出如下结论
- 一步操作后,将A从1到尾的子串和B从2到尾的子串变成相同的字符串
- 一步操作后,将A从2到尾的子串和B从1到尾的子串变成相同的字符串
- 一步操作后,将A从2到尾的子串和B从2到尾的子串变成相同的字符串
那么我们就可以通过递归的方式得出结果
递归算法
public class Solution {
public int minDistance(String word1, String word2) {
// 初始化结果数组
return caculateDistance(word1,0,word1.length()-1,
word2,0,word2.length() - 1);
}
public int caculateDistance(String word1,int begin1,int end1,String word2,int begin2,int end2){
if(begin1 > end1){
if(begin2 > end2){
return 0;
}
else{
return end2 - begin2 + 1;
}
}
if(begin2 > end2){
if(begin1 > end1){
return 0;
}
else{
return end1 - begin1 + 1;
}
}
//相等直接跳下一个
if(word1.charAt(begin1) == word2.charAt(begin2)){
return caculateDistance(word1,begin1+1,end1,word2,begin2+1,end2);
}else {
//情况1
int t1 = caculateDistance(word1,begin1,end1,word2,begin2+1,end2);
//情况2
int t2 = caculateDistance(word1,begin1+1,end1,word2,begin2,end2);
//情况3
int t3 = caculateDistance(word1,begin1+1,end1,word2,begin2+1,end2);
return min(t1,t2,t3)+1;
}
}
public int min(int t1,int t2,int t3){
int min = t1 > t2? t2 : t1;
min = min > t3? t3 : min;
return min;
}
}
上面的算法就是《编程之美》里面提供的算法。
不过我们可以发现,它在每一次调用的时候都会进行三次的自我递归调用。不难发现他的时间复杂度是O(3^n)。是一种很糟糕的算法。对于特别大的数据,耗时是非常大的。那么有没有改进措施呢?
该书也给出了一种改进的方法。
如下图是部分展开的递归调用
(注:图来源于《编程之美》)
可以发现,圈中的两个子问题被重复计算了。那么我们就像能不能减少计算的量。既然重复计算了,那么第二次计算就是没有必要的。所以我们最有效的方法就是把计算结果存储起来。当第二次调用的时候,我们直接用就好了。
下面是优化后的算法
带记忆的递归算法
public class Solution {
//用来记录结果
int[][] result;
public int minDistance(String word1, String word2) {
// 初始化结果数组
result = new int[word1.length()+1][word2.length()+1];
for(int i = 0; i < word1.length()+1 ;i++){
for(int j = 0; j <word2.length()+1 ;j++){
result[i][j] = -1 ;
}
}
return caculateDistance(word1,0,word1.length()-1,
word2,0,word2.length() - 1);
}
public int caculateDistance(String word1,int begin1,int end1,String word2,int begin2,int end2){
if(result[begin1][begin2] > 0 ){
return result[begin1][begin2];
}
if(begin1 > end1){
if(begin2 > end2){
result[begin1][begin2] = 0;
return 0;
}
else{
result[begin1][begin2] = end2 - begin2 + 1;
return result[begin1][begin2];
}
}
if(begin2 > end2){
if(begin1 > end1){
result[begin1][begin2] = 0;
return 0;
}
else{
result[begin1][begin2] = end1 - begin1 + 1;
return result[begin1][begin2];
}
}
if(word1.charAt(begin1) == word2.charAt(begin2)){
return caculateDistance(word1,begin1+1,end1,word2,begin2+1,end2);
}else {
int t1 = caculateDistance(word1,begin1,end1,word2,begin2+1,end2);
int t2 = caculateDistance(word1,begin1+1,end1,word2,begin2,end2);
int t3 = caculateDistance(word1,begin1+1,end1,word2,begin2+1,end2);
result[begin1][begin2] = min(t1,t2,t3)+1;
return result[begin1][begin2];
}
}
public int min(int t1,int t2,int t3){
int min = t1 > t2? t2 : t1;
min = min > t3? t3 : min;
return min;
}
}