LeetCode 热题 HOT 100 Java题解——72. 编辑距离

72. 编辑距离

题目:

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

  1. 插入一个字符
  2. 删除一个字符
  3. 替换一个字符

示例:

输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

自顶向下递归

我们使用两个指针 i , j i,j i,j倒着向前看,如果:

  1. w o r d 1 [ i ] = = w o r d 2 [ j ] word1[i] == word2[j] word1[i]==word2[j],说明最末尾的字符相同,可以同时去掉不用管了,继续关注前面的字符。
  2. w o r d 1 [ i ] ! = w o r d 2 [ j ] word1[i] != word2[j] word1[i]!=word2[j],当前末尾的字符不相同,有三种处理办法:
    1. 替换:把当前的 w o r d 1 [ i ] word1[i] word1[i]替换为 w o r d 2 [ j ] word2[j] word2[j],那么也可以同时去掉这两个字符,继续向前看,但是操作数需要 + 1 +1 +1
    2. 删除:把当前的 w o r d 1 [ i ] word1[i] word1[i]删掉,那么 w o r d 1 word1 word1就需要继续向前看一位,而 w o r d 2 word2 word2不变。
    3. 插入: w o r d 1 word1 word1插入一位字符,该字符与 w o r d 2 [ j ] word2[j] word2[j]相同,因此它俩可以同时去掉,向前看,那么就等同于 w o r d 1 word1 word1不变, w o r d 2 word2 word2向前看一位。

上述所有的向前看操作都可以使用字符串子串进行操作,但是在java中如此做会占用大量的空间,因此我们使用将 S t r i n g String String转换为 c h a r [ ] char[] char[]的方法,并携带一个 x x x y y y用来标记两个数组的末尾,节省空间。

class Solution {
    public int recur(char[] w1, int x, char[] w2, int y) {
        if (x == -1 || y == -1) return Math.max(x, y) + 1;
        if (w1[x] == w2[y]) return recur(w1, x - 1, w2, y - 1);
        else return Math.min(Math.min(
                recur(w1, x - 1, w2, y - 1),    //替换
                recur(w1, x - 1, w2, y)),   //删除
                recur(w1, x, w2, y - 1)) + 1;   //插入
    }
    public int minDistance(String word1, String word2) {
        char[] w1 = word1.toCharArray(), w2 = word2.toCharArray();
        return recur(w1, word1.length() - 1, w2, word2.length() - 1);
    }
}

这个代码思路非常简单,但是解决这道困难题目意料之中的超时了,仔细分析,这里面包含着当量的重复递归计算,一个最简单的想法就是使用记忆化递归,建立一个记忆数组用于存放计算过的操作数,一旦发现当前的情况已经计算过,那么就直接返回数组中的结果即可。

image.png

public class Solution {
    int[][] mem;
    public int recur(char[] w1, int x, char[] w2, int y) {
        if (x == -1 || y == -1) return Math.max(x, y) + 1;
        if (w1[x] == w2[y]) return recur(w1, x - 1, w2, y - 1);
        else {
            if (mem[x][y] != 0) return mem[x][y];
            int res = Math.min(Math.min(
                    recur(w1, x - 1, w2, y - 1),    //替换
                    recur(w1, x - 1, w2, y)),   //删除
                    recur(w1, x, w2, y - 1)) + 1;   //插入
            mem[x][y] = res;
            return res;
        }
    }
    public int minDistance(String word1, String word2) {
        char[] w1 = word1.toCharArray(), w2 = word2.toCharArray();
        mem = new int[w1.length][w2.length];
        return recur(w1, word1.length() - 1, w2, word2.length() - 1);
    }
}

image.png

可以看到,记忆化递归已经达到了很好的结果。

复杂度分析
  • 时间复杂度: O ( m × n ) O(m \times n) O(m×n)

    递归相当于还是将 m ∗ n m * n mn的数组遍历了一遍,因此复杂度为 O ( m × n ) O(m \times n) O(m×n)

  • 空间复杂度: O ( m × n ) O(m \times n) O(m×n)

    使用 O ( m × n ) O(m \times n) O(m×n)的额外记忆数组空间。

自底向上动态规划

仔细思索一下递归的过程,自顶向下分解成子问题之后其实就是在填写 m e m mem mem的表格,那么使用动态规划解决子问题就可以自底向上解决问题了, d p [ i ] [ j ] dp[i][j] dp[i][j]代表的是 w o r d 1 word1 word1 i i i个字符变换到 w o r d 2 word2 word2 j j j个字符所需要的最少操作数:

转移方程其实与递归的思想相同:

  1. w o r d 1 [ i ] = = w o r d 2 [ j ] word1[i] == word2[j] word1[i]==word2[j],那么 d p [ i ] [ j ] = = d p [ i − 1 ] [ j − 1 ] dp[i][j] == dp[i - 1][j - 1] dp[i][j]==dp[i1][j1]
  2. w o r d 1 [ i ] ! = w o r d 2 [ j ] word1[i] != word2[j] word1[i]!=word2[j]时,以下三种情况取最小:
    1. 替换: d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + 1 dp[i][j] = dp[i - 1][j - 1] + 1 dp[i][j]=dp[i1][j1]+1
    2. 删除: d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + 1 dp[i][j] = dp[i - 1][j] + 1 dp[i][j]=dp[i1][j]+1
    3. 插入: d p [ i ] [ j ] = d p [ i ] [ j − 1 ] + 1 dp[i][j] = dp[i][j - 1] + 1 dp[i][j]=dp[i][j1]+1
class Solution {
    public int minDistance(String word1, String word2) {
        char[] w1 = word1.toCharArray(), w2 = word2.toCharArray();
        int[][] dp = new int[w1.length + 1][w2.length + 1];
        for (int i = 0; i < dp.length; i++) {
            dp[i][0] = i;
        }
        for (int i = 0; i < dp[0].length; i++) {
            dp[0][i] = i;
        }
        for (int i = 1; i < dp.length; i++) {
            for (int j = 1; j < dp[0].length; j++) {
                if (w1[i - 1] == w2[j - 1]) dp[i][j] = dp[i - 1][j - 1];
                else dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1],dp[i - 1][j]), dp[i][j - 1]) + 1;
            }
        }
        return dp[w1.length][w2.length];
    }
}

image.png

时间与记忆化递归相同。

复杂度分析
  • 时间复杂度: O ( m × n ) O(m \times n) O(m×n)

    填写 m ∗ n m * n mn的数组,因此复杂度为 O ( m × n ) O(m \times n) O(m×n)

  • 空间复杂度: O ( m × n ) O(m \times n) O(m×n)

    使用 O ( m × n ) O(m \times n) O(m×n)的数组空间。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值