字符串的编辑距离也被称为距Levenshtein距离(Levenshtein Distance),属于经典算法,常用方法使用递归,更好的方法是使用动态规划算法,以避免出现重叠子问题的反复计算,减少系统开销。

《编程之美》一书中3.3节中计算两个字符串的相似度,归根到底也是要求两个字符串的距离,其中问题是这样提出的:

  许多程序会大量使用字符串。对于不同的字符串,我们希望能够有办法判断其相似程序。我们定义一套操作方法来把两个不相同的字符串变得相同,具体的操作方法为:

  •         修改一个字符(如把"a"替换为"b");
  •         增加一个字符(如把"abdd"变为"aebdd");
  •         删除一个字符(如把"travelling"变为"traveling");

    比如,对于"abcdefg"和"abcdef"两个字符串来说,我们认为可以通过增加/减少一个"g"的方式来达到目的。上面的两种方案,都仅需要一 次 。把这个操作所需要的次数定义为两个字符串的距离,而相似度等于"距离+1"的倒数。也就是说,"abcdefg"和"abcdef"的距离为1,相似度 为1/2=0.5。给定任意两个字符串,你是否能写出一个算法来计算它们的相似度呢? 

  其实这个问题的关键是要求两个字符串的编辑距离

例如 将kitten一字转成sitting:

  1. sitten (k→s)

  2. sittin (e→i)

  3. sitting (→g)

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

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

方法1:递归法,时间复杂度O(log(max(M,N)*M*N)

假设字符串 a, 共 m 位,从 a[1] 到 a[m]
字符串 b, 共 n 位,从 b[1] 到 b[n]
d[i][j] 表示字符串 a[1]-a[i] 转换为 b[1]-b[j] 的编辑距离

那么有如下递归规律(a[i] 和 b[j] 分别是字符串 a 和 b 的最后一位):

  1. 当 a[i] 等于 b[j] 时,d[i][j] = d[i-1][j-1], 比如 fxy -> fay 的编辑距离等于 fx -> fa 的编辑距离
  2. 当 a[i] 不等于 b[j] 时,d[i][j] 等于如下 3 项的最小值:
    • d[i-1][j] + 1(删除 a[i]), 比如 fxy -> fab 的编辑距离 = fx -> fab 的编辑距离 + 1
    • d[i][j-1] + 1(插入 b[j]), 比如 fxy -> fab 的编辑距离 = fxyb -> fab 的编辑距离 + 1 = fxy -> fa 的编辑距离 + 1
    • d[i-1][j-1] + 1(将 a[i] 替换为 b[j]), 比如 fxy -> fab 的编辑距离 = fxb -> fab 的编辑距离 + 1 = fx -> fa 的编辑距离 + 1

递归边界:

  1. a[i][0] = i, b 字符串为空,表示将 a[1]-a[i] 全部删除,所以编辑距离为 i
  2. a[0][j] = j, a 字符串为空,表示 a 插入 b[1]-b[j],所以编辑距离为 j
int edit_distance(char *a, char *b, int i, int j)
{if (j == 0) {return i;} else if (i == 0) {return j;// 算法中 a, b 字符串下标从 1 开始,c 语言从 0 开始,所以 -1} else if (a[i-1] == b[j-1]) {return edit_distance(a, b, i - 1, j - 1);} else {return min_of_three(edit_distance(a, b, i - 1, j) + 1,edit_distance(a, b, i, j - 1) + 1,edit_distance(a, b, i - 1, j - 1) + 1);}
}edit_distance(stra, strb, strlen(stra), strlen(strb));

但是有个严重的问题,就是代码的性能很低下,时间复杂度是指数增长的
上面的代码中,很多相同的子问题其实是经过了多次求解,解决这类问题的办法是用动态规划

下面我们就针对这个问题来详细阐述一下:

我们假定函数dist(str1, str2)表示字串str1转变到字串str2的编辑距离,那么对于下面3种极端情况,我们很容易给出解答(0表示空串)。

  • dist(0, 0) = 0

  • dist(0, s) = strlen(s)

  • dist(s, 0) = strlen(s)

对于一般的情况,dist(str1, str2)我们应该如何求解呢?

假定我们现在正在求解dist(str1+char1, str2+char2),也就是把"str1+char1"转变成"str2+char2"。在这个转变过称中,我们要分情况讨论:

  1. str1可以直接转变成str2。这时我们只要把char1转成char2就可以了(如果char1 != char2)。

  2. str1+char1可以直接转变成str2。这时我们处理的方式是插入char2。

  3. str1可以直接转成str2+char2。这时的情况是我们需要删除char1。

  综合上面三种情况,dist(str1+char1, str2+char2)应该是三者的最小值。

解析:

首先定义这样一个函数——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

  • if i ≥ 1  且 j ≥ 1 ,edit(i, j) == min{ edit(i-1, j) + 1, edit(i, j-1) + 1, edit(i-1, j-1) + f(i, j) },当第一个字符串的第i个字符不等于第二个字符串的第j个字符时,f(i, j) = 1;否则,f(i, j) = 0。

我们建立以下表格,将两个字符串按照表格1所示的样子进行摆放,规则按照以上公式进行输入,如下所示,我们可以得到每个表格中的值,如下表格2所示:

 

0

a

b

c

d

e

f

0

a

c

e

           表格1(字符串摆放表格)

 

0

a

b

c

d

e

f

0

0

1

2

3

4

5

6

a

1

c

2

e

3

表格2(按照规则计算i==0 或 j==0的情况)

计算edit(1, 1),edit(0, 1) + 1 == 2,edit(1, 0) + 1 == 2,edit(0, 0) + f(1, 1) == 0 + 1 == 1,min(edit(0, 1),edit(1, 0),edit(0, 0) + f(1, 1))==1,因此edit(1, 1) == 1。依次类推,有如下表格3所示最终的矩阵:

 

0

a

b

c

d

e

f

0

0

1

2

3

4

5

6

a

1

0

1

2

3

4

5

c

2

1

1

1

2

3

4

e

3

2

2

2

2

2

3

表格3(最终计算得到的字符串相对距离)

此时右下角即为我们所需要的两个字符串的编辑距离。即字符串 "abcdef"和"ace"的编辑距离为3.

有了以上的步骤,相信大家已经很清楚了,使用动态规划算法的时候,需要建立子问题的表格,以上的表格就是。而且我们能够很容易的使用二维数组建立。代码实现也就易如反掌了!

以下是我的实现过程,希望对大家有用,如果有什么可以优化或者错误的地方,希望能够得到批评指正。

  1 #include <iostream>2 #include <string>3 4 using namespace  std;5 6 int min3Value(int a, int b, int c)7 {8     int tmp = (a <= b? a:b);9     return (tmp<=c? tmp: c);10 }11 12 13 int Get2StringEditDis(string strA, string strB)14 {15     int nLenA = strA.length();16     int nLenB = strB.length();17     int **matrix = new int *[nLenA + 1];18     for (int i = 0; i != nLenA +1; i++)19     {20         matrix[i] = new int[nLenB + 1];21     }22     // 动态规划 计算23     // 初始化数组24     matrix[0][0] = 0;25     int p,q; 26     // j = 0; edit(i, j) = i27     for (p = 1; p!= nLenA+1; p++)28     {29         matrix[p][0] = p;30     }31     // i = 0; edit(i,j) = j32     for (q=1; q != nLenB+1; q++)33     {34         matrix[0][q] = q;35     }36     // i>0, j>037     for (int j = 1; j != nLenA+1; j++)38     {39         for (int k = 1; k !=  nLenB+1; k++)40         {41             int Fjk = 0;42             if (strA[j-1] != strB[k-1])43             {44                 Fjk = 1;45             }46             matrix[j][k] = min3Value(matrix[j-1][k]+1,matrix[j][k-1]+1,matrix[j-1][k-1]+Fjk);47         }48     }49     50 51 52 53     // 输出距离矩阵54     // 第一行输出字符串b55     // 第一列输出字符串A56     cout<<"*****************************"<<endl;57     cout<<"字符串编辑距离矩阵如下:\n";58     for (p = -1; p!= nLenA +1; p++)59     {60         for (q = -1; q !=nLenB+1; q++)61         {62             //cout.width(3),cout<<matrix[p][q];63             cout.width(3);64             if (p ==-1 && q == -1)65             {66                 cout<<" ";67             }68             else if (p + q == -1)69             {70                 cout<<"NUL";71             }72             else if (p == -1 && q >0)73             {74                 cout<<strB[q-1];75             }76             else if(q == -1 && p > 0)77             {78                 cout<<strA[p-1];79             }80             else81             {82                 cout<<matrix[p][q];83             }84         }85         cout<<endl;86     }87     cout<<"*****************************"<<endl;88     //89     int  nEditDis = matrix[nLenA][nLenB];90     for (int m = 0; m!=nLenA + 1; m++)91     {92         delete[] matrix[m];93     }94     delete[] matrix;95 96 97     return  nEditDis;98 }99
100
101 int main()
102 {
103     string strA("abcdefgh");
104     string strB("adgcf");
105
106     int nDist = Get2StringEditDis(strA,strB);
107     cout<<"The edit dis is  "<<nDist<<endl;
108
109     return 0;
110 }

根据具体问题优化空间复杂度

还是以 a = "fxy", b = "fab" 为例,例如计算 d[1][3], 也就是下图中的绿色方块, 我们需要知道的值只需 3 个,下图中蓝色方块的值

进一步分析,我们知道,当计算 d[1] 这行的时候,我们只需知道 d[0] 这行的值, 同理我们计算当前行的时候只需知道上一行就可以了
再进一步分析,其实我们只需要一行就可以了,每次计算的时候我们需要的 3 个值, 其中上边和左边的值我们可以直接得到,坐上角的值需要临时变量(如下代码使用 old)来记录

代码如下:

int edit_distance(char *a, char *b)
{int lena = strlen(a);int lenb = strlen(b);int d[lenb+1];int i, j, old, tnmp;for (j = 0; j <= lenb; j++) {d[j] = j;}for (i = 1; i <= lena; i++) {old = i - 1;d[0] = i;for (j = 1; j <= lenb; j++) {temp = d[j];// 算法中 a, b 字符串下标从 1 开始,c 语言从 0 开始,所以 -1if (a[i-1] == b[j-1]) {d[j] = old;} else {d[j] = min_of_three(d[j] + 1, d[j-1] + 1, old + 1);}old = temp;}}return d[lenb];
}

写代码的过程中需要注意的一点就是,当一行计算好之后开始下一行的时候, 要初始化 old 和 d[0] 的值

优化过后时间复杂度还是 O(mn), 空间复杂度降低了,以上代码是 O(n), 其实很简单可以写成 O(min(m,n)), 为了便于理解,就不具体写了

LeetCode经典算法精解-字符串编辑距离相关推荐

  1. 资料 | O‘Reilly精品图书系列:算法精解 C 语言描述 (简体中文)

    下载地址:资料 | O'Reilly精品图书系列:算法精解 C 语言描述 (简体中文) 内容简介 · · · · · · 本书是数据结构和算法领域的经典之作,十余年来,畅销不衰! 全书共分为三部分:第 ...

  2. JVM内存管理------GC算法精解(五分钟教你终极算法---分代搜集算法)

    转载自   JVM内存管理------GC算法精解(五分钟教你终极算法---分代搜集算法) 引言 何为终极算法? 其实就是现在的JVM采用的算法,并非真正的终极.说不定若干年以后,还会有新的终极算法, ...

  3. JVM内存管理------GC算法精解(五分钟让你彻底明白标记/清除算法)

    转载自  JVM内存管理------GC算法精解(五分钟让你彻底明白标记/清除算法) 相信不少猿友看到标题就认为LZ是标题党了,不过既然您已经被LZ忽悠进来了,那就好好的享受一顿算法大餐吧.不过LZ丑 ...

  4. JVM内存管理------GC算法精解(复制算法与标记/整理算法)

    转载自  JVM内存管理------GC算法精解(复制算法与标记/整理算法) 本次LZ和各位分享GC最后两种算法,复制算法以及标记/整理算法.上一章在讲解标记/清除算法时已经提到过,这两种算法都是在此 ...

  5. 经典算法详解--CART分类决策树、回归树和模型树

    Classification And Regression Tree(CART)是一种很重要的机器学习算法,既可以用于创建分类树(Classification Tree),也可以用于创建回归树(Reg ...

  6. 算法精解 c语言描述 豆瓣,斯坦福大学教授亲授,这本美亚4.7星的算法书,新手程序员都看得懂!...

    原标题:斯坦福大学教授亲授,这本美亚4.7星的算法书,新手程序员都看得懂! "算法会扩展并提高大家的编程技巧,而学习基本的算法设计范式,可以和许多不同领域的不同问题密切相关,还能作为预测算法 ...

  7. 机器学习10大经典算法详解

    "数据+算法=模型". 面对具体的问题,选择切合问题的模型进行求解十分重要.有经验的数据科学家根据日常算法的积累,往往能在最短时间内选择更适合该问题的算法,因此构建的模型往往更准确 ...

  8. 机器学习经典算法详解及Python实现--元算法、AdaBoost

    http://blog.csdn.net/suipingsp/article/details/41822313 第一节,元算法略述 遇到罕见病例时,医院会组织专家团进行临床会诊共同分析病例以判定结果. ...

  9. 起名算法 php,PHP实现各种经典算法详解

    //-------------------- // 基本数据结构算法 //-------------------- //二分查找(数组里查找某个元素) function bin_sch($array, ...

  10. 经典算法详解 之 递归算法

    递归算法:递归算法是把问题转化为规模缩小了的同类问题的子问题.然后递归调用函数(或过程)来表示问题的解. 递归算法是算法设计中比较常用的一种算法,它的优点在于考虑问题的角度不再局限于过程,而是从整体的 ...

最新文章

  1. redis面试全家桶
  2. 绝对自回归模型(或将解决标注问题)
  3. flex 设置换行flex-wrap
  4. Jdbc模版式写法与Spring-JdbcTemplate的比较
  5. P7717-「EZEC-10」序列【Trie】
  6. SpringCloudConfig整合Nacos
  7. 利用FormData对象实现AJAX文件上传功能及后端实现
  8. LeetCode 845. 数组中的最长山脉(中心扩展)
  9. matlab 手工实现normalize函数 未定义与 ‘double‘ 类型的输入参数相对应的函数 ‘normalize‘
  10. 水烟炭行业调研报告 - 市场现状分析与发展前景预测
  11. synchronousqueue场景_【JUC】JDK1.8源码分析之SynchronousQueue(九)
  12. linux 键盘 键值0x1e,Linux文本处理三剑客之awk学习笔记11:选项、内置变量和内置函数...
  13. 明解C语言第三章习题
  14. 想找一款读书笔记软件?快来试试BookxNote
  15. 平面设计常用标准尺寸
  16. clion msys2 Mingw 未找到
  17. Springboot集成Mybatis怎么在控制台打印sql语句
  18. 齐聚一堂:共话网络安全人才培养新模式
  19. 2001-2019年中国境内企业并购数据
  20. C语言中的逗号的作用,c语言中什么是逗号运算符并举例

热门文章

  1. Go语言的指针的一些测试
  2. 查看一个数是不是2的n次方
  3. 数据库的跨平台设计(转)
  4. 一天一个小技巧(4)——利用Python和MATLAB进行图片二值化
  5. 【填坑】Ubuntu安装vsftpd
  6. 处理收到的Stanzas
  7. 转 sql server性能分析--执行sql次数和逻辑次数
  8. nginx 启动报错 “/var/run/nginx/nginx.pid failed” 解决方法
  9. leecode第二百九十二题(Nim游戏)
  10. STM32基础分析——USART的DMA模式