最小编辑距离问题

最近在看一些react里面的Virtual DOM的相关文章,看到了看到了 livoras 的这篇文章,其中讲到了在比较两棵虚拟DOM树的差异的时候会用到字符串最小编辑距离的算法,因为那篇文章主要讲述的点并不在此,所以对于这个算法着墨不多,于是就认真去研究了下这个算法,在此处做个记录吧。

问题描述:

给定两个字符串m和n,只允许进行如下三种操作:

  1. 插入,例如:ab -> abc
  2. 删除,例如:abc -> ab
  3. 替换,例如:abc -> abd

那么请求出将m变成n的最小操作次数(也就是最小编辑距离)

求解这个问题,一般有两种思路:递归和动态规划。

递归:

首先假设字符串m有j位,字符串n有k位,此时我们将m -> n的最小编辑距离记为d[j][k],此时我们能总结出如下的规律:

  1. 当m[j] === n[k](m[j]和n[k]为字符串的最后一位)时,例如:asd -> jkl时候,很明显此时最后一位是不需要做变动的,因此最小编辑距离等同于:as -> jk,那么我们可以确定d[j][k] === d[j - 1][k - 1];
  2. 当 m[j] !== n[k] 时,字符串asd到字符串jkl的d[j][k] 又可以分为如下三种情况:
    • asd -> jkl的最小编辑距离 = as -> jkl的最小编辑距离+1(加一个插入d的操作),对此可以描述为:d[j][k] === d[j - 1][k] + 1;
    • asd -> jkl的最小编辑距离 = asdl -> jkl的最小编辑距离 + 1(加一个删除l的操作),此时可以看到asdl与jkl的最后一位相等,因此可以再次简化为:asd -> jk的最小编辑距离 + 1,对此可以描述为:d[j][k] = d[j][k - 1] + 1;
    • asd -> jkl的最小编辑距离 = asl -> jkl的最下编辑距离 + 1(加上一个将d替换成l的操作),此时asl与jkl的最后一位再次相等,因此同样可以进行简化为:as -> jk的最下编辑距离 + 1,对此可以描述为:d[j][k] = d[j - 1][k - 1] + 1;
  3. 如果m的长度为 0,那么 m -> n 的最小编辑距离为n的长度;反过来,如果n的长度为 0,那么 m -> n 的最小编辑距离为m的长度(全部执行删除操作),可以描述为:d[j][0] = j,d[0][k] = k;

那么我们可以开始按照上述思路写代码了:

/**
 * 递归算法
 * @param {string} m
 * @param {string} n
 * @param {number} j 字符串m的长度
 * @param {number} k 字符串n的长度
 * @returns {number} 从 m -> n 的最小编辑距离
 */
function editDistance(m, n, j, k) {
    // 触碰到边界条件
    if (k === 0) {
        return j;
    } else if (j === 0) {
        return k;
    } else if (m[j - 1] === n[k - 1]) {
        // 当最后一位相等的时候
        return editDistance(m, n, j - 1, k - 1);
    } else {
        // 当最后一位不相等的时候,取最小值
        const d1 = editDistance(m, n, j - 1, k) + 1;
        const d2 = editDistance(m, n, j, k - 1) + 1;
        const d3 = editDistance(m, n, j - 1, k - 1) + 1;

        return Math.min(d1, d2, d3);
    }
}

这个代码虽然能实现,但是有个严重的问题,就是代码的性能很低下,时间复杂度是指数增长的,因此可以考虑另外一种实现。

动态规划

动态规划看起来跟递归很像,不过推理逻辑正好是反过来的。递归的逻辑是:“要求得 d[j][k],先要求得 d[j-1][k-1]……”,动态规划的逻辑是:“先求得 d[j-1][k-1],再求 d[j][k]……”这是它们的主要区别。

同样先举个例子,有两个字符串分别是m = ‘asdfgh’ 和 n = ‘zscv’,我们一步步的来进行如下处理:

1:首先将我们的两个字符串放入如下的矩阵中去:

0asdfgh
0
z
s
c
v

2: 此时我们可以将d[0][0]、d[0][1] … d[0][4]等等的最小编辑距离填入此矩阵:

0asdfgh
00123456
z1
s2
c3
v4

3: 这个时候我们可以去计算d[1][1]了,上面在讲述递归的方法的时候我们已经说过计算d[j][k]的时候,当m[j] !== n[k]时会有三种方法,此时 d[0][1] + 1 = 2、 d[1][0] + 1 = 2、d[0][0] + 1 = 1,因此可以得知d[1][1]的最小编辑距离就是1,然后这一行后面的我们都可以直接进行插入操作一次递增即可,由此得出下面的矩阵:

0asdfgh
00123456
z1123456
s2
c3
v4

4:这个时候我们开始去计算d[2][2],首先我们可以按照之前的方式计算出d[1][2] = 2,填入矩阵,这个时候再看d[2][2],此时我们可以发现矩阵正好满足条件m[j] === n[k],因此此时d[j][k] === d[j - 1][k - 1] = 1,填入矩阵如下:

0asdfgh
00123456
z1123456
s2212345
c3
v4

5:不断重复上述步骤直到完成矩阵:

0asdfgh
00123456
z1123456
s2212345
c3332345
v4444345

此时我们自然可以看到d[j][k] = 5;

按照思路写下代码:

/**
 * 动态规划算法
 * @param {string} m
 * @param {string} n
 * @return {number} 从 m → n 的最小编辑距离
 */
function dynamicPlanning(m, n) {
    const lenM = m.length;
    const lenN = n.length;
    const d = [];

    for (let i = 0; i <= lenM; i++) {
        d[i] = [];
        d[i][0] = i;
    }

    for (let j = 0; j <= lenN; j++) {
        d[0].push(j);
    }

    for (let i = 1; i <= lenM; i++) {
        for (let j = 1; j <= lenN; j++) {
            if (m[i - 1] === n[j - 1]) {
                d[i][j] = d[i - 1][j - 1];
            } else {
                const d1 = d[i - 1][j] + 1;
                const d2 = d[i][j - 1] + 1;
                const d3 = d[i - 1][j - 1] + 1;

                d[i][j] = Math.min(d1, d2, d3);
            }
        }
    }

    return d[lenM][lenN];
}

这次的算法复杂度就为线性了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值