一文看懂 编辑距离(Edit Distance) 动态规划算法详解 JS代码实现

最近看了一下别人的面试博客中提到了编辑距离的算法,才发现LeetCode中 这道题的难度已经从困难变成了中等,真是应了那句 “学如逆水行舟,不进则退” , 想当初一杯茶,一包烟,一道困难看一天,没想到如今能把这道题卷成中等

话不多说今天分享一下我对这道题的一些理解

        ''  r   o   s
    ''  0   1   2   3
    h   1   1   2   3
    o   2   2   1   2
    r   3   2   2   2
    s   4   3   3   2
    e   5   4   4   3

先看一下这个矩阵
我们假设 word1 = 'horse' word2 = 'ros'

第一行表示 从一个空字符串到 字符串 ‘ros’ 所需要的最小步骤 也就是 3

第一列表示 从空字符串到 字符串 ‘horse’ 所需要的最小步骤 也就是 5

每个单元格都表示 word1的前 i 个字符 转换成 word2 的前 j 个字符所需要的最小操作数

根据当前字符是否相等,来判断当前字符是否可以不需要操作,还是需要通过 替换 删除 插入来操作

可能说的有点抽象,举个例子来说

比如我想将 ‘horse’ 的前两个字符 ‘ho’ 转换成 ‘ros’ 的前三个字符

首先,我们定义 dp[i][j] 为字符串 word1 的前 i 个字符和字符串 word2 的前 j 个字符之间的最小编辑距离

‘ho’ 转换成 ‘ros’ ,在单元格中就是(2,3)单元格,此单元格就是记录 dp[2][3] 的最小距离,值是2

说明: 这里的 (2,3) 单元格 不是第二行第三列 可以看做 2 3 是数组下标 所以是 第三行第四列 因为矩阵中我们还包含了空字符串的情况 下面的单元格都是如此 看作下标 + 1即可

那么 距离2 是怎么得来的呢

首先我们 假设 每个单元格都记录着word1 前 i 个字符转换成 word2 前 j 个字符的最小距离

什么意思呢 举个例子(1,1)表示 ‘h’ 转换成 ‘r’ 的最小距离,(1,2)表示 ‘h’ 转换成 ‘ro’ 的最小距离,

(1,3)表示 ‘h’ 转换成 ‘ros’ 的最小距离,以此类推 (我们先假设这样,后面会验证)

现在我们想填充单元格 (2,3)的值,我们首先需要判断 当前 word1的 ‘ho’ 和 word2的 ‘ros’ 的最后一个字符串是否相同
如果不相同,那就需要通过 替换 删除 插入 来操作word1

我们首先看一下 (2,2)单元格,上面我们假设了每个单元格都记录着word1 前 i 个字符转换成 word2 前 j 个字符的最小距离,也就是说(2,2)代表 ‘ho’ 转换成 ‘ro’ 的最小距离,可能有人会疑问为什么跟 (2,2)有关系,因为(2,2)转换成(2,3)只需要在 ‘ho’ 后面插入 ‘s’ 即可此时'ho'已经转换成'ro',也就是说根据 (2,2)的值的基础上加一,就能将 ‘ho’ 转换成 ‘ros’

同理我们看一下(1,3)也就是 ‘h’ 转换成 ‘ros’ 的最小距离,在此基础上我们只需要删除 ‘ho’ (此时'h'已经转换成'ros') 后面的 ‘o’,也就是说根据 (1,3)的值的基础上加一,就能将 ‘ho’ 转换成 ‘ros’

再者,(1,2)就是将 ‘ho’ 的 ‘o’ 替换成 ‘ros’ 的 ‘s’, 根据 (1,2)的值的基础上加一,就能将 ‘ho’ 转换成 ‘ros’

这里我解释一下上面三个方法都要加一 是因为我们根据前一步的操作,再接上插入 删除 或替换,所以是在前一步的操作基础上加一

这三种方法都可以实现我们的需求,所以我们只需要选其中最小的一步即可获取最小距离,这个时候发现最小的值是 (2,2) + 1 = 2,所以(2,3)的最小值就是 2

刚才我们假设 每个单元格都记录着word1 前 i 个字符转换成 word2 前 j 个字符的最小距离

现在我们来验证一下

矩阵中的第一行第一列上面提到了,分别代表从一个空字符串到 字符串 ‘ros’ 所需要的步骤和从空字符串到 字符串 ‘horse’ 所需要的最少步骤

        ''  r   o   s
    ''  0   1   2   3
    h   1   
    o   2  
    r   3  
    s   4   
    e   5   

这个值是固定的,应该都能理解吧,现在我们要填充剩余的矩阵,那么按照上面我们的方法,当前单元格(i, j)的最小值就是从 (i-1, i)(i, j-1)(i-1, j-1) 中取最小值在加一,我们所填充的每一步都是根据最小距离的基准来计算,所以我们填充的单元格一定是表示 当前 word1[i] 转换成 word2[j] 的最小距离

但是,其中还包含着一种特殊情况,就是当前字符相同的情况,什么意思呢,还是举例说明

我们在填充 (2, 2)时,也就是将 ‘ho’ 转换成 ‘ro’, 发现 ‘ho’ 的 ‘o’ 与 ‘ro’ 的 ‘o’ 相同

    	''  r   o   s
    ''  0   1   2   3
    h   1   1   2   3
    o   2   2   ?
    r   3  
    s   4   
    e   5   

这个时候按照我们上面的方法,我们会取(1,1)的值在加一 最后填充(2, 2)的值为 2,但是结果并不如此,因为这种情况,我们可以直接继承 (1, 1)的值,为什么呢,因为当前字符如果相同,我们可以不进行操作,我们只需要考虑将 ‘h’ 转换成 ‘r’ 即可,所以(2, 2)的值 等于 (1, 1)的值 也就是 1

至此,我们可以以最优解完整的填充整个矩阵,最小值也就是矩阵的右下角

动态规划通过填充一个矩阵来解决编辑距离问题,每个单元格的填充操作基于其相邻的三个单元格(左边、上边和左上角)

下面我用js代码,实现以上操作

function minDistance(word1, word2) {
   const m = word1.length, n = word2.length;
  
  //初始化dp数组(生成 m+1 行, n+1 列的矩阵)
  const dp = Array.from({length: m + 1}, () => Array(n + 1).fill(0));
  
  //初始化边界条件(可以理解为填充第一行第一列)
  for(i = 0; i <= m; i++) {
    dp[i][0] = i //填充第一列
  }
  
  for(j = 0; j <= n; j++) {
    dp[0][j] = j //填充第一列
  }
  
  //填充矩阵
  for(i = 0; i <= m; i++) {
    for(j = 0; j <= n; j++) {
      if(word1[i-1] === word2[j-1]) {
        //如果相同 直接继承
        dp[i][j] = dp[i-1][j-1] 
      }else {
        //如果不同,从上一步操作中选取最小值,并 +1 填充
        dp[i][j] = Math.min(dp[i][j-1], dp[i-1][j], dp[i-1][j-1]) + 1
      }
    }
  }
  
  return dp[m][n] //返回右下角的值(最小距离)
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值