【LeetCode热题100】打卡第22天:编辑距离&颜色分类

文章目录

  • 【LeetCode热题100】打卡第22天:编辑距离&颜色分类
    • ⛅前言
  • 编辑距离
    • 题目
    • 题解
  • 颜色分类
    • 题目
    • 题解

【LeetCode热题100】打卡第22天:编辑距离&颜色分类

⛅前言

大家好,我是知识汲取者,欢迎来到我的LeetCode热题100刷题专栏!

精选 100 道力扣(LeetCode)上最热门的题目,适合初识算法与数据结构的新手和想要在短时间内高效提升的人,熟练掌握这 100 道题,你就已经具备了在代码世界通行的基本能力。在此专栏中,我们将会涵盖各种类型的算法题目,包括但不限于数组、链表、树、字典树、图、排序、搜索、动态规划等等,并会提供详细的解题思路以及Java代码实现。如果你也想刷题,不断提升自己,就请加入我们吧!QQ群号:827302436。我们共同监督打卡,一起学习,一起进步。

LeetCode热题100专栏:LeetCode热题100

Gitee地址:知识汲取者 (aghp) - Gitee.com

题目来源:LeetCode 热题 100 - 学习计划 - 力扣(LeetCode)全球极客挚爱的技术成长平台

PS:作者水平有限,如有错误或描述不当的地方,恳请及时告诉作者,作者将不胜感激

编辑距离

题目

原题链接:72.编辑距离(二刷了(●ˇ∀ˇ●))

【LeetCode热题100】打卡第22天:编辑距离&颜色分类_第1张图片

题解

  • 解法一:暴力DFS(时间超限)

    直接暴力DFS相当于是进行了三层for循环,枚举出每一种操作的组合,时间复杂度相当高

    /**
     * @author ghp
     * @title
     */
    
    class Solution {
    
        public int minDistance(String word1, String word2) {
            return dfs(word1, word2, 0, 0);
        }
    
        public int dfs(String word1, String word2, int i, int j) {
            if (i == word1.length()) {
                // word1已经遍历完了
                return word2.length() - j;
            }
            if (j == word2.length()) {
                // word2已经遍历完了
                return word1.length() - i;
            }
            int res = 0;
            if (word1.charAt(i) == word2.charAt(j)) {
                // 当前两个字符相同,比较下一个字符
                res = dfs(word1, word2, i + 1, j + 1);
            } else {
                // 删除word1[i],相当于在word2的j位置插入word1[i]
                int r1 = dfs(word1, word2, i + 1, j);
                // 替换word1[i]为word2[j],相当于替换word2[j]为word1[i],相当于同时删除word1[i]和word2[j]
                int r2 = dfs(word1, word2, i + 1, j + 1);
                // 删除word2[j],相当于在word1的i位置插入word2[j]
                int r3 = dfs(word1, word2, i, j + 1);
                // 获取本次最小操作的次数
                res = 1 + Math.min(r1, Math.min(r2, r3));
            }
            return res;
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( 3 m + n ) O(3^{m+n}) O(3m+n)
    • 空间复杂度: O ( m + n ) O(m+n) O(m+n)

    其中 n n n 为word1的长度, m m m为word2的长度

    代码优化:DFS+记忆搜搜

    直接使用DFS一般都是会超时,所以我们需要使用记忆搜索对搜索树进行剪枝,这样就能加快搜索效率,节约搜索时间

    import java.util.Arrays;
    
    /**
     * @author ghp
     * @title
     */
    
    class Solution {
    
        private int[][] memo;
    
        public int minDistance(String word1, String word2) {
            memo = new int[word1.length()][word2.length()];
            for (int i = 0; i < memo.length; i++) {
                Arrays.fill(memo[i], -1);
            }
            return dfs(word1, word2, 0, 0);
        }
    
        public int dfs(String word1, String word2, int i, int j) {
            if (i == word1.length()) {
                // word1已经遍历完了
                return word2.length() - j;
            }
            if (j == word2.length()) {
                // word2已经遍历完了
                return word1.length() - i;
            }
            if (memo[i][j] != -1) {
                // 当前路径已被搜索
                return memo[i][j];
            }
            int res = 0;
            if (word1.charAt(i) == word2.charAt(j)) {
                // 当前两个字符相同,比较下一个字符
                res = dfs(word1, word2, i + 1, j + 1);
            } else {
                // 删除word1[i],相当于在word2的j位置插入word1[i]
                int r1 = dfs(word1, word2, i + 1, j);
                // 替换word1[i]为word2[j],相当于替换word2[j]为word1[i],相当于同时删除word1[i]和word2[j]
                int r2 = dfs(word1, word2, i + 1, j + 1);
                // 删除word2[j],相当于在word1的i位置插入word2[j]
                int r3 = dfs(word1, word2, i, j + 1);
                // 获取本次最小操作的次数
                res = 1 + Math.min(r1, Math.min(r2, r3));
            }
            memo[i][j] = res;
            return res;
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( m ∗ n ) O(m*n) O(mn)
    • 空间复杂度: O ( m ∗ n ) O(m*n) O(mn)

    其中 n n n 为word1的长度, m m m为word2的长度

  • 解法二:动态规划

    可能时间复杂度到 O ( n ∗ m ) O(n*m) O(nm),已经是极限了,但是由于DFS需要递归,而每次递归都需要占用大量的栈内存,所以这里我们可以使用迭代替代递归,节约递归所消耗的栈内存,所以这题毫无疑问最优解就是动态规划,其实动规做得多的,一看这题就知道这是用过经典的动态规划问题,这一点从官方题解也可以看出,LeetCode官方也只提供了动态规划的题解。

    但需要注意的是,并不是所有的题目,动态规划要优于DFS+记忆搜索,在非极值问题上,就不一定,比如在子问题数量超多,而DFS可以进行高效剪枝的情况下,DFS+memo的效率会优于DP算法,比如这道题 【403.青蛙过河】

    状态转移方程最难的就是状态转移方程以及DP的定义,这里大致给出一个思路:

    • Step1:定义DP

      dp[i][j]表示word1中前i给字符,变换成word2中前j个字符,最短需要的操作数。由于wold1或world2中可能存在一个字母都没有的情况,即全增/删的情况,所以需要预留dp[0][j]dp[i][0],方便进行状态转移

    • Step2:构造状态转移方程

      第一种情况:如果word1[i](word1的第i个单词)和word2[j])(word2的第j个单词)相同,则可以直接比较下一个,当前状态没有发生改变,也就是说当前的状态就是word1[i-1]和word2[j-1]的状态,所以状态方程是 d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] dp[i][j] = dp[i - 1][j - 1] dp[i][j]=dp[i1][j1]

      第二种情况:如果word[i]不等于word2[j],则需要进行三种操作,增、删、改,但是我们只需要选取三种操作中操作次数最小的一种即可。其中:

      增: d p [ i ] [ j ] = d p [ i ] [ j − 1 ] + 1 dp[i][j]=dp[i][j-1]+1 dp[i][j]=dp[i][j1]+1dp[i][j-1]表示word1前i个字母于word2前j+1个字母进行匹配的最小操作数,相当于是在word2的第 j 个单词前添加一个word1[i],是word[i]与word[j]匹配,然后当前操作数需要+1

      删: 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,和增同理

      改: 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,和增同理

      当然,每次状态转移,我们都需要选取当前操作次数最小的一种,也就是有: d p [ i ] [ j ] = 1 + M a t h . m i n ( d p [ i − 1 ] [ j ] , M a t h . m i n ( d p [ i ] [ j − 1 ] , d p [ i − 1 ] [ j − 1 ] ) ) dp[i][j] = 1 + Math.min(dp[i - 1][j], Math.min(dp[i][j - 1], dp[i - 1][j - 1])) dp[i][j]=1+Math.min(dp[i1][j],Math.min(dp[i][j1],dp[i1][j1]))

    初始化DP(这里word1是horse,word2是ros):

    【LeetCode热题100】打卡第22天:编辑距离&颜色分类_第2张图片

    经过状态转移后的DP:

    【LeetCode热题100】打卡第22天:编辑距离&颜色分类_第3张图片

    /**
     * @author ghp
     * @title
     */
    
    class Solution {
    
        public int minDistance(String word1, String word2) {
            int m = word1.length(), n = word2.length();
            int[][] dp = new int[m + 1][n + 1];
            // 初始化DP
            for (int i = 1; i <= m; i++) {
                dp[i][0] = i;
            }
            for (int j = 1; j <= n; j++) {
                dp[0][j] = j;
            }
            for (int i = 1; i <= m; i++) {
                for (int j = 1; j <= n; j++) {
                    if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                        dp[i][j] = dp[i - 1][j - 1];
                    } else {
                        dp[i][j] = 1 + Math.min(dp[i - 1][j], Math.min(dp[i][j - 1], dp[i - 1][j - 1]));
                    }
                }
            }
            return dp[m][n];
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n ∗ m ) O(n*m) O(nm)
    • 空间复杂度: O ( n ∗ m ) O(n*m) O(nm)

    其中 n n n 为word1的长度, m m m为word2的长度

    代码优化:将二维DP压缩成一维DP

    最后,我们还可以进一步优化空间,因为dp[i][j]的值只与它的邻居dp[i-1][j]dp[i][j-1]dp[i-1][j-1]有关,将二维数组压缩为一维数组,可以天然解决前两个依赖,问题就在于dp[i-1][j-1]的值如何保存,很显然,可以将这一维的数据压缩成一个值,于是,我们可以使用一个一维数组加一个变量来替换原来的二维数组

    /**
     * @author ghp
     * @title
     */
    
    class Solution {
    
        public int minDistance(String word1, String word2) {
            int m = word1.length(), n = word2.length();
            int[] dp = new int[n + 1];
            for (int i = 1; i <= n; i++) {
                dp[i] = i;
            }
            for (int i = 1; i <= m; i++) {
                int pre = dp[0];
                dp[0] = i;
                for (int j = 1; j <= n; j++) {
                    int temp = dp[j];
                    if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                        dp[j] = pre;
                    } else {
                        dp[j] = 1 + Math.min(dp[j-1], Math.min(dp[j], pre));
                    }
                    pre = temp;
                }
            }
            return dp[n];
        }
    }
    

最后我们经过 D F S → D F S + 记忆搜索 → 二维 D P → 一维 D P DFS→DFS+记忆搜索→二维DP→一维DP DFSDFS+记忆搜索二维DP一维DP 的层层优化,最终得到本题的最优解,也就是一维DP,但需要注意并不是说所有的可以使用动规和DFS的题,就一定是DP优于DFS,有些情况,题目的子问题过多可能就是DFS优于DP

PS:说句实话,我感觉DP还更加好理解一点,DFS+记忆搜索反而绕的有点晕,而且这里DP的效率是要高于DFS+记忆搜索的

颜色分类

题目

原题链接:75.颜色分类

【LeetCode热题100】打卡第22天:编辑距离&颜色分类_第4张图片

题解

  • 解法一:调用API

    Arrays.sort是Java中用于排序的静态方法,它的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。具体来说,它使用的是快速排序或Tim排序(Java 7及以上版本)。在空间复杂度方面,Arrays.sort属于“原地排序”,也就是说,它只使用了常数级别的额外空间,因此空间复杂度为 O ( 1 ) O(1) O(1)。需要注意的是,对于基本类型数组,Arrays.sort使用的是“双轴快速排序”,而对于对象数组,Arrays.sort使用的是“归并排序”。这可能会影响排序的性能和稳定性。同时,如果要对对象数组进行排序,并且排序字段可能有重复的值,那么建议使用Java 8及以上版本的Streams API中的sorted方法,它提供了更好的排序性能和稳定性。

    import java.util.Arrays;
    
    /**
     * @author ghp
     * @title
     */
    class Solution {
        public void sortColors(int[] nums) {
            Arrays.sort(nums);
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
    • 空间复杂度: O ( 1 ) O(1) O(1)

    其中 n n n 为数组中元素的个数

  • 解法二:快排

    解法一的Arrays.sort底层也是快排算法,这里就手写一遍快排,就当作重新复习一遍吧O(∩_∩)O

    /**
     * @author ghp
     * @title
     */
    class Solution {
        public void sortColors(int[] nums) {
            quickSort(nums, 0, nums.length - 1);
        }
    
        private void quickSort(int[] nums, int l, int r) {
            if (l >= r) {
                return;
            }
            // 划分区间,同时获取主元索引
            int pivot = partition(nums, l, r);
            quickSort(nums, l, pivot - 1);
            quickSort(nums, pivot + 1, r);
        }
    
        private int partition(int[] nums, int l, int r) {
            int pivot = nums[r];
            int i = l - 1;
            int j = l;
            int temp;
            // 划分区间(左侧区间元素<主元,右侧区间元素>=主元)
            while (j < r) {
                if (nums[j] < pivot) {
                    temp = nums[j];
                    nums[j] = nums[i + 1];
                    nums[i + 1] = temp;
                    i++;
                }
                j++;
            }
            // 将主元放到分界点
            temp = nums[r];
            nums[r] = nums[i + 1];
            nums[i + 1] = temp;
            return i + 1;
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
    • 空间复杂度: O ( 1 ) O(1) O(1)

    其中 n n n 为数组中元素的个数

  • 解法三:三路快排算法

    上面的代码使用的是荷兰国旗问题的算法,也叫三路快排算法。该算法的思想源于快速排序,可以在 O ( n ) O(n) O(n)的时间复杂度内将一个数组分为三部分:小于某个数、等于某个数和大于某个数。

    这应该是本体的最优解了!这方法实现起来也简单,也容易懂,就是很难想得到

    /**
     * @author ghp
     * @title
     */
    class Solution {
        public void sortColors(int[] nums) {
            int i = 0;
            int j = 0;
            for (int k = 0; k < nums.length; k++) {
                int num = nums[k];
                nums[k] = 2;
                if (num < 2) {
                    nums[j++] = 1;
                }
                if (num < 1) {
                    nums[i++] = 0;
                }
            }
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n ) O(n) O(n)
    • 空间复杂度: O ( 1 ) O(1) O(1)

    其中 n n n 为数组中元素的个数

  • 解法四:三指针算法

    /**
     * @author ghp
     * @title
     */
    class Solution {
        public void sortColors(int[] nums) {
            int i = 0;
            int j = 0;
            int k = nums.length - 1;
            while (i <= k) {
                if (nums[i] == 0) {
                    swap(nums, i++, j++);
                } else if (nums[i] == 2) {
                    swap(nums, i, k--);
                } else {
                    i++;
                }
            }
        }
    
        private void swap(int[] nums, int i, int j) {
            int t = nums[i];
            nums[i] = nums[j];
            nums[j] = t;
        }
    }
    
  • 解法五:归并排序

    这种解法也能过,但是不符合题意,因为题目要求要原地排序,不能使用第三方数组。我写在这里,单纯是为了复习一遍归并排序算法

    import java.util.Arrays;
    
    /**
     * @author ghp
     * @title
     */
    
    class Solution {
        public void sortColors(int[] nums) {
            divide(nums, 0, nums.length - 1);
            System.out.println(Arrays.toString(nums));
        }
    
        private void divide(int[] nums, int l, int r) {
            if (l >= r) {
                return;
            }
            int mid = (r - l) / 2 + l;
            divide(nums, l, mid);
            divide(nums, mid + 1, r);
            merge(nums, l, mid, r);
        }
    
        private void merge(int[] nums, int l, int mid, int r) {
            int i = l;
            int j = mid + 1;
            int k = 0;
            int[] temp = new int[r - l + 1];
            while (i <= mid && j <= r) {
                if (nums[i] < nums[j]) {
                    temp[k++] = nums[i++];
                } else {
                    temp[k++] = nums[j++];
                }
            }
            while (i <= mid) {
                temp[k++] = nums[i++];
            }
            while (j<=r){
                temp[k++] = nums[j++];
            }
            for (int m = 0; m < k; m++) {
                nums[l+m] = temp[m];
            }
        }
    }
    

参考题解

  • 极值求解:从Brute Force到1维DP - 编辑距离 - 力扣(LeetCode)
  • 最简洁的双指针解法!一次遍历,无交换操作 - 颜色分类 - 力扣(LeetCode)
  • 三指针解法,清晰易懂 - 颜色分类 - 力扣(LeetCode)

你可能感兴趣的:(#,LeetCode热题100,Programming,practice,leetcode,分类,算法)