最小编辑距离问题

来源:互联网 发布:linux压缩和解压命令 编辑:程序博客网 时间:2024/05/21 14:07

最近在看一些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:首先将我们的两个字符串放入如下的矩阵中去:

空 0 a s d f g h 0 z s c v

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

空 0 a s d f g h 0 0 1 2 3 4 5 6 z 1 s 2 c 3 v 4

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,然后这一行后面的我们都可以直接进行插入操作一次递增即可,由此得出下面的矩阵:

空 0 a s d f g h 0 0 1 2 3 4 5 6 z 1 1 2 3 4 5 6 s 2 c 3 v 4

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

空 0 a s d f g h 0 0 1 2 3 4 5 6 z 1 1 2 3 4 5 6 s 2 2 1 2 3 4 5 c 3 v 4

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

空 0 a s d f g h 0 0 1 2 3 4 5 6 z 1 1 2 3 4 5 6 s 2 2 1 2 3 4 5 c 3 3 3 2 3 4 5 v 4 4 4 4 3 4 5

此时我们自然可以看到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];}

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

原创粉丝点击