From 7008873792321d11abddf49d17e3dafd00dfdfe7 Mon Sep 17 00:00:00 2001 From: labuladong Date: Sun, 5 Mar 2023 11:37:16 +0800 Subject: [PATCH] update content --- README.md | 1 - ...32\345\274\210\351\227\256\351\242\230.md" | 4 + ...33\351\224\256\351\224\256\347\233\230.md" | 4 + ...43\345\210\231\350\241\250\350\276\276.md" | 2 + ...36\345\255\220\345\272\217\345\210\227.md" | 4 + ...46\350\247\243\350\277\233\351\230\266.md" | 66 ++- ...25\350\257\215\346\213\274\346\216\245.md" | 366 +++++++------- ...41\347\245\250\351\227\256\351\242\230.md" | 11 + .../\346\212\242\346\210\277\345\255\220.md" | 1 + ...30\345\255\220\347\273\223\346\236\204.md" | 6 + ...13\347\274\251\346\212\200\345\267\247.md" | 6 +- ...26\350\276\221\350\267\235\347\246\273.md" | 68 +-- ...03\345\272\246\351\227\256\351\242\230.md" | 6 + .../\351\255\224\345\241\224.md" | 79 +-- ...67\351\242\230\346\212\200\345\267\247.md" | 6 + .../BST1.md" | 7 + .../BST2.md" | 9 + .../dijkstra\347\256\227\346\263\225.md" | 24 + ...10\347\272\247\351\230\237\345\210\227.md" | 94 ++-- ...11\346\240\221\346\200\273\347\273\223.md" | 208 ++++---- ...1\346\240\221\347\263\273\345\210\2271.md" | 10 + ...1\346\240\221\347\263\273\345\210\2272.md" | 18 + ...25\350\260\203\351\230\237\345\210\227.md" | 29 +- .../\345\233\276.md" | 69 +-- ...60\350\256\241\347\256\227\345\231\250.md" | 5 + ...23\346\211\221\346\216\222\345\272\217.md" | 187 +++---- .../\350\256\276\350\256\241Twitter.md" | 61 ++- ...04\344\270\200\351\203\250\345\210\206.md" | 7 + ...36\347\216\260\351\230\237\345\210\227.md" | 119 +++-- .../BFS\346\241\206\346\236\266.md" | 13 + ...27\346\263\225\350\257\246\350\247\243.md" | 159 +++--- ...45\346\211\276\350\257\246\350\247\243.md" | 11 + ...00\345\222\214\346\212\200\345\267\247.md" | 4 + ...07\351\222\210\346\212\200\345\267\247.md" | 15 + ...43\344\277\256\350\256\242\347\211\210.md" | 146 +++--- ...30\346\225\210\346\226\271\346\263\225.md" | 12 + ...56\345\210\206\346\212\200\345\267\247.md" | 10 + ...00\345\267\247\350\277\233\351\230\266.md" | 5 + ...61\345\274\217\351\201\215\345\216\206.md" | 7 + .../LRU\347\256\227\346\263\225.md" | 121 +++-- ...15\350\275\254\351\223\276\350\241\250.md" | 3 + ...14\345\210\206\350\277\220\347\224\250.md" | 2 + ...15\344\272\272\351\227\256\351\242\230.md" | 6 + ...22\345\210\227\347\273\204\345\220\210.md" | 468 ++++++++++-------- ...22\344\274\232\350\256\256\345\256\244.md" | 1 + ...17\346\234\272\346\235\203\351\207\215.md" | 3 + 46 files changed, 1500 insertions(+), 963 deletions(-) diff --git a/README.md b/README.md index d0d5135..5df75e7 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,6 @@ Gitee Pages 地址:https://labuladong.gitee.io/algo/ * [番外:用算法打败算法](https://labuladong.github.io/article/fname.html?fname=PDF中的算法) * [数据结构精品课](https://labuladong.github.io/article/fname.html?fname=ds课程简介) * [二叉树(递归)专题课](https://labuladong.github.io/article/fname.html?fname=tree课程简介) - * [14 天刷题打卡挑战](https://labuladong.github.io/article/fname.html?fname=打卡挑战简介) * [学习本站所需的 Java 基础](https://labuladong.github.io/article/fname.html?fname=网站Java基础) ### [第零章、核心框架汇总](https://labuladong.github.io/algo/) diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\215\232\345\274\210\351\227\256\351\242\230.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\215\232\345\274\210\351\227\256\351\242\230.md" index 83adf5c..1100606 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\215\232\345\274\210\351\227\256\351\242\230.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\215\232\345\274\210\351\227\256\351\242\230.md" @@ -40,12 +40,14 @@ 函数签名如下: + ```java boolean PredictTheWinner(int[] nums); ``` 那么如果有了一个计算先手和后手分差的 `stoneGame` 函数,这道题的解法就直接出来了: + ```java public boolean PredictTheWinner(int[] nums) { // 先手的分数大于等于后手,则能赢 @@ -163,6 +165,7 @@ for (int i = n - 2; i >= 0; i--) { 如何实现这个 fir 和 sec 元组呢,你可以用 python,自带元组类型;或者使用 C++ 的 pair 容器;或者用一个三维数组 `dp[n][n][2]`,最后一个维度就相当于元组;或者我们自己写一个 Pair 类: + ```java class Pair { int fir, sec; @@ -175,6 +178,7 @@ class Pair { 然后直接把我们的状态转移方程翻译成代码即可,注意我们要倒着遍历数组: + ```java /* 返回游戏最后先手和后手的得分之差 */ int stoneGame(int[] piles) { diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\233\233\351\224\256\351\224\256\347\233\230.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\233\233\351\224\256\351\224\256\347\233\230.md" index fe1607b..49345a2 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\233\233\351\224\256\351\224\256\347\233\230.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\345\233\233\351\224\256\351\224\256\347\233\230.md" @@ -39,6 +39,7 @@ 函数签名如下: + ```java int maxA(int N); ``` @@ -84,6 +85,7 @@ dp(n - 2, a_num, a_num) # C-A C-C 这样可以看到问题的规模 `n` 在不断减小,肯定可以到达 `n = 0` 的 base case,所以这个思路是正确的: + ```python def maxA(N: int) -> int: @@ -105,6 +107,7 @@ def maxA(N: int) -> int: 这个解法应该很好理解,因为语义明确。下面就继续走流程,用备忘录消除一下重叠子问题: + ```python def maxA(N: int) -> int: # 备忘录 @@ -170,6 +173,7 @@ dp[i] = dp[i - 1] + 1; **刚才说了,最优的操作序列一定是 `C-A C-C` 接着若干 `C-V`,所以我们用一个变量 `j` 作为若干 `C-V` 的起点**。那么 `j` 之前的 2 个操作就应该是 `C-A C-C` 了: + ```java public int maxA(int N) { int[] dp = new int[N + 1]; diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\346\255\243\345\210\231\350\241\250\350\276\276.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\346\255\243\345\210\231\350\241\250\350\276\276.md" index 700c790..15435ed 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\346\255\243\345\210\231\350\241\250\350\276\276.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213\346\255\243\345\210\231\350\241\250\350\276\276.md" @@ -32,6 +32,7 @@ 函数签名如下: + ```cpp bool isMatch(string s, string p); ``` @@ -48,6 +49,7 @@ bool isMatch(string s, string p); **如果不考虑 `*` 通配符,面对两个待匹配字符 `s[i]` 和 `p[j]`,我们唯一能做的就是看他俩是否匹配**: + ```cpp bool isMatch(string s, string p) { int i = 0, j = 0; diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\256\276\350\256\241\357\274\232\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\256\276\350\256\241\357\274\232\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227.md" index b620e0f..9eb04b1 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\256\276\350\256\241\357\274\232\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\256\276\350\256\241\357\274\232\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227.md" @@ -32,6 +32,7 @@ 输入一个无序的整数数组,请你找到其中最长的严格递增子序列的长度,函数签名如下: + ```java int lengthOfLIS(int[] nums); ``` @@ -121,6 +122,7 @@ for (int i = 0; i < nums.length; i++) { 结合我们刚才说的 base case,下面我们看一下完整代码: + ```java int lengthOfLIS(int[] nums) { // 定义:dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度 @@ -184,6 +186,7 @@ int lengthOfLIS(int[] nums) { > tip:前文 [二分查找算法详解](https://labuladong.github.io/article/fname.html?fname=二分查找详解) 详细介绍了二分查找的细节及变体,这里就完美应用上了,如果没读过强烈建议阅读。 + ```java int lengthOfLIS(int[] nums) { int[] top = new int[nums.length]; @@ -257,6 +260,7 @@ int lengthOfLIS(int[] nums) { 下面看解法代码: + ```java // envelopes = [[w, h], [w, h]...] public int maxEnvelopes(int[][] envelopes) { diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266.md" index 18c0df4..fcef58a 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\350\257\246\350\247\243\350\277\233\351\230\266.md" @@ -77,6 +77,7 @@ for 状态1 in 状态1的所有取值: 斐波那契数列的数学形式就是递归的,写成代码就是这样: + ```java int fib(int N) { if (N == 1 || N == 2) return 1; @@ -110,20 +111,22 @@ int fib(int N) { 一般使用一个数组充当这个「备忘录」,当然你也可以使用哈希表(字典),思想都是一样的。 + ```java int fib(int N) { // 备忘录全初始化为 0 int[] memo = new int[N + 1]; // 进行带备忘录的递归 - return helper(memo, N); + return dp(memo, N); } -int helper(int[] memo, int n) { +// 带着备忘录进行递归 +int dp(int[] memo, int n) { // base case if (n == 0 || n == 1) return n; // 已经计算过,不用再计算了 if (memo[n] != 0) return memo[n]; - memo[n] = helper(memo, n - 1) + helper(memo, n - 2); + memo[n] = dp(memo, n - 1) + dp(memo, n - 2); return memo[n]; } ``` @@ -154,6 +157,7 @@ int helper(int[] memo, int n) { 有了上一步「备忘录」的启发,我们可以把这个「备忘录」独立出来成为一张表,通常叫做 DP table,在这张表上完成「自底向上」的推算岂不美哉! + ```java int fib(int N) { if (N == 0) return 0; @@ -195,6 +199,7 @@ int fib(int N) { 所以,可以进一步优化,把空间复杂度降为 O(1)。这也就是我们最常见的计算斐波那契数的算法: + ```java int fib(int n) { if (n == 0 || n == 1) { @@ -226,6 +231,7 @@ int fib(int n) { 给你 `k` 种面值的硬币,面值分别为 `c1, c2 ... ck`,每种硬币的数量无限,再给一个总金额 `amount`,问你**最少**需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。算法的函数签名如下: + ```java // coins 中是可选硬币面值,amount 是目标金额 int coinChange(int[] coins, int amount); @@ -265,6 +271,7 @@ int coinChange(int[] coins, int amount); 搞清楚上面这几个关键点,解法的伪码就可以写出来了: + ```java // 伪码框架 int coinChange(int[] coins, int amount) { @@ -284,6 +291,7 @@ int dp(int[] coins, int n) { 根据伪码,我们加上 base case 即可得到最终的答案。显然目标金额为 0 时,所需硬币数量为 0;当目标金额小于 0 时,无解,返回 -1: + ```java int coinChange(int[] coins, int amount) { // 题目要求的最终结果是 dp(amount) @@ -334,36 +342,39 @@ int dp(int[] coins, int amount) { 类似之前斐波那契数列的例子,只需要稍加修改,就可以通过备忘录消除子问题: + ```java -int[] memo; +class Solution { + int[] memo; -int coinChange(int[] coins, int amount) { - memo = new int[amount + 1]; - // 备忘录初始化为一个不会被取到的特殊值,代表还未被计算 - Arrays.fill(memo, -666); + int coinChange(int[] coins, int amount) { + memo = new int[amount + 1]; + // 备忘录初始化为一个不会被取到的特殊值,代表还未被计算 + Arrays.fill(memo, -666); - return dp(coins, amount); -} + return dp(coins, amount); + } -int dp(int[] coins, int amount) { - if (amount == 0) return 0; - if (amount < 0) return -1; - // 查备忘录,防止重复计算 - if (memo[amount] != -666) - return memo[amount]; + int dp(int[] coins, int amount) { + if (amount == 0) return 0; + if (amount < 0) return -1; + // 查备忘录,防止重复计算 + if (memo[amount] != -666) + return memo[amount]; - int res = Integer.MAX_VALUE; - for (int coin : coins) { - // 计算子问题的结果 - int subProblem = dp(coins, amount - coin); - // 子问题无解则跳过 - if (subProblem == -1) continue; - // 在子问题中选择最优解,然后加一 - res = Math.min(res, subProblem + 1); + int res = Integer.MAX_VALUE; + for (int coin : coins) { + // 计算子问题的结果 + int subProblem = dp(coins, amount - coin); + // 子问题无解则跳过 + if (subProblem == -1) continue; + // 在子问题中选择最优解,然后加一 + res = Math.min(res, subProblem + 1); + } + // 把计算结果存入备忘录 + memo[amount] = (res == Integer.MAX_VALUE) ? -1 : res; + return memo[amount]; } - // 把计算结果存入备忘录 - memo[amount] = (res == Integer.MAX_VALUE) ? -1 : res; - return memo[amount]; } ``` @@ -377,6 +388,7 @@ int dp(int[] coins, int amount) { 根据我们文章开头给出的动态规划代码框架可以写出如下解法: + ```java int coinChange(int[] coins, int amount) { int[] dp = new int[amount + 1]; diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\215\225\350\257\215\346\213\274\346\216\245.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\215\225\350\257\215\346\213\274\346\216\245.md" index c0271c5..7090420 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\215\225\350\257\215\346\213\274\346\216\245.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\215\225\350\257\215\346\213\274\346\216\245.md" @@ -42,6 +42,7 @@ 函数签名如下: + ```java boolean wordBreak(String s, List wordDict); ``` @@ -54,35 +55,39 @@ boolean wordBreak(String s, List wordDict); 这就是前文 [回溯算法秒杀排列组合问题的九种变体](https://labuladong.github.io/article/fname.html?fname=子集排列组合) 中讲到的最后一种变体:元素无重可复选的排列问题,前文我写了一个 `permuteRepeat` 函数,代码如下: + ```java -List> res = new LinkedList<>(); -LinkedList track = new LinkedList<>(); +class Solution { + List> res = new LinkedList<>(); + LinkedList track = new LinkedList<>(); -// 元素无重可复选的全排列 -public List> permuteRepeat(int[] nums) { - backtrack(nums); - return res; -} - -// 回溯算法核心函数 -void backtrack(int[] nums) { - // base case,到达叶子节点 - if (track.size() == nums.length) { - // 收集根到叶子节点路径上的值 - res.add(new LinkedList(track)); - return; + // 元素无重可复选的全排列 + public List> permuteRepeat(int[] nums) { + backtrack(nums); + return res; } - // 回溯算法标准框架 - for (int i = 0; i < nums.length; i++) { - // 做选择 - track.add(nums[i]); - // 进入下一层回溯树 - backtrack(nums); - // 取消选择 - track.removeLast(); + // 回溯算法核心函数 + void backtrack(int[] nums) { + // base case,到达叶子节点 + if (track.size() == nums.length) { + // 收集根到叶子节点路径上的值 + res.add(new LinkedList(track)); + return; + } + + // 回溯算法标准框架 + for (int i = 0; i < nums.length; i++) { + // 做选择 + track.add(nums[i]); + // 进入下一层回溯树 + backtrack(nums); + // 取消选择 + track.removeLast(); + } } } + ``` 给这个函数输入 `nums = [1,2,3]`,输出是 3^3 = 27 种可能的组合: @@ -107,47 +112,50 @@ void backtrack(int[] nums) { 回溯算法解法代码如下: + ```java -List wordDict; -// 记录是否找到一个合法的答案 -boolean found = false; -// 记录回溯算法的路径 -LinkedList track = new LinkedList<>(); - -// 主函数 -public boolean wordBreak(String s, List wordDict) { - this.wordDict = wordDict; - // 执行回溯算法穷举所有可能的组合 - backtrack(s, 0); - return found; -} - -// 回溯算法框架 -void backtrack(String s, int i) { - // base case - if (found) { - // 如果已经找到答案,就不要再递归搜索了 - return; - } - if (i == s.length()) { - // 整个 s 都被匹配完成,找到一个合法答案 - found = true; - return; +class Solution { + List wordDict; + // 记录是否找到一个合法的答案 + boolean found = false; + // 记录回溯算法的路径 + LinkedList track = new LinkedList<>(); + + // 主函数 + public boolean wordBreak(String s, List wordDict) { + this.wordDict = wordDict; + // 执行回溯算法穷举所有可能的组合 + backtrack(s, 0); + return found; } // 回溯算法框架 - for (String word : wordDict) { - // 看看哪个单词能够匹配 s[i..] 的前缀 - int len = word.length(); - if (i + len <= s.length() - && s.substring(i, i + len).equals(word)) { - // 找到一个单词匹配 s[i..i+len) - // 做选择 - track.addLast(word); - // 进入回溯树的下一层,继续匹配 s[i+len..] - backtrack(s, i + len); - // 撤销选择 - track.removeLast(); + void backtrack(String s, int i) { + // base case + if (found) { + // 如果已经找到答案,就不要再递归搜索了 + return; + } + if (i == s.length()) { + // 整个 s 都被匹配完成,找到一个合法答案 + found = true; + return; + } + + // 回溯算法框架 + for (String word : wordDict) { + // 看看哪个单词能够匹配 s[i..] 的前缀 + int len = word.length(); + if (i + len <= s.length() + && s.substring(i, i + len).equals(word)) { + // 找到一个单词匹配 s[i..i+len) + // 做选择 + track.addLast(word); + // 进入回溯树的下一层,继续匹配 s[i+len..] + backtrack(s, i + len); + // 撤销选择 + track.removeLast(); + } } } } @@ -161,6 +169,7 @@ void backtrack(String s, int i) { 那么 `backtrack` 函数本身的时间复杂度是多少呢?主要的时间消耗是遍历 `wordDict` 寻找匹配 `s[i..]` 的前缀的单词: + ```java // 遍历 wordDict 的所有单词 for (String word : wordDict) { @@ -178,6 +187,7 @@ for (String word : wordDict) { 这里顺便说一个细节优化,其实你也可以反过来,通过穷举 `s[i..]` 的前缀去判断 `wordDict` 中是否有对应的单词: + ```java // 注意,要转化成哈希集合,提高 contains 方法的效率 HashSet wordDict = new HashSet<>(wordDict); @@ -213,6 +223,7 @@ for (int len = 1; i + len <= s.length(); len++) { 有了这个思路就可以定义一个 `dp` 函数,并给出该函数的定义: + ```java // 定义:返回 s[i..] 是否能够被拼出 int dp(String s, int i); @@ -222,6 +233,7 @@ int dp(String s, int i); 有了这个函数定义,就可以把刚才的逻辑大致翻译成伪码: + ```java List wordDict; @@ -248,6 +260,7 @@ int dp(String s, int i) { 类似之前讲的回溯算法,`dp` 函数中的 for 循环也可以优化一下: + ```java // 注意,用哈希集合快速判断元素是否存在 HashSet wordDict; @@ -275,51 +288,55 @@ int dp(String s, int i) { 对于这个 `dp` 函数,指针 `i` 的位置就是「状态」,所以我们可以通过添加备忘录的方式优化效率,避免对相同的子问题进行冗余计算。最终的解法代码如下: + ```java -// 用哈希集合方便快速判断是否存在 -HashSet wordDict; -// 备忘录,-1 代表未计算,0 代表无法凑出,1 代表可以凑出 -int[] memo; - -// 主函数 -public boolean wordBreak(String s, List wordDict) { - // 转化为哈希集合,快速判断元素是否存在 - this.wordDict = new HashSet<>(wordDict); - // 备忘录初始化为 -1 - this.memo = new int[s.length()]; - Arrays.fill(memo, -1); - return dp(s, 0); -} - -// 定义:s[i..] 是否能够被拼出 -boolean dp(String s, int i) { - // base case - if (i == s.length()) { - return true; - } - // 防止冗余计算 - if (memo[i] != -1) { - return memo[i] == 0 ? false : true; +class Solution { + // 用哈希集合方便快速判断是否存在 + HashSet wordDict; + // 备忘录,-1 代表未计算,0 代表无法凑出,1 代表可以凑出 + int[] memo; + + // 主函数 + public boolean wordBreak(String s, List wordDict) { + // 转化为哈希集合,快速判断元素是否存在 + this.wordDict = new HashSet<>(wordDict); + // 备忘录初始化为 -1 + this.memo = new int[s.length()]; + Arrays.fill(memo, -1); + return dp(s, 0); } - // 遍历 s[i..] 的所有前缀 - for (int len = 1; i + len <= s.length(); len++) { - // 看看哪些前缀存在 wordDict 中 - String prefix = s.substring(i, i + len); - if (wordDict.contains(prefix)) { - // 找到一个单词匹配 s[i..i+len) - // 只要 s[i+len..] 可以被拼出,s[i..] 就能被拼出 - boolean subProblem = dp(s, i + len); - if (subProblem == true) { - memo[i] = 1; - return true; + // 定义:s[i..] 是否能够被拼出 + boolean dp(String s, int i) { + // base case + if (i == s.length()) { + return true; + } + // 防止冗余计算 + if (memo[i] != -1) { + return memo[i] == 0 ? false : true; + } + + // 遍历 s[i..] 的所有前缀 + for (int len = 1; i + len <= s.length(); len++) { + // 看看哪些前缀存在 wordDict 中 + String prefix = s.substring(i, i + len); + if (wordDict.contains(prefix)) { + // 找到一个单词匹配 s[i..i+len) + // 只要 s[i+len..] 可以被拼出,s[i..] 就能被拼出 + boolean subProblem = dp(s, i + len); + if (subProblem == true) { + memo[i] = 1; + return true; + } } } + // s[i..] 无法被拼出 + memo[i] = 0; + return false; } - // s[i..] 无法被拼出 - memo[i] = 0; - return false; } + ``` 这个解法能够通过所有测试用例,我们根据 [算法时空复杂度使用指南](https://labuladong.github.io/article/fname.html?fname=时间复杂度) 来算一下它的时间复杂度: @@ -336,100 +353,107 @@ boolean dp(String s, int i) { 上一道题的回溯算法维护一个 `found` 变量,只要找到一种拼接方案就提前结束遍历回溯树,那么在这道题中我们不要提前结束遍历,并把所有可行的拼接方案收集起来就能得到答案: + ```java -// 记录结果 -List res = new LinkedList<>(); -// 记录回溯算法的路径 -LinkedList track = new LinkedList<>(); -List wordDict; - -// 主函数 -public List wordBreak(String s, List wordDict) { - this.wordDict = wordDict; - // 执行回溯算法穷举所有可能的组合 - backtrack(s, 0); - return res; -} - -// 回溯算法框架 -void backtrack(String s, int i) { - // base case - if (i == s.length()) { - // 找到一个合法组合拼出整个 s,转化成字符串 - res.add(String.join(" ", track)); - return; +class Solution { + // 记录结果 + List res = new LinkedList<>(); + // 记录回溯算法的路径 + LinkedList track = new LinkedList<>(); + List wordDict; + + // 主函数 + public List wordBreak(String s, List wordDict) { + this.wordDict = wordDict; + // 执行回溯算法穷举所有可能的组合 + backtrack(s, 0); + return res; } // 回溯算法框架 - for (String word : wordDict) { - // 看看哪个单词能够匹配 s[i..] 的前缀 - int len = word.length(); - if (i + len <= s.length() - && s.substring(i, i + len).equals(word)) { - // 找到一个单词匹配 s[i..i+len) - // 做选择 - track.addLast(word); - // 进入回溯树的下一层,继续匹配 s[i+len..] - backtrack(s, i + len); - // 撤销选择 - track.removeLast(); + void backtrack(String s, int i) { + // base case + if (i == s.length()) { + // 找到一个合法组合拼出整个 s,转化成字符串 + res.add(String.join(" ", track)); + return; + } + + // 回溯算法框架 + for (String word : wordDict) { + // 看看哪个单词能够匹配 s[i..] 的前缀 + int len = word.length(); + if (i + len <= s.length() + && s.substring(i, i + len).equals(word)) { + // 找到一个单词匹配 s[i..i+len) + // 做选择 + track.addLast(word); + // 进入回溯树的下一层,继续匹配 s[i+len..] + backtrack(s, i + len); + // 撤销选择 + track.removeLast(); + } } } } + ``` 这个解法的时间复杂度和前一道题类似,依然是 `O(2^N * MN)`,但由于这道题给的数据规模较小,所以可以通过所有测试用例。 类似的,这个问题也可以用分解问题的思维解决,把上一道题的 `dp` 函数稍作修改即可: + ```java -HashSet wordDict; -// 备忘录 -List[] memo; - -public List wordBreak(String s, List wordDict) { - this.wordDict = new HashSet<>(wordDict); - memo = new List[s.length()]; - return dp(s, 0); -} +class Solution { + HashSet wordDict; + // 备忘录 + List[] memo; + + public List wordBreak(String s, List wordDict) { + this.wordDict = new HashSet<>(wordDict); + memo = new List[s.length()]; + return dp(s, 0); + } -// 定义:返回用 wordDict 构成 s[i..] 的所有可能 -List dp(String s, int i) { - List res = new LinkedList<>(); - if (i == s.length()) { - res.add(""); - return res; - } - // 防止冗余计算 - if (memo[i] != null) { - return memo[i]; - } - - // 遍历 s[i..] 的所有前缀 - for (int len = 1; i + len <= s.length(); len++) { - // 看看哪些前缀存在 wordDict 中 - String prefix = s.substring(i, i + len); - if (wordDict.contains(prefix)) { - // 找到一个单词匹配 s[i..i+len) - List subProblem = dp(s, i + len); - // 构成 s[i+len..] 的所有组合加上 prefix - // 就是构成构成 s[i] 的所有组合 - for (String sub : subProblem) { - if (sub.isEmpty()) { - // 防止多余的空格 - res.add(prefix); - } else { - res.add(prefix + " " + sub); + // 定义:返回用 wordDict 构成 s[i..] 的所有可能 + List dp(String s, int i) { + List res = new LinkedList<>(); + if (i == s.length()) { + res.add(""); + return res; + } + // 防止冗余计算 + if (memo[i] != null) { + return memo[i]; + } + + // 遍历 s[i..] 的所有前缀 + for (int len = 1; i + len <= s.length(); len++) { + // 看看哪些前缀存在 wordDict 中 + String prefix = s.substring(i, i + len); + if (wordDict.contains(prefix)) { + // 找到一个单词匹配 s[i..i+len) + List subProblem = dp(s, i + len); + // 构成 s[i+len..] 的所有组合加上 prefix + // 就是构成构成 s[i] 的所有组合 + for (String sub : subProblem) { + if (sub.isEmpty()) { + // 防止多余的空格 + res.add(prefix); + } else { + res.add(prefix + " " + sub); + } } } } + // 存入备忘录 + memo[i] = res; + + return res; } - // 存入备忘录 - memo[i] = res; - - return res; } ``` diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\233\242\347\201\255\350\202\241\347\245\250\351\227\256\351\242\230.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\233\242\347\201\255\350\202\241\347\245\250\351\227\256\351\242\230.md" index fcfcc0d..0be9a44 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\233\242\347\201\255\350\202\241\347\245\250\351\227\256\351\242\230.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\233\242\347\201\255\350\202\241\347\245\250\351\227\256\351\242\230.md" @@ -199,6 +199,7 @@ dp[i][1] = max(dp[i-1][1], -prices[i]) 直接写出代码: + ```java int n = prices.length; int[][] dp = new int[n][2]; @@ -231,6 +232,7 @@ if (i - 1 == -1) { 第一题就解决了,但是这样处理 base case 很麻烦,而且注意一下状态转移方程,新状态只和相邻的一个状态有关,所以可以用前文 [动态规划的降维打击:空间压缩技巧](https://labuladong.github.io/article/fname.html?fname=状态压缩技巧),不需要用整个 `dp` 数组,只需要一个变量储存相邻的那个状态就足够了,这样可以把空间复杂度降到 O(1): + ```java // 原始版本 int maxProfit_k_1(int[] prices) { @@ -286,6 +288,7 @@ dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i]) 直接翻译成代码: + ```java // 原始版本 int maxProfit_k_inf(int[] prices) { @@ -331,6 +334,7 @@ dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i]) 翻译成代码: + ```java // 原始版本 int maxProfit_with_cool(int[] prices) { @@ -392,6 +396,7 @@ dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee) 直接翻译成代码,注意状态转移方程改变后 base case 也要做出对应改变: + ```java // 原始版本 int maxProfit_with_fee(int[] prices, int fee) { @@ -444,6 +449,7 @@ dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]) 按照之前的代码,我们可能想当然这样写代码(错误的): + ```java int k = 2; int[][][] dp = new int[n][k + 1][2]; @@ -466,6 +472,7 @@ return dp[n - 1][k][0]; 比如说第一题,`k = 1` 时的代码框架: + ```java int n = prices.length; int[][] dp = new int[n][2]; @@ -478,6 +485,7 @@ return dp[n - 1][0]; 但当 `k = 2` 时,由于没有消掉 `k` 的影响,所以必须要对 `k` 进行穷举: + ```java // 原始版本 int maxProfit_k_2(int[] prices) { @@ -512,6 +520,7 @@ int maxProfit_k_2(int[] prices) { 当然,这里 `k` 取值范围比较小,所以也可以不用 for 循环,直接把 k = 1 和 2 的情况全部列举出来也可以: + ```java // 状态转移方程: // dp[i][2][0] = max(dp[i-1][2][0], dp[i-1][2][1] + prices[i]) @@ -548,6 +557,7 @@ int maxProfit_k_2(int[] prices) { 所以我们可以直接把之前的代码重用: + ```java int maxProfit_k_any(int max_k, int[] prices) { int n = prices.length; @@ -602,6 +612,7 @@ int maxProfit_all_in_one(int max_k, int[] prices, int cooldown, int fee); 所以,我们只要把之前实现的几种代码掺和到一块,**在 base case 和状态转移方程中同时加上 `cooldown` 和 `fee` 的约束就行了**: + ```java // 同时考虑交易次数的限制、冷冻期和手续费 int maxProfit_all_in_one(int max_k, int[] prices, int cooldown, int fee) { diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\212\242\346\210\277\345\255\220.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\212\242\346\210\277\345\255\220.md" index 86aed33..0b40a51 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\212\242\346\210\277\345\255\220.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\212\242\346\210\277\345\255\220.md" @@ -39,6 +39,7 @@ 请你写一个算法,计算在不触动报警器的前提下,最多能够盗窃多少现金呢?函数签名如下: + ```java int rob(int[] nums); ``` diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\234\200\344\274\230\345\255\220\347\273\223\346\236\204.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\234\200\344\274\230\345\255\220\347\273\223\346\236\204.md" index 907f4f1..eef5e4d 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\234\200\344\274\230\345\255\220\347\273\223\346\236\204.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\346\234\200\344\274\230\345\255\220\347\273\223\346\236\204.md" @@ -64,6 +64,7 @@ return result; 再举个常见但也十分简单的例子,求一棵二叉树的最大值,不难吧(简单起见,假设节点中的值都是非负数): + ```java int maxVal(TreeNode root) { if (root == null) @@ -108,6 +109,7 @@ int maxVal(TreeNode root) { 比如在 [最小路径和问题](https://labuladong.github.io/article/fname.html?fname=最小路径和) 中,我们写出了这样一个暴力解法: + ```java int dp(int[][] grid, int i, int j) { if (i == 0 && j == 0) { @@ -147,6 +149,7 @@ int dp(int[][] grid, int i, int j) { 再举个稍微复杂的例子,前文 [正则表达式问题](https://labuladong.github.io/article/fname.html?fname=动态规划之正则表达) 的暴力解代码: + ```cpp bool dp(string& s, int i, string& p, int j) { int m = s.size(), n = p.size(); @@ -193,6 +196,7 @@ bool dp(string& s, int i, string& p, int j) { 比如说前文 [编辑距离问题](https://labuladong.github.io/article/fname.html?fname=编辑距离),我首先讲的是自顶向下的递归解法,实现了这样一个 `dp` 函数: + ```java int minDistance(String s1, String s2) { int m = s1.length(), n = s2.length(); @@ -225,6 +229,7 @@ int dp(String s1, int i, String s2, int j) { 然后改造成了自底向上的迭代解法: + ```java int minDistance(String s1, String s2) { int m = s1.length(), n = s2.length(); @@ -344,6 +349,7 @@ for (int i = 1; i < m; i++) - [一个方法团灭 LeetCode 股票买卖问题](https://labuladong.github.io/article/fname.html?fname=团灭股票问题) - [分治算法详解:运算优先级](https://labuladong.github.io/article/fname.html?fname=分治算法) + - [动态规划之子序列问题解题模板](https://labuladong.github.io/article/fname.html?fname=子序列问题模板) - [动态规划解题套路框架](https://labuladong.github.io/article/fname.html?fname=动态规划详解进阶) - [对动态规划进行降维打击](https://labuladong.github.io/article/fname.html?fname=状态压缩技巧) - [经典动态规划:博弈问题](https://labuladong.github.io/article/fname.html?fname=动态规划之博弈问题) diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\347\212\266\346\200\201\345\216\213\347\274\251\346\212\200\345\267\247.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\347\212\266\346\200\201\345\216\213\347\274\251\346\212\200\345\267\247.md" index 7fe12a1..fe6e82b 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\347\212\266\346\200\201\345\216\213\347\274\251\346\212\200\345\267\247.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\347\212\266\346\200\201\345\216\213\347\274\251\346\212\200\345\267\247.md" @@ -27,10 +27,11 @@ 什么叫「和 `dp[i][j]` 相邻的状态」呢,比如前文 [最长回文子序列](https://labuladong.github.io/article/fname.html?fname=子序列问题模板) 中,最终的代码如下: + ```cpp int longestPalindromeSubseq(string s) { int n = s.size(); - // dp 数组全部初始化为 0 + // nxn 的 dp 数组全部初始化为 0 vector> dp(n, vector(n, 0)); // base case for (int i = 0; i < n; i++) @@ -168,6 +169,7 @@ for (int i = 5; i--) { 那么现在我们成功对状态转移方程进行了降维打击,算是最硬的的骨头啃掉了,但注意到我们还有 base case 要处理呀: + ```cpp // dp 数组全部初始化为 0 vector> dp(n, vector(n, 0)); @@ -182,6 +184,7 @@ for (int i = 0; i < n; i++) 二维 `dp` 数组中的 base case 全都落入了一维 `dp` 数组,不存在冲突和覆盖,所以说我们直接这样写代码就行了: + ```cpp // 一维 dp 数组全部初始化为 1 vector dp(n, 1); @@ -189,6 +192,7 @@ vector dp(n, 1); 至此,我们把 base case 和状态转移方程都进行了降维,实际上已经写出完整代码了: + ```cpp int longestPalindromeSubseq(string s) { int n = s.size(); diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\347\274\226\350\276\221\350\267\235\347\246\273.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\347\274\226\350\276\221\350\267\235\347\246\273.md" index 7ec7199..f63d226 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\347\274\226\350\276\221\350\267\235\347\246\273.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\347\274\226\350\276\221\350\267\235\347\246\273.md" @@ -33,6 +33,7 @@ 函数签名如下: + ```java int minDistance(String s1, String s2) ``` @@ -96,6 +97,7 @@ else: 有这个框架,问题就已经解决了。读者也许会问,这个「三选一」到底该怎么选择呢?很简单,全试一遍,哪个操作最后得到的编辑距离最小,就选谁。这里需要递归技巧,先看下暴力解法代码: + ```java int minDistance(String s1, String s2) { int m = s1.length(), n = s2.length(); @@ -128,6 +130,7 @@ int min(int a, int b, int c) { 都说递归代码的可解释性很好,这是有道理的,只要理解函数的定义,就能很清楚地理解算法的逻辑。我们这里 `dp` 函数的定义是这样的: + ```java // 定义:返回 s1[0..i] 和 s2[0..j] 的最小编辑距离 int dp(String s1, int i, String s2, int j) { @@ -197,42 +200,45 @@ int dp(i, j) { 备忘录很好加,原来的代码稍加修改即可: + ```java -// 备忘录 -int[][] memo; - -public int minDistance(String s1, String s2) { - int m = s1.length(), n = s2.length(); - // 备忘录初始化为特殊值,代表还未计算 - memo = new int[m][n]; - for (int[] row : memo) { - Arrays.fill(row, -1); +class Solution { + // 备忘录 + int[][] memo; + + public int minDistance(String s1, String s2) { + int m = s1.length(), n = s2.length(); + // 备忘录初始化为特殊值,代表还未计算 + memo = new int[m][n]; + for (int[] row : memo) { + Arrays.fill(row, -1); + } + return dp(s1, m - 1, s2, n - 1); } - return dp(s1, m - 1, s2, n - 1); -} -int dp(String s1, int i, String s2, int j) { - if (i == -1) return j + 1; - if (j == -1) return i + 1; - // 查备忘录,避免重叠子问题 - if (memo[i][j] != -1) { + int dp(String s1, int i, String s2, int j) { + if (i == -1) return j + 1; + if (j == -1) return i + 1; + // 查备忘录,避免重叠子问题 + if (memo[i][j] != -1) { + return memo[i][j]; + } + // 状态转移,结果存入备忘录 + if (s1.charAt(i) == s2.charAt(j)) { + memo[i][j] = dp(s1, i - 1, s2, j - 1); + } else { + memo[i][j] = min( + dp(s1, i, s2, j - 1) + 1, + dp(s1, i - 1, s2, j) + 1, + dp(s1, i - 1, s2, j - 1) + 1 + ); + } return memo[i][j]; } - // 状态转移,结果存入备忘录 - if (s1.charAt(i) == s2.charAt(j)) { - memo[i][j] = dp(s1, i - 1, s2, j - 1); - } else { - memo[i][j] = min( - dp(s1, i, s2, j - 1) + 1, - dp(s1, i - 1, s2, j) + 1, - dp(s1, i - 1, s2, j - 1) + 1 - ); - } - return memo[i][j]; -} -int min(int a, int b, int c) { - return Math.min(a, Math.min(b, c)); + int min(int a, int b, int c) { + return Math.min(a, Math.min(b, c)); + } } ``` @@ -256,6 +262,7 @@ dp[i-1][j-1] 既然 `dp` 数组和递归 `dp` 函数含义一样,也就可以直接套用之前的思路写代码,**唯一不同的是,DP table 是自底向上求解,递归解法是自顶向下求解**: + ```java int minDistance(String s1, String s2) { int m = s1.length(), n = s2.length(); @@ -301,6 +308,7 @@ int min(int a, int b, int c) { 这个其实很简单,代码稍加修改,给 dp 数组增加额外的信息即可: + ```java // int[][] dp; Node[][] dp; diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\350\264\252\345\277\203\347\256\227\346\263\225\344\271\213\345\214\272\351\227\264\350\260\203\345\272\246\351\227\256\351\242\230.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\350\264\252\345\277\203\347\256\227\346\263\225\344\271\213\345\214\272\351\227\264\350\260\203\345\272\246\351\227\256\351\242\230.md" index 7842473..dc57182 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\350\264\252\345\277\203\347\256\227\346\263\225\344\271\213\345\214\272\351\227\264\350\260\203\345\272\246\351\227\256\351\242\230.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\350\264\252\345\277\203\347\256\227\346\263\225\344\271\213\345\214\272\351\227\264\350\260\203\345\272\246\351\227\256\351\242\230.md" @@ -38,6 +38,7 @@ 给你很多形如 `[start, end]` 的闭区间,请你设计一个算法,**算出这些区间中最多有几个互不相交的区间**。 + ```java int intervalSchedule(int[][] intvs); ``` @@ -72,6 +73,7 @@ int intervalSchedule(int[][] intvs); 看下代码: + ```java public int intervalSchedule(int[][] intvs) { if (intvs.length == 0) return 0; @@ -105,6 +107,7 @@ public int intervalSchedule(int[][] intvs) { 输入一个区间的集合,请你计算,要想使其中的区间都互不重叠,至少需要移除几个区间?函数签名如下: + ```java int eraseOverlapIntervals(int[][] intvs); ``` @@ -115,6 +118,7 @@ int eraseOverlapIntervals(int[][] intvs); 我们已经会求最多有几个区间不会重叠了,那么剩下的不就是至少需要去除的区间吗? + ```java int eraseOverlapIntervals(int[][] intervals) { int n = intervals.length; @@ -128,6 +132,7 @@ int eraseOverlapIntervals(int[][] intervals) { 函数签名如下: + ```java int findMinArrowShots(int[][] intvs); ``` @@ -144,6 +149,7 @@ int findMinArrowShots(int[][] intvs); 所以只要将之前的算法稍作修改,就是这道题目的答案: + ```java int findMinArrowShots(int[][] intvs) { // ... diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\351\255\224\345\241\224.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\351\255\224\345\241\224.md" index ac440e7..904b18b 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\351\255\224\345\241\224.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\351\255\224\345\241\224.md" @@ -39,6 +39,7 @@ 函数签名如下: + ```java int calculateMinimumHP(int[][] grid); ``` @@ -69,6 +70,7 @@ int calculateMinimumHP(int[][] grid); 这类求最值的问题,肯定要借助动态规划技巧,要合理设计 `dp` 数组/函数的定义。类比前文 [最小路径和问题](https://labuladong.github.io/article/fname.html?fname=最小路径和),`dp` 函数签名肯定长这样: + ```java int dp(int[][] grid, int i, int j); ``` @@ -79,6 +81,7 @@ int dp(int[][] grid, int i, int j); 这样定义的话,base case 就是 `i, j` 都等于 0 的时候,我们可以这样写代码: + ```java int calculateMinimumHP(int[][] grid) { int m = grid.length; @@ -131,6 +134,7 @@ int dp(int[][] grid, int i, int j) { 正确的做法需要反向思考,依然是如下的 `dp` 函数: + ```java int dp(int[][] grid, int i, int j); ``` @@ -141,6 +145,7 @@ int dp(int[][] grid, int i, int j); 那么可以这样写代码: + ```java int calculateMinimumHP(int[][] grid) { // 我们想计算左上角到右下角所需的最小生命值 @@ -185,48 +190,52 @@ dp(i, j) = res <= 0 ? 1 : res; 根据这个核心逻辑,加一个备忘录消除重叠子问题,就可以直接写出最终的代码了: + ```java -/* 主函数 */ -int calculateMinimumHP(int[][] grid) { - int m = grid.length; - int n = grid[0].length; - // 备忘录中都初始化为 -1 - memo = new int[m][n]; - for (int[] row : memo) { - Arrays.fill(row, -1); +class Solution { + /* 主函数 */ + public int calculateMinimumHP(int[][] grid) { + int m = grid.length; + int n = grid[0].length; + // 备忘录中都初始化为 -1 + memo = new int[m][n]; + for (int[] row : memo) { + Arrays.fill(row, -1); + } + + return dp(grid, 0, 0); } - return dp(grid, 0, 0); -} - -// 备忘录,消除重叠子问题 -int[][] memo; + // 备忘录,消除重叠子问题 + int[][] memo; + + /* 定义:从 (i, j) 到达右下角,需要的初始血量至少是多少 */ + int dp(int[][] grid, int i, int j) { + int m = grid.length; + int n = grid[0].length; + // base case + if (i == m - 1 && j == n - 1) { + return grid[i][j] >= 0 ? 1 : -grid[i][j] + 1; + } + if (i == m || j == n) { + return Integer.MAX_VALUE; + } + // 避免重复计算 + if (memo[i][j] != -1) { + return memo[i][j]; + } + // 状态转移逻辑 + int res = Math.min( + dp(grid, i, j + 1), + dp(grid, i + 1, j) + ) - grid[i][j]; + // 骑士的生命值至少为 1 + memo[i][j] = res <= 0 ? 1 : res; -/* 定义:从 (i, j) 到达右下角,需要的初始血量至少是多少 */ -int dp(int[][] grid, int i, int j) { - int m = grid.length; - int n = grid[0].length; - // base case - if (i == m - 1 && j == n - 1) { - return grid[i][j] >= 0 ? 1 : -grid[i][j] + 1; - } - if (i == m || j == n) { - return Integer.MAX_VALUE; - } - // 避免重复计算 - if (memo[i][j] != -1) { return memo[i][j]; } - // 状态转移逻辑 - int res = Math.min( - dp(grid, i, j + 1), - dp(grid, i + 1, j) - ) - grid[i][j]; - // 骑士的生命值至少为 1 - memo[i][j] = res <= 0 ? 1 : res; - - return memo[i][j]; } + ``` 这就是自顶向下带备忘录的动态规划解法,参考前文 [动态规划套路详解](https://labuladong.github.io/article/fname.html?fname=动态规划详解进阶) 很容易就可以改写成 `dp` 数组的迭代解法,这里就不写了,读者可以尝试自己写一写。 diff --git "a/\346\212\200\346\234\257/\345\210\267\351\242\230\346\212\200\345\267\247.md" "b/\346\212\200\346\234\257/\345\210\267\351\242\230\346\212\200\345\267\247.md" index 8ede9b9..7844cc1 100644 --- "a/\346\212\200\346\234\257/\345\210\267\351\242\230\346\212\200\345\267\247.md" +++ "b/\346\212\200\346\234\257/\345\210\267\351\242\230\346\212\200\345\267\247.md" @@ -65,6 +65,7 @@ 首先这样提交一下: + ```java public class Main { public static void main(String[] args) { @@ -75,6 +76,7 @@ public class Main { 看下 case 通过率,假设是 60%,那么说明结果为 `YES` 有 60% 的概率,所以可以这样写代码: + ```java public class Main { public static void main(String[] args) { @@ -106,6 +108,7 @@ Python 的话我刷题用的比较少,因为我不太喜欢用动态语言, 举个例子,比如说一道题,我决定用带备忘录的动态规划求解,代码的大致结构是这样: + ```java public class Main { public static void main(String[] args) { @@ -165,6 +168,7 @@ public class Main { 最能提升我们 debug 效率的是缩进,除了解法函数,我们新定义一个函数 `printIndent` 和一个全局变量 `count`: + ```cpp // 全局变量,记录递归函数的递归层数 int count = 0; @@ -183,6 +187,7 @@ void printIndent(int n) { 举个具体的例子,比如说上篇文章 [练琴时悟出的一个动态规划算法](https://labuladong.github.io/article/fname.html?fname=转盘) 中实现了一个递归的 `dp` 函数,大致的结构如下: + ```cpp int dp(string& ring, int i, string& key, int j) { /* base case */ @@ -202,6 +207,7 @@ int dp(string& ring, int i, string& key, int j) { 这个递归的 `dp` 函数在我进行了 debug 之后,变成了这样: + ```cpp int count = 0; void printIndent(int n) { diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/BST1.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/BST1.md" index 4280e11..c28b500 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/BST1.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/BST1.md" @@ -42,6 +42,7 @@ 也就是说,如果输入一棵 BST,以下代码可以将 BST 中每个节点的值升序打印出来: + ```java void traverse(TreeNode root) { if (root == null) return; @@ -64,6 +65,7 @@ void traverse(TreeNode root) { 按照这个思路,可以直接写出代码: + ```java int kthSmallest(TreeNode root, int k) { // 利用 BST 的中序遍历特性 @@ -124,6 +126,7 @@ void traverse(TreeNode root, int k) { 也就是说,我们 `TreeNode` 中的字段应该如下: + ```java class TreeNode { int val; @@ -148,6 +151,7 @@ class TreeNode { 我们需要把 BST 转化成累加树,函数签名如下: + ```java TreeNode convertBST(TreeNode root) ``` @@ -162,6 +166,7 @@ BST 的每个节点左小右大,这似乎是一个有用的信息,既然累 刚才我们说了 BST 的中序遍历代码可以升序打印节点的值: + ```java void traverse(TreeNode root) { if (root == null) return; @@ -176,6 +181,7 @@ void traverse(TreeNode root) { 很简单,只要把递归顺序改一下就行了: + ```java void traverse(TreeNode root) { if (root == null) return; @@ -192,6 +198,7 @@ void traverse(TreeNode root) { 看下代码就明白了: + ```java TreeNode convertBST(TreeNode root) { traverse(root); diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/BST2.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/BST2.md" index abb1a4c..1c2f94a 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/BST2.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/BST2.md" @@ -34,6 +34,7 @@ BST 的基础操作主要依赖「左小右大」的特性,可以在二叉树 对于 BST 相关的问题,你可能会经常看到类似下面这样的代码逻辑: + ```java void BST(TreeNode root, int target) { if (root.val == target) @@ -51,6 +52,7 @@ void BST(TreeNode root, int target) { 力扣第 98 题「验证二叉搜索树」就是让你判断输入的 BST 是否合法。注意,这里是有坑的哦,按照 BST 左小右大的特性,每个节点想要判断自己是否是合法的 BST 节点,要做的事不就是比较自己和左右孩子吗?感觉应该这样写代码: + ```java boolean isValidBST(TreeNode root) { if (root == null) return true; @@ -74,6 +76,7 @@ boolean isValidBST(TreeNode root) { 问题是,对于某一个节点 `root`,他只能管得了自己的左右子节点,怎么把 `root` 的约束传递给左右子树呢?请看正确的代码: + ```java boolean isValidBST(TreeNode root) { return isValidBST(root, null, null); @@ -98,12 +101,14 @@ boolean isValidBST(TreeNode root, TreeNode min, TreeNode max) { 力扣第 700 题「二叉搜索树中的搜索」就是让你在 BST 中搜索值为 `target` 的节点,函数签名如下: + ```java TreeNode searchBST(TreeNode root, int target); ``` 如果是在一棵普通的二叉树中寻找,可以这样写代码: + ```java TreeNode searchBST(TreeNode root, int target); if (root == null) return null; @@ -120,6 +125,7 @@ TreeNode searchBST(TreeNode root, int target); 很简单,其实不需要递归地搜索两边,类似二分查找思想,根据 `target` 和 `root.val` 的大小比较,就能排除一边。我们把上面的思路稍稍改动: + ```java TreeNode searchBST(TreeNode root, int target) { if (root == null) { @@ -143,6 +149,7 @@ TreeNode searchBST(TreeNode root, int target) { 上一个问题,我们总结了 BST 中的遍历框架,就是「找」的问题。直接套框架,加上「改」的操作即可。**一旦涉及「改」,就类似二叉树的构造问题,函数要返回 `TreeNode` 类型,并且要对递归调用的返回值进行接收**。 + ```java TreeNode insertIntoBST(TreeNode root, int val) { // 找到空位置插入新节点 @@ -161,6 +168,7 @@ TreeNode insertIntoBST(TreeNode root, int val) { 这个问题稍微复杂,跟插入操作类似,先「找」再「改」,先把框架写出来再说: + ```java TreeNode deleteNode(TreeNode root, int key) { if (root.val == key) { @@ -214,6 +222,7 @@ if (root.left != null && root.right != null) { 三种情况分析完毕,填入框架,简化一下代码: + ```java TreeNode deleteNode(TreeNode root, int key) { if (root == null) return null; diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/dijkstra\347\256\227\346\263\225.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/dijkstra\347\256\227\346\263\225.md" index 5da9944..ca18834 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/dijkstra\347\256\227\346\263\225.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/dijkstra\347\256\227\346\263\225.md" @@ -47,6 +47,7 @@ 前文 [图论第二期:拓扑排序](https://labuladong.github.io/article/fname.html?fname=拓扑排序) 告诉你,我们用邻接表的场景更多,结合上图,一幅图可以用如下 Java 代码表示: + ```java // graph[s] 存储节点 s 指向的节点(出度) List[] graph; @@ -54,6 +55,7 @@ List[] graph; **如果你想把一个问题抽象成「图」的问题,那么首先要实现一个 API `adj`**: + ```java // 输入节点 s 返回 s 的相邻节点 List adj(int s); @@ -63,6 +65,7 @@ List adj(int s); 比如上面说的用邻接表表示「图」的方式,`adj` 函数就可以这样表示: + ```java List[] graph; @@ -74,6 +77,7 @@ List adj(int s) { 当然,对于「加权图」,我们需要知道两个节点之间的边权重是多少,所以还可以抽象出一个 `weight` 方法: + ```java // 返回节点 from 到节点 to 之间的边的权重 int weight(int from, int to); @@ -87,6 +91,7 @@ int weight(int from, int to); 我们之前说过二叉树的层级遍历框架: + ```java // 输入一棵二叉树的根节点,层序遍历这棵二叉树 void levelTraverse(TreeNode root) { @@ -130,6 +135,7 @@ void levelTraverse(TreeNode root) { 基于二叉树的遍历框架,我们又可以扩展出多叉树的层序遍历框架: + ```java // 输入一棵多叉树的根节点,层序遍历这棵多叉树 void levelTraverse(TreeNode root) { @@ -158,6 +164,7 @@ void levelTraverse(TreeNode root) { 基于多叉树的遍历框架,我们又可以扩展出 BFS(广度优先搜索)的算法框架: + ```java // 输入起点,进行 BFS 搜索 int BFS(Node start) { @@ -216,6 +223,7 @@ int BFS(Node start) { 怎么去掉?就拿二叉树的层级遍历来说,其实你可以直接去掉 `for` 循环相关的代码: + ```java // 输入一棵二叉树的根节点,遍历这棵二叉树所有节点 void levelTraverse(TreeNode root) { @@ -243,6 +251,7 @@ void levelTraverse(TreeNode root) { 如果你想同时维护 `depth` 变量,让每个节点 `cur` 知道自己在第几层,可以想其他办法,比如新建一个 `State` 类,记录每个节点所在的层数: + ```java class State { // 记录 node 节点的深度 @@ -287,6 +296,7 @@ void levelTraverse(TreeNode root) { **首先,我们先看一下 Dijkstra 算法的签名**: + ```java // 输入一幅图和一个起点 start,计算 start 到其他节点的最短距离 int[] dijkstra(int start, List[] graph); @@ -302,6 +312,7 @@ int[] dijkstra(int start, List[] graph); **其次,我们也需要一个 `State` 类来辅助算法的运行**: + ```java class State { // 图节点的 id @@ -330,6 +341,7 @@ class State { **其实,Dijkstra 可以理解成一个带 dp table(或者说备忘录)的 BFS 算法,伪码如下**: + ```java // 返回节点 from 到节点 to 之间的边的权重 int weight(int from, int to); @@ -398,6 +410,7 @@ int[] dijkstra(int start, List[] graph) { `while` 循环每执行一次,都会往外拿一个元素,但想往队列里放元素,可就有很多限制了,必须满足下面这个条件: + ```java // 看看从 curNode 达到 nextNode 的距离是否会更短 if (distTo[nextNodeID] > distToNextNode) { @@ -437,6 +450,7 @@ if (distTo[nextNodeID] > distToNextNode) { 需要在代码中做的修改也非常少,只要改改函数签名,再加个 if 判断就行了: + ```java // 输入起点 start 和终点 end,计算起点到终点的最短距离 int dijkstra(int start, int end, List[] graph) { @@ -493,6 +507,7 @@ Dijkstra 算法的时间复杂度是多少?你去网上查,可能会告诉 函数签名如下: + ```java // times 记录边和权重,n 为节点个数(从 1 开始),k 为起点 // 计算从 k 发出的信号至少需要多久传遍整幅图 @@ -505,6 +520,7 @@ int networkDelayTime(int[][] times, int n, int k) 根据我们之前 Dijkstra 算法的框架,我们可以写出下面代码: + ```java int networkDelayTime(int[][] times, int n, int k) { // 节点编号是从 1 开始的,所以要一个大小为 n + 1 的邻接表 @@ -542,6 +558,7 @@ int[] dijkstra(int start, List[] graph) {} 上述代码首先利用题目输入的数据转化成邻接表表示一幅图,接下来我们可以直接套用 Dijkstra 算法的框架: + ```java class State { // 图节点的 id @@ -602,6 +619,7 @@ int[] dijkstra(int start, List[] graph) { 函数签名如下: + ```java // 输入一个二维矩阵,计算从左上角到右下角的最小体力消耗 int minimumEffortPath(int[][] heights); @@ -613,6 +631,7 @@ int minimumEffortPath(int[][] heights); 这样一想,是不是就在让你以左上角坐标为起点,以右下角坐标为终点,计算起点到终点的最短路径?Dijkstra 算法是不是可以做到? + ```java // 输入起点 start 和终点 end,计算起点到终点的最短距离 int dijkstra(int start, int end, List[] graph) @@ -624,6 +643,7 @@ int dijkstra(int start, int end, List[] graph) 二维矩阵抽象成图,我们先实现一下图的 `adj` 方法,之后的主要逻辑会清晰一些: + ```java // 方向数组,上下左右的坐标偏移量 int[][] dirs = new int[][]{{0,1}, {1,0}, {0,-1}, {-1,0}}; @@ -648,6 +668,7 @@ List adj(int[][] matrix, int x, int y) { 类似的,我们现在认为一个二维坐标 `(x, y)` 是图中的一个节点,所以这个 `State` 类也需要修改一下: + ```java class State { // 矩阵中的一个位置 @@ -665,6 +686,7 @@ class State { 接下来,就可以套用 Dijkstra 算法的代码模板了: + ```java // Dijkstra 算法,计算 (0, 0) 到 (m - 1, n - 1) 的最小体力消耗 int minimumEffortPath(int[][] heights) { @@ -729,6 +751,7 @@ int minimumEffortPath(int[][] heights) { 函数签名如下: + ```java // 输入一幅无向图,边上的权重代表概率,返回从 start 到达 end 最大的概率 double maxProbability(int n, int[][] edges, double[] succProb, int start, int end) @@ -758,6 +781,7 @@ double maxProbability(int n, int[][] edges, double[] succProb, int start, int en 只不过,这道题的解法要把优先级队列的排序顺序反过来,一些 if 大小判断也要反过来,我们直接看解法代码吧: + ```java double maxProbability(int n, int[][] edges, double[] succProb, int start, int end) { List[] graph = new LinkedList[n]; diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\345\240\206\350\257\246\350\247\243\345\256\236\347\216\260\344\274\230\345\205\210\347\272\247\351\230\237\345\210\227.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\345\240\206\350\257\246\350\247\243\345\256\236\347\216\260\344\274\230\345\205\210\347\272\247\351\230\237\345\210\227.md" index 353b17e..4bf89d7 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\345\240\206\350\257\246\350\247\243\345\256\236\347\216\260\344\274\230\345\205\210\347\272\247\351\230\237\345\210\227.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\345\240\206\350\257\246\350\247\243\345\256\236\347\216\260\344\274\230\345\205\210\347\272\247\351\230\237\345\210\227.md" @@ -25,6 +25,7 @@ 因为,二叉堆在逻辑上其实是一种特殊的二叉树(完全二叉树),只不过存储在数组里。一般的链表二叉树,我们操作节点的指针,而在数组里,我们把数组索引作为指针: + ```java // 父节点的索引 int parent(int root) { @@ -64,6 +65,7 @@ int right(int root) { > tip:这里用到 Java 的泛型,`Key` 可以是任何一种可比较大小的数据类型,比如 Integer 等类型。 + ```java public class MaxPQ > { @@ -132,14 +134,19 @@ public class MaxPQ **上浮的代码实现:** + ```java -private void swim(int x) { - // 如果浮到堆顶,就不能再上浮了 - while (x > 1 && less(parent(x), x)) { - // 如果第 x 个元素比上层大 - // 将 x 换上去 - swap(parent(x), x); - x = parent(x); +public class MaxPQ > { + // 为了节约篇幅,省略上文给出的代码部分... + + private void swim(int x) { + // 如果浮到堆顶,就不能再上浮了 + while (x > 1 && less(parent(x), x)) { + // 如果第 x 个元素比上层大 + // 将 x 换上去 + swap(parent(x), x); + x = parent(x); + } } } ``` @@ -152,20 +159,25 @@ private void swim(int x) { 下沉比上浮略微复杂一点,因为上浮某个节点 A,只需要 A 和其父节点比较大小即可;但是下沉某个节点 A,需要 A 和其**两个子节点**比较大小,如果 A 不是最大的就需要调整位置,要把较大的那个子节点和 A 交换。 + ```java -private void sink(int x) { - // 如果沉到堆底,就沉不下去了 - while (left(x) <= size) { - // 先假设左边节点较大 - int max = left(x); - // 如果右边节点存在,比一下大小 - if (right(x) <= size && less(max, right(x))) - max = right(x); - // 结点 x 比俩孩子都大,就不必下沉了 - if (less(max, x)) break; - // 否则,不符合最大堆的结构,下沉 x 结点 - swap(x, max); - x = max; +public class MaxPQ > { + // 为了节约篇幅,省略上文给出的代码部分... + + private void sink(int x) { + // 如果沉到堆底,就沉不下去了 + while (left(x) <= size) { + // 先假设左边节点较大 + int max = left(x); + // 如果右边节点存在,比一下大小 + if (right(x) <= size && less(max, right(x))) + max = right(x); + // 结点 x 比俩孩子都大,就不必下沉了 + if (less(max, x)) break; + // 否则,不符合最大堆的结构,下沉 x 结点 + swap(x, max); + x = max; + } } } ``` @@ -184,29 +196,39 @@ private void sink(int x) { ![](https://labuladong.gitee.io/pictures/heap/insert.gif) + ```java -public void insert(Key e) { - size++; - // 先把新元素加到最后 - pq[size] = e; - // 然后让它上浮到正确的位置 - swim(size); +public class MaxPQ > { + // 为了节约篇幅,省略上文给出的代码部分... + + public void insert(Key e) { + size++; + // 先把新元素加到最后 + pq[size] = e; + // 然后让它上浮到正确的位置 + swim(size); + } } ``` **`delMax` 方法先把堆顶元素 `A` 和堆底最后的元素 `B` 对调,然后删除 `A`,最后让 `B` 下沉到正确位置**。 + ```java -public Key delMax() { - // 最大堆的堆顶就是最大元素 - Key max = pq[1]; - // 把这个最大元素换到最后,删除之 - swap(1, size); - pq[size] = null; - size--; - // 让 pq[1] 下沉到正确位置 - sink(1); - return max; +public class MaxPQ > { + // 为了节约篇幅,省略上文给出的代码部分... + + public Key delMax() { + // 最大堆的堆顶就是最大元素 + Key max = pq[1]; + // 把这个最大元素换到最后,删除之 + swap(1, size); + pq[size] = null; + size--; + // 让 pq[1] 下沉到正确位置 + sink(1); + return max; + } } ``` diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\346\200\273\347\273\223.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\346\200\273\347\273\223.md" index 44a321b..356575f 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\346\200\273\347\273\223.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\346\200\273\347\273\223.md" @@ -58,6 +58,7 @@ 快速排序的代码框架如下: + ```java void sort(int[] nums, int lo, int hi) { /****** 前序遍历位置 ******/ @@ -76,6 +77,7 @@ void sort(int[] nums, int lo, int hi) { 归并排序的代码框架如下: + ```java // 定义:排序 nums[lo..hi] void sort(int[] nums, int lo, int hi) { @@ -114,6 +116,7 @@ void sort(int[] nums, int lo, int hi) { 首先,回顾一下 [学习数据结构和算法的框架思维](https://labuladong.github.io/article/fname.html?fname=学习数据结构和算法的高效方法) 中说到的二叉树遍历框架: + ```java void traverse(TreeNode root) { if (root == null) { @@ -131,6 +134,7 @@ void traverse(TreeNode root) { 其实它就是一个能够遍历二叉树所有节点的一个函数,和你遍历数组或者链表本质上没有区别: + ```java /* 迭代遍历数组 */ void traverse(int[] arr) { @@ -179,6 +183,7 @@ void traverse(ListNode head) { 实现方式当然有很多,但如果你对递归的理解足够透彻,可以利用后序位置来操作: + ```java /* 递归遍历单链表,倒序打印链表元素 */ void traverse(ListNode head) { @@ -237,6 +242,7 @@ void traverse(ListNode head) { 解法代码如下: + ```java // 记录最大深度 int res = 0; @@ -277,6 +283,7 @@ void traverse(TreeNode root) { 解法代码如下: + ```java // 定义:输入根节点,返回这棵二叉树的最大深度 int maxDepth(TreeNode root) { @@ -302,6 +309,7 @@ int maxDepth(TreeNode root) { 我们熟悉的解法就是用「遍历」的思路,我想应该没什么好说的: + ```java List res = new LinkedList<>(); @@ -335,6 +343,7 @@ void traverse(TreeNode root) { 所以,你可以这样实现前序遍历算法: + ```java // 定义:输入一棵二叉树的根节点,返回这棵树的前序遍历结果 List preorderTraverse(TreeNode root) { @@ -400,6 +409,7 @@ Java 的话无论 ArrayList 还是 LinkedList,`addAll` 方法的复杂度都 第一个问题可以这样写代码: + ```java // 二叉树遍历函数 void traverse(TreeNode root, int level) { @@ -418,6 +428,7 @@ traverse(root, 1); 第二个问题可以这样写代码: + ```java // 定义:输入一棵二叉树,返回这棵二叉树的节点总数 int count(TreeNode root) { @@ -454,40 +465,43 @@ int count(TreeNode root) { 最大深度的算法我们刚才实现过了,上述思路就可以写出以下代码: + ```java -// 记录最大直径的长度 -int maxDiameter = 0; - -public int diameterOfBinaryTree(TreeNode root) { - // 对每个节点计算直径,求最大直径 - traverse(root); - return maxDiameter; -} +class Solution { + // 记录最大直径的长度 + int maxDiameter = 0; + + public int diameterOfBinaryTree(TreeNode root) { + // 对每个节点计算直径,求最大直径 + traverse(root); + return maxDiameter; + } -// 遍历二叉树 -void traverse(TreeNode root) { - if (root == null) { - return; + // 遍历二叉树 + void traverse(TreeNode root) { + if (root == null) { + return; + } + // 对每个节点计算直径 + int leftMax = maxDepth(root.left); + int rightMax = maxDepth(root.right); + int myDiameter = leftMax + rightMax; + // 更新全局最大直径 + maxDiameter = Math.max(maxDiameter, myDiameter); + + traverse(root.left); + traverse(root.right); } - // 对每个节点计算直径 - int leftMax = maxDepth(root.left); - int rightMax = maxDepth(root.right); - int myDiameter = leftMax + rightMax; - // 更新全局最大直径 - maxDiameter = Math.max(maxDiameter, myDiameter); - - traverse(root.left); - traverse(root.right); -} -// 计算二叉树的最大深度 -int maxDepth(TreeNode root) { - if (root == null) { - return 0; + // 计算二叉树的最大深度 + int maxDepth(TreeNode root) { + if (root == null) { + return 0; + } + int leftMax = maxDepth(root.left); + int rightMax = maxDepth(root.right); + return 1 + Math.max(leftMax, rightMax); } - int leftMax = maxDepth(root.left); - int rightMax = maxDepth(root.right); - return 1 + Math.max(leftMax, rightMax); } ``` @@ -499,26 +513,29 @@ int maxDepth(TreeNode root) { 所以,稍微改一下代码逻辑即可得到更好的解法: + ```java -// 记录最大直径的长度 -int maxDiameter = 0; - -public int diameterOfBinaryTree(TreeNode root) { - maxDepth(root); - return maxDiameter; -} +class Solution { + // 记录最大直径的长度 + int maxDiameter = 0; -int maxDepth(TreeNode root) { - if (root == null) { - return 0; + public int diameterOfBinaryTree(TreeNode root) { + maxDepth(root); + return maxDiameter; } - int leftMax = maxDepth(root.left); - int rightMax = maxDepth(root.right); - // 后序位置,顺便计算最大直径 - int myDiameter = leftMax + rightMax; - maxDiameter = Math.max(maxDiameter, myDiameter); - return 1 + Math.max(leftMax, rightMax); + int maxDepth(TreeNode root) { + if (root == null) { + return 0; + } + int leftMax = maxDepth(root.left); + int rightMax = maxDepth(root.right); + // 后序位置,顺便计算最大直径 + int myDiameter = leftMax + rightMax; + maxDiameter = Math.max(maxDiameter, myDiameter); + + return 1 + Math.max(leftMax, rightMax); + } } ``` @@ -538,6 +555,7 @@ int maxDepth(TreeNode root) { 二叉树题型主要是用来培养递归思维的,而层序遍历属于迭代遍历,也比较简单,这里就过一下代码框架吧: + ```java // 输入一棵二叉树的根节点,层序遍历这棵二叉树 void levelTraverse(TreeNode root) { @@ -591,32 +609,36 @@ void levelTraverse(TreeNode root) { 如果你对二叉树足够熟悉,可以想到很多方式通过递归函数得到层序遍历结果,比如下面这种写法: + ```java -List> res = new ArrayList<>(); +class Solution { + List> res = new ArrayList<>(); -List> levelTraverse(TreeNode root) { - if (root == null) { + List> levelTraverse(TreeNode root) { + if (root == null) { + return res; + } + // root 视为第 0 层 + traverse(root, 0); return res; } - // root 视为第 0 层 - traverse(root, 0); - return res; -} -void traverse(TreeNode root, int depth) { - if (root == null) { - return; - } - // 前序位置,看看是否已经存储 depth 层的节点了 - if (res.size() <= depth) { - // 第一次进入 depth 层 - res.add(new LinkedList<>()); + void traverse(TreeNode root, int depth) { + if (root == null) { + return; + } + // 前序位置,看看是否已经存储 depth 层的节点了 + if (res.size() <= depth) { + // 第一次进入 depth 层 + res.add(new LinkedList<>()); + } + // 前序位置,在 depth 层添加 root 节点的值 + res.get(depth).add(root.val); + traverse(root.left, depth + 1); + traverse(root.right, depth + 1); } - // 前序位置,在 depth 层添加 root 节点的值 - res.get(depth).add(root.val); - traverse(root.left, depth + 1); - traverse(root.right, depth + 1); } + ``` 这种思路从结果上说确实可以得到层序遍历结果,但其本质还是二叉树的前序遍历,或者说 DFS 的思路,而不是层序遍历,或者说 BFS 的思路。因为这个解法是依赖前序遍历自顶向下、自左向右的顺序特点得到了正确的结果。 @@ -625,41 +647,45 @@ void traverse(TreeNode root, int depth) { 还有优秀读者评论了这样一种递归进行层序遍历的思路: + ```java -List> res = new LinkedList<>(); +class Solution { -List> levelTraverse(TreeNode root) { - if (root == null) { + List> res = new LinkedList<>(); + + List> levelTraverse(TreeNode root) { + if (root == null) { + return res; + } + List nodes = new LinkedList<>(); + nodes.add(root); + traverse(nodes); return res; } - List nodes = new LinkedList<>(); - nodes.add(root); - traverse(nodes); - return res; -} -void traverse(List curLevelNodes) { - // base case - if (curLevelNodes.isEmpty()) { - return; - } - // 前序位置,计算当前层的值和下一层的节点列表 - List nodeValues = new LinkedList<>(); - List nextLevelNodes = new LinkedList<>(); - for (TreeNode node : curLevelNodes) { - nodeValues.add(node.val); - if (node.left != null) { - nextLevelNodes.add(node.left); + void traverse(List curLevelNodes) { + // base case + if (curLevelNodes.isEmpty()) { + return; } - if (node.right != null) { - nextLevelNodes.add(node.right); + // 前序位置,计算当前层的值和下一层的节点列表 + List nodeValues = new LinkedList<>(); + List nextLevelNodes = new LinkedList<>(); + for (TreeNode node : curLevelNodes) { + nodeValues.add(node.val); + if (node.left != null) { + nextLevelNodes.add(node.left); + } + if (node.right != null) { + nextLevelNodes.add(node.right); + } } + // 前序位置添加结果,可以得到自顶向下的层序遍历 + res.add(nodeValues); + traverse(nextLevelNodes); + // 后序位置添加结果,可以得到自底向上的层序遍历结果 + // res.add(nodeValues); } - // 前序位置添加结果,可以得到自顶向下的层序遍历 - res.add(nodeValues); - traverse(nextLevelNodes); - // 后序位置添加结果,可以得到自底向上的层序遍历结果 - // res.add(nodeValues); } ``` diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\347\263\273\345\210\2271.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\347\263\273\345\210\2271.md" index 30faa79..7458c56 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\347\263\273\345\210\2271.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\347\263\273\345\210\2271.md" @@ -78,6 +78,7 @@ 综上,可以写出如下解法代码: + ```java // 主函数 TreeNode invertTree(TreeNode root) { @@ -112,6 +113,7 @@ void traverse(TreeNode root) { 我们尝试给 `invertTree` 函数赋予一个定义: + ```java // 定义:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点 TreeNode invertTree(TreeNode root); @@ -123,6 +125,7 @@ TreeNode invertTree(TreeNode root); 直接写出解法代码: + ```java // 定义:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点 TreeNode invertTree(TreeNode root) { @@ -154,6 +157,7 @@ TreeNode invertTree(TreeNode root) { 函数签名如下: + ```java Node connect(Node root); ``` @@ -174,6 +178,7 @@ Node connect(Node root); 也许你会模仿上一道题,直接写出如下代码: + ```java // 二叉树遍历函数 void traverse(Node root) { @@ -204,6 +209,7 @@ void traverse(Node root) { 现在,我们只要实现一个 `traverse` 函数来遍历这棵三叉树,每个「三叉树节点」需要做的事就是把自己内部的两个二叉树节点穿起来: + ```java // 主函数 Node connect(Node root) { @@ -244,6 +250,7 @@ void traverse(Node node1, Node node2) { 函数签名如下: + ```java void flatten(TreeNode root); ``` @@ -252,6 +259,7 @@ void flatten(TreeNode root); 乍一看感觉是可以的:对整棵树进行前序遍历,一边遍历一边构造出一条「链表」就行了: + ```java // 虚拟头节点,dummy.right 就是结果 TreeNode dummy = new TreeNode(-1); @@ -279,6 +287,7 @@ void traverse(TreeNode root) { 我们尝试给出 `flatten` 函数的定义: + ```java // 定义:输入节点 root,然后 root 为根的二叉树就会被拉平为一条链表 void flatten(TreeNode root); @@ -298,6 +307,7 @@ void flatten(TreeNode root); 直接看代码实现: + ```java // 定义:将以 root 为根的树拉平为链表 void flatten(TreeNode root) { diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\347\263\273\345\210\2272.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\347\263\273\345\210\2272.md" index 231eaa2..3a5a5f2 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\347\263\273\345\210\2272.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\347\263\273\345\210\2272.md" @@ -53,6 +53,7 @@ 函数签名如下: + ```java TreeNode constructMaximumBinaryTree(int[] nums); ``` @@ -63,6 +64,7 @@ TreeNode constructMaximumBinaryTree(int[] nums); 按照题目给出的例子,输入的数组为 `[3,2,1,6,0,5]`,对于整棵树的根节点来说,其实在做这件事: + ```java TreeNode constructMaximumBinaryTree([3,2,1,6,0,5]) { // 找到数组中的最大值 @@ -76,6 +78,7 @@ TreeNode constructMaximumBinaryTree([3,2,1,6,0,5]) { 再详细一点,就是如下伪码: + ```java TreeNode constructMaximumBinaryTree(int[] nums) { if (nums is empty) return null; @@ -101,6 +104,7 @@ TreeNode constructMaximumBinaryTree(int[] nums) { 明确了思路,我们可以重新写一个辅助函数 `build`,来控制 `nums` 的索引: + ```java /* 主函数 */ TreeNode constructMaximumBinaryTree(int[] nums) { @@ -143,6 +147,7 @@ TreeNode build(int[] nums, int lo, int hi) { 函数签名如下: + ```java TreeNode buildTree(int[] preorder, int[] inorder); ``` @@ -153,6 +158,7 @@ TreeNode buildTree(int[] preorder, int[] inorder); 我们先来回顾一下,前序遍历和中序遍历的结果有什么特点? + ```java void traverse(TreeNode root) { // 前序遍历 @@ -179,6 +185,7 @@ void traverse(TreeNode root) { 换句话说,对于以下代码中的 `?` 部分应该填入什么: + ```java /* 主函数 */ public TreeNode buildTree(int[] preorder, int[] inorder) { @@ -250,6 +257,7 @@ TreeNode build(int[] preorder, int preStart, int preEnd, 现在我们来看图做填空题,下面这几个问号处应该填什么: + ```java root.left = build(preorder, ?, ?, inorder, ?, ?); @@ -262,6 +270,7 @@ root.right = build(preorder, ?, ?, ![](https://labuladong.gitee.io/pictures/二叉树系列2/3.jpeg) + ```java root.left = build(preorder, ?, ?, inorder, inStart, index - 1); @@ -278,6 +287,7 @@ root.right = build(preorder, ?, ?, 看着这个图就可以把 `preorder` 对应的索引写进去了: + ```java int leftSize = index - inStart; @@ -290,6 +300,7 @@ root.right = build(preorder, preStart + leftSize + 1, preEnd, 至此,整个算法思路就完成了,我们再补一补 base case 即可写出解法代码: + ```java TreeNode build(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd) { @@ -327,12 +338,14 @@ TreeNode build(int[] preorder, int preStart, int preEnd, 函数签名如下: + ```java TreeNode buildTree(int[] inorder, int[] postorder); ``` 类似的,看下后序和中序遍历的特点: + ```java void traverse(TreeNode root) { traverse(root.left); @@ -357,6 +370,7 @@ void traverse(TreeNode root) { 整体的算法框架和上一题非常类似,我们依然写一个辅助函数 `build`: + ```java // 存储 inorder 中值到索引的映射 HashMap valToIndex = new HashMap<>(); @@ -399,6 +413,7 @@ TreeNode build(int[] inorder, int inStart, int inEnd, 我们可以按照上图将问号处的索引正确填入: + ```java int leftSize = index - inStart; @@ -411,6 +426,7 @@ root.right = build(inorder, index + 1, inEnd, 综上,可以写出完整的解法代码: + ```java TreeNode build(int[] inorder, int inStart, int inEnd, int[] postorder, int postStart, int postEnd) { @@ -443,6 +459,7 @@ TreeNode build(int[] inorder, int inStart, int inEnd, 函数签名如下: + ```java TreeNode constructFromPrePost(int[] preorder, int[] postorder); ``` @@ -481,6 +498,7 @@ preorder = [1,2,3], postorder = [3,2,1] 详情见代码。 + ```java class Solution { // 存储 postorder 中值到索引的映射 diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\351\230\237\345\210\227.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\351\230\237\345\210\227.md" index 84bcfb7..696cd33 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\351\230\237\345\210\227.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\351\230\237\345\210\227.md" @@ -49,6 +49,7 @@ 函数签名如下: + ```java int[] maxSlidingWindow(int[] nums, int k); ``` @@ -63,6 +64,7 @@ int[] maxSlidingWindow(int[] nums, int k); 在介绍「单调队列」这种数据结构的 API 之前,先来看看一个普通的队列的标准 API: + ```java class Queue { // enqueue 操作,在队尾加入元素 n @@ -74,6 +76,7 @@ class Queue { 我们要实现的「单调队列」的 API 也差不多: + ```java class MonotonicQueue { // 在队尾添加元素 n @@ -87,6 +90,7 @@ class MonotonicQueue { 当然,这几个 API 的实现方法肯定跟一般的 Queue 不一样,不过我们暂且不管,而且认为这几个操作的时间复杂度都是 O(1),先把这道「滑动窗口」问题的解答框架搭出来: + ```java int[] maxSlidingWindow(int[] nums, int k) { MonotonicQueue window = new MonotonicQueue(); @@ -125,6 +129,7 @@ int[] maxSlidingWindow(int[] nums, int k) { 「单调队列」的核心思路和「单调栈」类似,`push` 方法依然在队尾添加元素,但是要把前面比自己小的元素都删掉: + ```java class MonotonicQueue { // 双链表,支持头部和尾部增删元素 @@ -147,19 +152,29 @@ public void push(int n) { 如果每个元素被加入时都这样操作,最终单调队列中的元素大小就会保持一个**单调递减**的顺序,因此我们的 `max` 方法可以可以这样写: + ```java -public int max() { - // 队头的元素肯定是最大的 - return maxq.getFirst(); +class MonotonicQueue { + // 为了节约篇幅,省略上文给出的代码部分... + + public int max() { + // 队头的元素肯定是最大的 + return maxq.getFirst(); + } } ``` `pop` 方法在队头删除元素 `n`,也很好写: + ```java -public void pop(int n) { - if (n == maxq.getFirst()) { - maxq.pollFirst(); +class MonotonicQueue { + // 为了节约篇幅,省略上文给出的代码部分... + + public void pop(int n) { + if (n == maxq.getFirst()) { + maxq.pollFirst(); + } } } ``` @@ -170,6 +185,7 @@ public void pop(int n) { 至此,单调队列设计完毕,看下完整的解题代码: + ```java /* 单调队列的实现 */ class MonotonicQueue { @@ -241,6 +257,7 @@ int[] maxSlidingWindow(int[] nums, int k) { 也就是说,你是否能够实现单调队列的通用实现: + ```java /* 单调队列的通用实现,可以高效维护最大值和最小值 */ class MonotonicQueue> { diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\233\276.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\233\276.md" index c3bfa19..705ab3c 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\233\276.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\233\276.md" @@ -40,6 +40,7 @@ 根据这个逻辑结构,我们可以认为每个节点的实现如下: + ```java /* 图节点的逻辑结构 */ class Vertex { @@ -50,6 +51,7 @@ class Vertex { 看到这个实现,你有没有很熟悉?它和我们之前说的多叉树节点几乎完全一样: + ```java /* 基本的 N 叉树节点 */ class TreeNode { @@ -76,6 +78,7 @@ class TreeNode { 如果用代码的形式来表现,邻接表和邻接矩阵大概长这样: + ```java // 邻接表 // graph[x] 存储 x 的所有邻居节点 @@ -122,6 +125,7 @@ boolean[][] matrix; 如果用代码的形式来表现,大概长这样: + ```java // 邻接表 // graph[x] 存储 x 的所有邻居节点以及对应的权重 @@ -150,6 +154,7 @@ int[][] matrix; 图怎么遍历?还是那句话,参考多叉树,多叉树的 DFS 遍历框架如下: + ```java /* 多叉树遍历框架 */ void traverse(TreeNode root) { @@ -166,6 +171,7 @@ void traverse(TreeNode root) { 所以,如果图包含环,遍历框架就要一个 `visited` 数组进行辅助: + ```java // 记录被遍历过的节点 boolean[] visited; @@ -203,6 +209,7 @@ void traverse(Graph graph, int s) { 他们的区别可以这样反应到代码上: + ```java // DFS 算法,关注点在节点 void traverse(TreeNode root) { @@ -229,6 +236,7 @@ void backtrack(TreeNode root) { 如果执行这段代码,你会发现根节点被漏掉了: + ```java void traverse(TreeNode root) { if (root == null) return; @@ -268,39 +276,42 @@ List> allPathsSourceTarget(int[][] graph); 既然输入的图是无环的,我们就不需要 `visited` 数组辅助了,直接套用图的遍历框架: + ```java -// 记录所有路径 -List> res = new LinkedList<>(); - -public List> allPathsSourceTarget(int[][] graph) { - // 维护递归过程中经过的路径 - LinkedList path = new LinkedList<>(); - traverse(graph, 0, path); - return res; -} - -/* 图的遍历框架 */ -void traverse(int[][] graph, int s, LinkedList path) { - // 添加节点 s 到路径 - path.addLast(s); - - int n = graph.length; - if (s == n - 1) { - // 到达终点 - res.add(new LinkedList<>(path)); - // 可以在这直接 return,但要 removeLast 正确维护 path - // path.removeLast(); - // return; - // 不 return 也可以,因为图中不包含环,不会出现无限递归 +class Solution { + // 记录所有路径 + List> res = new LinkedList<>(); + + public List> allPathsSourceTarget(int[][] graph) { + // 维护递归过程中经过的路径 + LinkedList path = new LinkedList<>(); + traverse(graph, 0, path); + return res; } - // 递归每个相邻节点 - for (int v : graph[s]) { - traverse(graph, v, path); + /* 图的遍历框架 */ + void traverse(int[][] graph, int s, LinkedList path) { + // 添加节点 s 到路径 + path.addLast(s); + + int n = graph.length; + if (s == n - 1) { + // 到达终点 + res.add(new LinkedList<>(path)); + // 可以在这直接 return,但要 removeLast 正确维护 path + // path.removeLast(); + // return; + // 不 return 也可以,因为图中不包含环,不会出现无限递归 + } + + // 递归每个相邻节点 + for (int v : graph[s]) { + traverse(graph, v, path); + } + + // 从路径移出节点 s + path.removeLast(); } - - // 从路径移出节点 s - path.removeLast(); } ``` diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\256\236\347\216\260\350\256\241\347\256\227\345\231\250.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\256\236\347\216\260\350\256\241\347\256\227\345\231\250.md" index 30e25a9..37c96f1 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\256\236\347\216\260\350\256\241\347\256\227\345\231\250.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\256\236\347\216\260\350\256\241\347\256\227\345\231\250.md" @@ -60,6 +60,7 @@ 是的,就是这么一个简单的问题,首先告诉我,怎么把一个字符串形式的**正**整数,转化成 int 型? + ```cpp string s = "458"; @@ -87,6 +88,7 @@ for (int i = 0; i < s.size(); i++) { 我们直接看代码,结合一张图就看明白了: + ```cpp int calculate(string s) { stack stk; @@ -141,6 +143,7 @@ int calculate(string s) { 比如上述例子就可以分解为 `+2`,`-3`,`*4`,`+5` 几对儿,我们刚才不是没有处理乘除号吗,很简单,**其他部分都不用变**,在 `switch` 部分加上对应的 case 就行了: + ```cpp for (int i = 0; i < s.size(); i++) { char c = s[i]; @@ -206,6 +209,7 @@ if ((!isdigit(c) && c != ' ') || i == s.size() - 1) { 为了规避编程语言的繁琐细节,我把前面解法的代码翻译成 Python 版本: + ```python def calculate(s: str) -> int: @@ -252,6 +256,7 @@ calculate(3 * (4 - 5/2) - 6) 现在的问题是,递归的开始条件和结束条件是什么?**遇到 `(` 开始递归,遇到 `)` 结束递归**: + ```python def calculate(s: str) -> int: diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\346\213\223\346\211\221\346\216\222\345\272\217.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\346\213\223\346\211\221\346\216\222\345\272\217.md" index 18b504c..6bcdbb3 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\346\213\223\346\211\221\346\216\222\345\272\217.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\346\213\223\346\211\221\346\216\222\345\272\217.md" @@ -41,6 +41,7 @@ 函数签名如下: + ```java boolean canFinish(int numCourses, int[][] prerequisites); ``` @@ -67,6 +68,7 @@ boolean canFinish(int numCourses, int[][] prerequisites); 以我刷题的经验,常见的存储方式是使用邻接表,比如下面这种结构: + ```java List[] graph; ``` @@ -75,6 +77,7 @@ List[] graph; 所以我们首先可以写一个建图函数: + ```java List[] buildGraph(int numCourses, int[][] prerequisites) { // 图中共有 numCourses 个节点 @@ -98,6 +101,7 @@ List[] buildGraph(int numCourses, int[][] prerequisites) { 前文 [图论基础](https://labuladong.github.io/article/fname.html?fname=图) 写了 DFS 算法遍历图的框架,无非就是从多叉树遍历框架扩展出来的,加了个 `visited` 数组罢了: + ```java // 防止重复遍历同一个节点 boolean[] visited; @@ -118,6 +122,7 @@ void traverse(List[] graph, int s) { 那么我们就可以直接套用这个遍历代码: + ```java // 防止重复遍历同一个节点 boolean[] visited; @@ -148,6 +153,7 @@ void traverse(List[] graph, int s) { 你也可以把 `traverse` 看做在图中节点上游走的指针,只需要再添加一个布尔数组 `onPath` 记录当前 `traverse` 经过的路径: + ```java boolean[] onPath; boolean[] visited; @@ -186,51 +192,55 @@ void traverse(List[] graph, int s) { 这样,就可以在遍历图的过程中顺便判断是否存在环了,完整代码如下: + ```java -// 记录一次递归堆栈中的节点 -boolean[] onPath; -// 记录遍历过的节点,防止走回头路 -boolean[] visited; -// 记录图中是否有环 -boolean hasCycle = false; - -boolean canFinish(int numCourses, int[][] prerequisites) { - List[] graph = buildGraph(numCourses, prerequisites); - - visited = new boolean[numCourses]; - onPath = new boolean[numCourses]; - - for (int i = 0; i < numCourses; i++) { - // 遍历图中的所有节点 - traverse(graph, i); +class Solution { + // 记录一次递归堆栈中的节点 + boolean[] onPath; + // 记录遍历过的节点,防止走回头路 + boolean[] visited; + // 记录图中是否有环 + boolean hasCycle = false; + + boolean canFinish(int numCourses, int[][] prerequisites) { + List[] graph = buildGraph(numCourses, prerequisites); + + visited = new boolean[numCourses]; + onPath = new boolean[numCourses]; + + for (int i = 0; i < numCourses; i++) { + // 遍历图中的所有节点 + traverse(graph, i); + } + // 只要没有循环依赖可以完成所有课程 + return !hasCycle; } - // 只要没有循环依赖可以完成所有课程 - return !hasCycle; -} -void traverse(List[] graph, int s) { - if (onPath[s]) { - // 出现环 - hasCycle = true; - } - - if (visited[s] || hasCycle) { - // 如果已经找到了环,也不用再遍历了 - return; + void traverse(List[] graph, int s) { + if (onPath[s]) { + // 出现环 + hasCycle = true; + } + + if (visited[s] || hasCycle) { + // 如果已经找到了环,也不用再遍历了 + return; + } + // 前序代码位置 + visited[s] = true; + onPath[s] = true; + for (int t : graph[s]) { + traverse(graph, t); + } + // 后序代码位置 + onPath[s] = false; } - // 前序代码位置 - visited[s] = true; - onPath[s] = true; - for (int t : graph[s]) { - traverse(graph, t); + + List[] buildGraph(int numCourses, int[][] prerequisites) { + // 代码见前文 } - // 后序代码位置 - onPath[s] = false; } -List[] buildGraph(int numCourses, int[][] prerequisites) { - // 代码见前文 -} ``` 这道题就解决了,核心就是判断一幅有向图中是否存在环。 @@ -257,6 +267,7 @@ List[] buildGraph(int numCourses, int[][] prerequisites) { 函数签名如下: + ```java int[] findOrder(int numCourses, int[][] prerequisites); ``` @@ -277,6 +288,7 @@ int[] findOrder(int numCourses, int[][] prerequisites); 首先,我们先判断一下题目输入的课程依赖是否成环,成环的话是无法进行拓扑排序的,所以我们可以复用上一道题的主函数: + ```java public int[] findOrder(int numCourses, int[][] prerequisites) { if (!canFinish(numCourses, prerequisites)) { @@ -301,59 +313,63 @@ public int[] findOrder(int numCourses, int[][] prerequisites) { 直接看解法代码吧,在上一题环检测的代码基础上添加了记录后序遍历结果的逻辑: + ```java -// 记录后序遍历结果 -List postorder = new ArrayList<>(); -// 记录是否存在环 -boolean hasCycle = false; -boolean[] visited, onPath; - -// 主函数 -public int[] findOrder(int numCourses, int[][] prerequisites) { - List[] graph = buildGraph(numCourses, prerequisites); - visited = new boolean[numCourses]; - onPath = new boolean[numCourses]; - // 遍历图 - for (int i = 0; i < numCourses; i++) { - traverse(graph, i); - } - // 有环图无法进行拓扑排序 - if (hasCycle) { - return new int[]{}; - } - // 逆后序遍历结果即为拓扑排序结果 - Collections.reverse(postorder); - int[] res = new int[numCourses]; - for (int i = 0; i < numCourses; i++) { - res[i] = postorder.get(i); +class Solution { + // 记录后序遍历结果 + List postorder = new ArrayList<>(); + // 记录是否存在环 + boolean hasCycle = false; + boolean[] visited, onPath; + + // 主函数 + public int[] findOrder(int numCourses, int[][] prerequisites) { + List[] graph = buildGraph(numCourses, prerequisites); + visited = new boolean[numCourses]; + onPath = new boolean[numCourses]; + // 遍历图 + for (int i = 0; i < numCourses; i++) { + traverse(graph, i); + } + // 有环图无法进行拓扑排序 + if (hasCycle) { + return new int[]{}; + } + // 逆后序遍历结果即为拓扑排序结果 + Collections.reverse(postorder); + int[] res = new int[numCourses]; + for (int i = 0; i < numCourses; i++) { + res[i] = postorder.get(i); + } + return res; } - return res; -} -// 图遍历函数 -void traverse(List[] graph, int s) { - if (onPath[s]) { - // 发现环 - hasCycle = true; - } - if (visited[s] || hasCycle) { - return; + // 图遍历函数 + void traverse(List[] graph, int s) { + if (onPath[s]) { + // 发现环 + hasCycle = true; + } + if (visited[s] || hasCycle) { + return; + } + // 前序遍历位置 + onPath[s] = true; + visited[s] = true; + for (int t : graph[s]) { + traverse(graph, t); + } + // 后序遍历位置 + postorder.add(s); + onPath[s] = false; } - // 前序遍历位置 - onPath[s] = true; - visited[s] = true; - for (int t : graph[s]) { - traverse(graph, t); + + // 建图函数 + List[] buildGraph(int numCourses, int[][] prerequisites) { + // 代码见前文 } - // 后序遍历位置 - postorder.add(s); - onPath[s] = false; } -// 建图函数 -List[] buildGraph(int numCourses, int[][] prerequisites) { - // 代码见前文 -} ``` 代码虽然看起来多,但是逻辑应该是很清楚的,只要图中无环,那么我们就调用 `traverse` 函数对图进行 DFS 遍历,记录后序遍历结果,最后把后序遍历结果反转,作为最终的答案。 @@ -362,6 +378,7 @@ List[] buildGraph(int numCourses, int[][] prerequisites) { 我这里也避免数学证明,用一个直观地例子来解释,我们就说二叉树,这是我们说过很多次的二叉树遍历框架: + ```java void traverse(TreeNode root) { // 前序遍历代码位置 @@ -398,6 +415,7 @@ void traverse(TreeNode root) { 先说环检测算法,直接看 BFS 的解法代码: + ```java // 主函数 public boolean canFinish(int numCourses, int[][] prerequisites) { @@ -510,6 +528,7 @@ List[] buildGraph(int n, int[][] edges) { 所以,我们稍微修改一下 BFS 版本的环检测算法,记录节点的遍历顺序即可得到拓扑排序的结果: + ```java // 主函数 public int[] findOrder(int numCourses, int[][] prerequisites) { diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\350\256\276\350\256\241Twitter.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\350\256\276\350\256\241Twitter.md" index 7d8d435..2a6a5da 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\350\256\276\350\256\241Twitter.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\350\256\276\350\256\241Twitter.md" @@ -29,6 +29,7 @@ Twitter 和微博功能差不多,我们主要要实现这样几个 API: + ```java class Twitter { @@ -49,6 +50,7 @@ class Twitter { 举个具体的例子,方便大家理解 API 的具体用法: + ```java Twitter twitter = new Twitter(); @@ -88,6 +90,7 @@ twitter.getNewsFeed(1); 根据刚才的分析,我们需要一个 User 类,储存 user 信息,还需要一个 Tweet 类,储存推文信息,并且要作为链表的节点。所以我们先搭建一下整体的框架: + ```java class Twitter { private static int timestamp = 0; @@ -108,6 +111,7 @@ class Twitter { 根据前面的分析,Tweet 类很容易实现:每个 Tweet 实例需要记录自己的 tweetId 和发表时间 time,而且作为链表节点,要有一个指向下一个节点的 next 指针。 + ```java class Tweet { private int id; @@ -133,6 +137,7 @@ class Tweet { 除此之外,根据面向对象的设计原则,「关注」「取关」和「发文」应该是 User 的行为,况且关注列表和推文列表也存储在 User 类中,所以我们也应该给 User 添加 follow,unfollow 和 post 这几个方法: + ```java // static int timestamp = 0 class User { @@ -172,6 +177,7 @@ class User { **3、几个 API 方法的实现** + ```java class Twitter { private static int timestamp = 0; @@ -239,34 +245,39 @@ while pq not empty: 借助这种牛逼的数据结构支持,我们就很容易实现这个核心功能了。注意我们把优先级队列设为按 time 属性**从大到小降序排列**,因为 time 越大意味着时间越近,应该排在前面: + ```java -public List getNewsFeed(int userId) { - List res = new ArrayList<>(); - if (!userMap.containsKey(userId)) return res; - // 关注列表的用户 Id - Set users = userMap.get(userId).followed; - // 自动通过 time 属性从大到小排序,容量为 users 的大小 - PriorityQueue pq = - new PriorityQueue<>(users.size(), (a, b)->(b.time - a.time)); - - // 先将所有链表头节点插入优先级队列 - for (int id : users) { - Tweet twt = userMap.get(id).head; - if (twt == null) continue; - pq.add(twt); - } +class Twitter { + // 为了节约篇幅,省略上文给出的代码部分... - while (!pq.isEmpty()) { - // 最多返回 10 条就够了 - if (res.size() == 10) break; - // 弹出 time 值最大的(最近发表的) - Tweet twt = pq.poll(); - res.add(twt.id); - // 将下一篇 Tweet 插入进行排序 - if (twt.next != null) - pq.add(twt.next); + public List getNewsFeed(int userId) { + List res = new ArrayList<>(); + if (!userMap.containsKey(userId)) return res; + // 关注列表的用户 Id + Set users = userMap.get(userId).followed; + // 自动通过 time 属性从大到小排序,容量为 users 的大小 + PriorityQueue pq = + new PriorityQueue<>(users.size(), (a, b)->(b.time - a.time)); + + // 先将所有链表头节点插入优先级队列 + for (int id : users) { + Tweet twt = userMap.get(id).head; + if (twt == null) continue; + pq.add(twt); + } + + while (!pq.isEmpty()) { + // 最多返回 10 条就够了 + if (res.size() == 10) break; + // 弹出 time 值最大的(最近发表的) + Tweet twt = pq.poll(); + res.add(twt.id); + // 将下一篇 Tweet 插入进行排序 + if (twt.next != null) + pq.add(twt.next); + } + return res; } - return res; } ``` diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\200\222\345\275\222\345\217\215\350\275\254\351\223\276\350\241\250\347\232\204\344\270\200\351\203\250\345\210\206.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\200\222\345\275\222\345\217\215\350\275\254\351\223\276\350\241\250\347\232\204\344\270\200\351\203\250\345\210\206.md" index 4daac7c..0c7186d 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\200\222\345\275\222\345\217\215\350\275\254\351\223\276\350\241\250\347\232\204\344\270\200\351\203\250\345\210\206.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\200\222\345\275\222\345\217\215\350\275\254\351\223\276\350\241\250\347\232\204\344\270\200\351\203\250\345\210\206.md" @@ -30,6 +30,7 @@ 本文就来由浅入深,step by step 地解决这个问题。如果你还不会递归地反转单链表也没关系,**本文会从递归反转整个单链表开始拓展**,只要你明白单链表的结构,相信你能够有所收获。 + ```java // 单链表节点的结构 public class ListNode { @@ -53,6 +54,7 @@ public class ListNode { 这也是力扣第 206 题「反转链表」,递归反转单链表的算法可能很多读者都听说过,这里详细介绍一下,直接看代码实现: + ```java // 定义:输入一个单链表头结点,将该链表反转,返回新的头结点 ListNode reverse(ListNode head) { @@ -133,6 +135,7 @@ head.next = null; 这次我们实现一个这样的函数: + ```java // 将链表的前 n 个节点反转(n <= 链表长度) ListNode reverseN(ListNode head, int n) @@ -144,6 +147,7 @@ ListNode reverseN(ListNode head, int n) 解决思路和反转整个链表差不多,只要稍加修改即可: + ```java ListNode successor = null; // 后驱节点 @@ -178,12 +182,14 @@ OK,如果这个函数你也能看懂,就离实现「反转一部分链表」 现在解决我们最开始提出的问题,给一个索引区间 `[m, n]`(索引从 1 开始),仅仅反转区间中的链表元素。 + ```java ListNode reverseBetween(ListNode head, int m, int n) ``` 首先,如果 `m == 1`,就相当于反转链表开头的 `n` 个元素嘛,也就是我们刚才实现的功能: + ```java ListNode reverseBetween(ListNode head, int m, int n) { // base case @@ -199,6 +205,7 @@ ListNode reverseBetween(ListNode head, int m, int n) { 区别于迭代思想,这就是递归思想,所以我们可以完成代码: + ```java ListNode reverseBetween(ListNode head, int m, int n) { // base case diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md" index 55a47cb..35a20ae 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md" @@ -33,6 +33,7 @@ 力扣第 232 题「用栈实现队列」让我们实现的 API 如下: + ```java class MyQueue { @@ -54,6 +55,7 @@ class MyQueue { ![](https://labuladong.gitee.io/pictures/栈队列/2.jpg) + ```java class MyQueue { private Stack s1, s2; @@ -70,10 +72,15 @@ class MyQueue { ![](https://labuladong.gitee.io/pictures/栈队列/3.jpg) + ```java -/** 添加元素到队尾 */ -public void push(int x) { - s1.push(x); +class MyQueue { + // 为了节约篇幅,省略上文给出的代码部分... + + /** 添加元素到队尾 */ + public void push(int x) { + s1.push(x); + } } ``` @@ -81,34 +88,49 @@ public void push(int x) { ![](https://labuladong.gitee.io/pictures/栈队列/4.jpg) + ```java -/** 返回队头元素 */ -public int peek() { - if (s2.isEmpty()) - // 把 s1 元素压入 s2 - while (!s1.isEmpty()) - s2.push(s1.pop()); - return s2.peek(); +class MyQueue { + // 为了节约篇幅,省略上文给出的代码部分... + + /** 返回队头元素 */ + public int peek() { + if (s2.isEmpty()) + // 把 s1 元素压入 s2 + while (!s1.isEmpty()) + s2.push(s1.pop()); + return s2.peek(); + } } ``` 同理,对于 `pop` 操作,只要操作 `s2` 就可以了。 + ```java -/** 删除队头的元素并返回 */ -public int pop() { - // 先调用 peek 保证 s2 非空 - peek(); - return s2.pop(); +class MyQueue { + // 为了节约篇幅,省略上文给出的代码部分... + + /** 删除队头的元素并返回 */ + public int pop() { + // 先调用 peek 保证 s2 非空 + peek(); + return s2.pop(); + } } ``` 最后,如何判断队列是否为空呢?如果两个栈都为空的话,就说明队列为空: + ```java -/** 判断队列是否为空 */ -public boolean empty() { - return s1.isEmpty() && s2.isEmpty(); +class MyQueue { + // 为了节约篇幅,省略上文给出的代码部分... + + /** 判断队列是否为空 */ + public boolean empty() { + return s1.isEmpty() && s2.isEmpty(); + } } ``` @@ -126,6 +148,7 @@ public boolean empty() { 力扣第 225 题「用队列实现栈」让我们实现如下 API: + ```java class MyStack { @@ -145,6 +168,7 @@ class MyStack { 先说 `push` API,直接将元素加入队列,同时记录队尾元素,因为队尾元素相当于栈顶元素,如果要 `top` 查看栈顶元素的话可以直接返回: + ```java class MyStack { Queue q = new LinkedList<>(); @@ -172,44 +196,59 @@ class MyStack { ![](https://labuladong.gitee.io/pictures/栈队列/6.jpg) + ```java -/** 删除栈顶的元素并返回 */ -public int pop() { - int size = q.size(); - while (size > 1) { - q.offer(q.poll()); - size--; +class MyStack { + // 为了节约篇幅,省略上文给出的代码部分... + + /** 删除栈顶的元素并返回 */ + public int pop() { + int size = q.size(); + while (size > 1) { + q.offer(q.poll()); + size--; + } + // 之前的队尾元素已经到了队头 + return q.poll(); } - // 之前的队尾元素已经到了队头 - return q.poll(); } ``` 这样实现还有一点小问题就是,原来的队尾元素被提到队头并删除了,但是 `top_elem` 变量没有更新,我们还需要一点小修改: + ```java -/** 删除栈顶的元素并返回 */ -public int pop() { - int size = q.size(); - // 留下队尾 2 个元素 - while (size > 2) { +class MyStack { + // 为了节约篇幅,省略上文给出的代码部分... + + /** 删除栈顶的元素并返回 */ + public int pop() { + int size = q.size(); + // 留下队尾 2 个元素 + while (size > 2) { + q.offer(q.poll()); + size--; + } + // 记录新的队尾元素 + top_elem = q.peek(); q.offer(q.poll()); - size--; + // 删除之前的队尾元素 + return q.poll(); } - // 记录新的队尾元素 - top_elem = q.peek(); - q.offer(q.poll()); - // 删除之前的队尾元素 - return q.poll(); } ``` 最后,API `empty` 就很容易实现了,只要看底层的队列是否为空即可: + ```java -/** 判断栈是否为空 */ -public boolean empty() { - return q.isEmpty(); +class MyStack { + // 为了节约篇幅,省略上文给出的代码部分... + + /** 判断栈是否为空 */ + public boolean empty() { + return q.isEmpty(); + } } ``` diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/BFS\346\241\206\346\236\266.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/BFS\346\241\206\346\236\266.md" index 8b70e04..f6f6701 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/BFS\346\241\206\346\236\266.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/BFS\346\241\206\346\236\266.md" @@ -53,6 +53,7 @@ BFS 相对 DFS 的最主要的区别是:**BFS 找到的路径一定是最短 记住下面这个框架就 OK 了: + ```java // 计算从起点 start 到终点 target 的最近距离 int BFS(Node start, Node target) { @@ -104,6 +105,7 @@ if (cur.left == null && cur.right == null) 那么,按照我们上述的框架稍加改造来写解法即可: + ```java int minDepth(TreeNode root) { if (root == null) return 0; @@ -167,6 +169,13 @@ BFS 可以找到最短距离,但是空间复杂度高,而 DFS 的空间复 ![](https://labuladong.gitee.io/pictures/BFS/title2.jpg) +函数签名如下: + + +```java +int openLock(String[] deadends, String target) +``` + 题目中描述的就是我们生活中常见的那种密码锁,如果没有任何约束,最少的拨动次数很好算,就像我们平时开密码锁那样直奔密码拨就行了。 但现在的难点就在于,不能出现 `deadends`,应该如何计算出最少的转动次数呢? @@ -179,6 +188,7 @@ BFS 可以找到最短距离,但是空间复杂度高,而 DFS 的空间复 **仔细想想,这就可以抽象成一幅图,每个节点有 8 个相邻的节点**,又让你求最短距离,这不就是典型的 BFS 嘛,框架就可以派上用场了,先写出一个「简陋」的 BFS 框架代码再说别的: + ```java // 将 s[j] 向上拨动一次 String plusOne(String s, int j) { @@ -238,6 +248,7 @@ void BFS(String target) { 如果你能够看懂上面那段代码,真得给你鼓掌,只要按照 BFS 框架在对应的位置稍作修改即可修复这些问题: + ```java int openLock(String[] deadends, String target) { // 记录需要跳过的死亡密码 @@ -303,6 +314,7 @@ int openLock(String[] deadends, String target) { **不过,双向 BFS 也有局限,因为你必须知道终点在哪里**。比如我们刚才讨论的二叉树最小高度的问题,你一开始根本就不知道终点在哪里,也就无法使用双向 BFS;但是第二个密码锁的问题,是可以使用双向 BFS 算法来提高效率的,代码稍加修改即可: + ```java int openLock(String[] deadends, String target) { Set deads = new HashSet<>(); @@ -357,6 +369,7 @@ int openLock(String[] deadends, String target) { 其实双向 BFS 还有一个优化,就是在 while 循环开始时做一个判断: + ```java // ... while (!q1.isEmpty() && !q2.isEmpty()) { diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\350\257\246\350\247\243.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\350\257\246\350\247\243.md" index b104adc..f6ab9b1 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\350\257\246\350\247\243.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\350\257\246\350\247\243.md" @@ -47,6 +47,7 @@ 现在我们的 Union-Find 算法主要需要实现这两个 API: + ```java class UF { /* 将 p 和 q 连接 */ @@ -86,6 +87,7 @@ class UF { ![](https://labuladong.gitee.io/pictures/unionfind/3.jpg) + ```java class UF { // 记录连通分量 @@ -111,29 +113,34 @@ class UF { ![](https://labuladong.gitee.io/pictures/unionfind/4.jpg) + ```java -public void union(int p, int q) { - int rootP = find(p); - int rootQ = find(q); - if (rootP == rootQ) - return; - // 将两棵树合并为一棵 - parent[rootP] = rootQ; - // parent[rootQ] = rootP 也一样 - count--; // 两个分量合二为一 -} +class UF { + // 为了节约篇幅,省略上文给出的代码部分... -/* 返回某个节点 x 的根节点 */ -private int find(int x) { - // 根节点的 parent[x] == x - while (parent[x] != x) - x = parent[x]; - return x; -} + public void union(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + if (rootP == rootQ) + return; + // 将两棵树合并为一棵 + parent[rootP] = rootQ; + // parent[rootQ] = rootP 也一样 + count--; // 两个分量合二为一 + } + + /* 返回某个节点 x 的根节点 */ + private int find(int x) { + // 根节点的 parent[x] == x + while (parent[x] != x) + x = parent[x]; + return x; + } -/* 返回当前的连通分量个数 */ -public int count() { - return count; + /* 返回当前的连通分量个数 */ + public int count() { + return count; + } } ``` @@ -141,11 +148,16 @@ public int count() { ![](https://labuladong.gitee.io/pictures/unionfind/5.jpg) + ```java -public boolean connected(int p, int q) { - int rootP = find(p); - int rootQ = find(q); - return rootP == rootQ; +class UF { + // 为了节约篇幅,省略上文给出的代码部分... + + public boolean connected(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + return rootP == rootQ; + } } ``` @@ -165,16 +177,22 @@ public boolean connected(int p, int q) { 我们要知道哪种情况下可能出现不平衡现象,关键在于 `union` 过程: + ```java -public void union(int p, int q) { - int rootP = find(p); - int rootQ = find(q); - if (rootP == rootQ) - return; - // 将两棵树合并为一棵 - parent[rootP] = rootQ; - // parent[rootQ] = rootP 也可以 - count--; +class UF { + // 为了节约篇幅,省略上文给出的代码部分... + + public void union(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + if (rootP == rootQ) + return; + // 将两棵树合并为一棵 + parent[rootP] = rootQ; + // parent[rootQ] = rootP 也可以 + count--; + } +} ``` 我们一开始就是简单粗暴的把 `p` 所在的树接到 `q` 所在的树的根节点下面,那么这里就可能出现「头重脚轻」的不平衡状况,比如下面这种局面: @@ -183,6 +201,7 @@ public void union(int p, int q) { 长此以往,树可能生长得很不平衡。**我们其实是希望,小一些的树接到大一些的树下面,这样就能避免头重脚轻,更平衡一些**。解决方法是额外使用一个 `size` 数组,记录每棵树包含的节点数,我们不妨称为「重量」: + ```java class UF { private int count; @@ -207,23 +226,29 @@ class UF { 比如说 `size[3] = 5` 表示,以节点 `3` 为根的那棵树,总共有 `5` 个节点。这样我们可以修改一下 `union` 方法: + ```java -public void union(int p, int q) { - int rootP = find(p); - int rootQ = find(q); - if (rootP == rootQ) - return; - - // 小树接到大树下面,较平衡 - if (size[rootP] > size[rootQ]) { - parent[rootQ] = rootP; - size[rootP] += size[rootQ]; - } else { - parent[rootP] = rootQ; - size[rootQ] += size[rootP]; +class UF { + // 为了节约篇幅,省略上文给出的代码部分... + + public void union(int p, int q) { + int rootP = find(p); + int rootQ = find(q); + if (rootP == rootQ) + return; + + // 小树接到大树下面,较平衡 + if (size[rootP] > size[rootQ]) { + parent[rootQ] = rootP; + size[rootP] += size[rootQ]; + } else { + parent[rootP] = rootQ; + size[rootQ] += size[rootP]; + } + count--; } - count--; } + ``` 这样,通过比较树的重量,就可以保证树的生长相对平衡,树的高度大致在 `logN` 这个数量级,极大提升执行效率。 @@ -246,14 +271,19 @@ public void union(int p, int q) { 第一种是在 `find` 中加一行代码: + ```java -private int find(int x) { - while (parent[x] != x) { - // 这行代码进行路径压缩 - parent[x] = parent[parent[x]]; - x = parent[x]; +class UF { + // 为了节约篇幅,省略上文给出的代码部分... + + private int find(int x) { + while (parent[x] != x) { + // 这行代码进行路径压缩 + parent[x] = parent[parent[x]]; + x = parent[x]; + } + return x; } - return x; } ``` @@ -265,20 +295,27 @@ private int find(int x) { 路径压缩的第二种写法是这样: + ```java -// 第二种路径压缩的 find 方法 -public int find(int x) { - if (parent[x] != x) { - parent[x] = find(parent[x]); +class UF { + // 为了节约篇幅,省略上文给出的代码部分... + + // 第二种路径压缩的 find 方法 + public int find(int x) { + if (parent[x] != x) { + parent[x] = find(parent[x]); + } + return parent[x]; } - return parent[x]; } + ``` 我一度认为这种递归写法和第一种迭代写法做的事情一样,但实际上是我大意了,有读者指出这种写法进行路径压缩的效率是高于上一种解法的。 这个递归过程有点不好理解,你可以自己手画一下递归过程。我把这个函数做的事情翻译成迭代形式,方便你理解它进行路径压缩的原理: + ```java // 这段迭代代码方便你理解递归代码所做的事情 public int find(int x) { @@ -306,6 +343,7 @@ public int find(int x) { **另外,如果使用路径压缩技巧,那么 `size` 数组的平衡优化就不是特别必要了**。所以你一般看到的 Union Find 算法应该是如下实现: + ```java class UF { // 连通分量个数 @@ -376,12 +414,14 @@ Union-Find 算法的复杂度可以这样分析:构造函数初始化数据结 函数签名如下: + ```java int countComponents(int n, int[][] edges) ``` 这道题我们可以直接套用 `UF` 类来解决: + ```java public int countComponents(int n, int[][] edges) { UF uf = new UF(n); @@ -404,6 +444,7 @@ class UF { 给你一个 M×N 的二维矩阵,其中包含字符 `X` 和 `O`,让你找到矩阵中**四面**被 `X` 围住的 `O`,并且把它们替换成 `X`。 + ```java void solve(char[][] board); ``` @@ -434,6 +475,7 @@ void solve(char[][] board); 看解法代码: + ```java void solve(char[][] board) { if (board.length == 0) return; @@ -497,6 +539,7 @@ class UF { **核心思想是,将 `equations` 中的算式根据 `==` 和 `!=` 分成两部分,先处理 `==` 算式,使得他们通过相等关系各自勾结成门派(连通分量);然后处理 `!=` 算式,检查不等关系是否破坏了相等关系的连通性**。 + ```java boolean equationsPossible(String[] equations) { // 26 个英文字母 diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\350\257\246\350\247\243.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\350\257\246\350\247\243.md" index f30b871..bafcb1d 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\350\257\246\350\247\243.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\350\257\246\350\247\243.md" @@ -45,6 +45,7 @@ ### 零、二分查找框架 + ```java int binarySearch(int[] nums, int target) { int left = 0, right = ...; @@ -73,6 +74,7 @@ int binarySearch(int[] nums, int target) { 这个场景是最简单的,可能也是大家最熟悉的,即搜索一个数,如果存在,返回其索引,否则返回 -1。 + ```java int binarySearch(int[] nums, int target) { int left = 0; @@ -146,6 +148,7 @@ int binarySearch(int[] nums, int target) { 以下是最常见的代码形式,其中的标记是需要注意的细节: + ```java int left_bound(int[] nums, int target) { int left = 0; @@ -218,6 +221,7 @@ return nums[left] == target ? left : -1; 因为你非要让搜索区间两端都闭,所以 `right` 应该初始化为 `nums.length - 1`,while 的终止条件应该是 `left == right + 1`,也就是其中应该用 `<=`: + ```java int left_bound(int[] nums, int target) { // 搜索区间为 [left, right] @@ -230,6 +234,7 @@ int left_bound(int[] nums, int target) { 因为搜索区间是两端都闭的,且现在是搜索左侧边界,所以 `left` 和 `right` 的更新逻辑如下: + ```java if (nums[mid] < target) { // 搜索区间变为 [mid+1, right] @@ -245,6 +250,7 @@ if (nums[mid] < target) { 和刚才相同,如果想在找不到 `target` 的时候返回 -1,那么检查一下 `nums[left]` 和 `target` 是否相等即可: + ```java // 此时 target 比所有数都大,返回 -1 if (left == nums.length) return -1; @@ -254,6 +260,7 @@ return nums[left] == target ? left : -1; 至此,整个算法就写完了,完整代码如下: + ```java int left_bound(int[] nums, int target) { int left = 0, right = nums.length - 1; @@ -285,6 +292,7 @@ int left_bound(int[] nums, int target) { 类似寻找左侧边界的算法,这里也会提供两种写法,还是先写常见的左闭右开的写法,只有两处和搜索左侧边界不同: + ```java int right_bound(int[] nums, int target) { int left = 0, right = nums.length; @@ -339,6 +347,7 @@ if (nums[mid] == target) { 类似之前的左侧边界搜索,`left` 的取值范围是 `[0, nums.length]`,但由于我们最后返回的是 `left - 1`,所以 `left` 取值为 0 的时候会造成索引越界,额外处理一下即可正确地返回 -1: + ```java while (left < right) { // ... @@ -354,6 +363,7 @@ return nums[left - 1] == target ? (left - 1) : -1; 答:当然可以,类似搜索左侧边界的统一写法,其实只要改两个地方就行了: + ```java int right_bound(int[] nums, int target) { int left = 0, right = nums.length - 1; @@ -427,6 +437,7 @@ int right_bound(int[] nums, int target) { 对于寻找左右边界的二分搜索,常见的手法是使用左闭右开的「搜索区间」,**我们还根据逻辑将「搜索区间」全都统一成了两端都闭,便于记忆,只要修改两处即可变化出三种写法**: + ```java int binary_search(int[] nums, int target) { int left = 0, right = nums.length - 1; diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\211\215\347\274\200\345\222\214\346\212\200\345\267\247.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\211\215\347\274\200\345\222\214\346\212\200\345\267\247.md" index 482fd0b..1079198 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\211\215\347\274\200\345\222\214\346\212\200\345\267\247.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\211\215\347\274\200\345\222\214\346\212\200\345\267\247.md" @@ -35,6 +35,7 @@ 题目要求你实现这样一个类: + ```java class NumArray { @@ -47,6 +48,7 @@ class NumArray { `sumRange` 函数需要计算并返回一个索引区间之内的元素和,没学过前缀和的人可能写出如下代码: + ```java class NumArray { @@ -72,6 +74,7 @@ class NumArray { 直接看代码实现: + ```java class NumArray { // 前缀和数组 @@ -144,6 +147,7 @@ for (int i = 1; i < count.length; i++) 那么做这道题更好的思路和一维数组中的前缀和是非常类似的,我们可以维护一个二维 `preSum` 数组,专门记录以原点为顶点的矩阵的元素之和,就可以用几次加减运算算出任何一个子矩阵的元素和: + ```java class NumMatrix { // 定义:preSum[i][j] 记录 matrix 中子矩阵 [0, 0, i-1, j-1] 的元素和 diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\217\214\346\214\207\351\222\210\346\212\200\345\267\247.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\217\214\346\214\207\351\222\210\346\212\200\345\267\247.md" index 35839fd..389b5ce 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\217\214\346\214\207\351\222\210\346\212\200\345\267\247.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\217\214\346\214\207\351\222\210\346\212\200\345\267\247.md" @@ -49,6 +49,7 @@ 函数签名如下: + ```java int removeDuplicates(int[] nums); ``` @@ -69,6 +70,7 @@ int removeDuplicates(int[] nums); 看代码: + ```java int removeDuplicates(int[] nums) { if (nums.length == 0) { @@ -96,6 +98,7 @@ int removeDuplicates(int[] nums) { 其实和数组去重是一模一样的,唯一的区别是把数组赋值操作变成操作指针而已,你对照着之前的代码来看: + ```java ListNode deleteDuplicates(ListNode head) { if (head == null) return null; @@ -134,6 +137,7 @@ ListNode deleteDuplicates(ListNode head) { 函数签名如下: + ```java int removeElement(int[] nums, int val); ``` @@ -144,6 +148,7 @@ int removeElement(int[] nums, int val); 这和前面说到的数组去重问题解法思路是完全一样的,就不画 GIF 了,直接看代码: + ```java int removeElement(int[] nums, int val) { int fast = 0, slow = 0; @@ -164,6 +169,7 @@ int removeElement(int[] nums, int val) { 给你输入一个数组 `nums`,请你**原地修改**,将数组中的所有值为 0 的元素移到数组末尾,函数签名如下: + ```java void moveZeroes(int[] nums); ``` @@ -176,6 +182,7 @@ void moveZeroes(int[] nums); 所以我们可以复用上一题的 `removeElement` 函数: + ```java void moveZeroes(int[] nums) { // 去除 nums 中的所有 0,返回不含 0 的数组长度 @@ -194,6 +201,7 @@ int removeElement(int[] nums, int val); 我在另一篇文章 [滑动窗口算法核心框架详解](https://labuladong.github.io/article/fname.html?fname=滑动窗口技巧进阶) 给出了滑动窗口的代码框架: + ```cpp /* 滑动窗口算法框架 */ void slidingWindow(string s, string t) { @@ -228,6 +236,7 @@ void slidingWindow(string s, string t) { 我在另一篇文章 [二分查找框架详解](https://labuladong.github.io/article/fname.html?fname=二分查找详解) 中有详细探讨二分搜索代码的细节问题,这里只写最简单的二分算法,旨在突出它的双指针特性: + ```java int binarySearch(int[] nums, int target) { // 一左一右两个指针相向而行 @@ -253,6 +262,7 @@ int binarySearch(int[] nums, int target) { 只要数组有序,就应该想到双指针技巧。这道题的解法有点类似二分查找,通过调节 `left` 和 `right` 就可以调整 `sum` 的大小: + ```java int[] twoSum(int[] nums, int target) { // 一左一右两个指针相向而行 @@ -278,6 +288,7 @@ int[] twoSum(int[] nums, int target) { 一般编程语言都会提供 `reverse` 函数,其实这个函数的原理非常简单,力扣第 344 题「反转字符串」就是类似的需求,让你反转一个 `char[]` 类型的字符数组,我们直接看代码吧: + ```java void reverseString(char[] s) { // 一左一右两个指针相向而行 @@ -301,6 +312,7 @@ void reverseString(char[] s) { 现在你应该能感觉到回文串问题和左右指针肯定有密切的联系,比如让你判断一个字符串是不是回文串,你可以写出下面这段代码: + ```java boolean isPalindrome(String s) { // 一左一右两个指针相向而行 @@ -324,6 +336,7 @@ boolean isPalindrome(String s) { 函数签名如下: + ```java String longestPalindrome(String s); ``` @@ -332,6 +345,7 @@ String longestPalindrome(String s); 如果回文串的长度为奇数,则它有一个中心字符;如果回文串的长度为偶数,则可以认为它有两个中心字符。所以我们可以先实现这样一个函数: + ```java // 在 s 中寻找以 s[l] 和 s[r] 为中心的最长回文串 String palindrome(String s, int l, int r) { @@ -359,6 +373,7 @@ for 0 <= i < len(s): 翻译成代码,就可以解决最长回文子串这个问题: + ```java String longestPalindrome(String s) { String res = ""; diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\233\236\346\272\257\347\256\227\346\263\225\350\257\246\350\247\243\344\277\256\350\256\242\347\211\210.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\233\236\346\272\257\347\256\227\346\263\225\350\257\246\350\247\243\344\277\256\350\256\242\347\211\210.md" index 959f8ee..2ff38fd 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\233\236\346\272\257\347\256\227\346\263\225\350\257\246\350\247\243\344\277\256\350\256\242\347\211\210.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\233\236\346\272\257\347\256\227\346\263\225\350\257\246\350\247\243\344\277\256\350\256\242\347\211\210.md" @@ -67,7 +67,7 @@ def backtrack(路径, 选择列表): 力扣第 46 题「全排列」就是给你输入一个数组 `nums`,让你返回这些数字的全排列。 -> info**:我们这次讨论的全排列问题不包含重复的数字,包含重复数字的扩展场景我在后文 [回溯算法秒杀排列组合子集的九种题型](https://labuladong.github.io/article/fname.html?fname=子集排列组合) 中讲解**。 +> info:**我们这次讨论的全排列问题不包含重复的数字,包含重复数字的扩展场景我在后文 [回溯算法秒杀排列组合子集的九种题型](https://labuladong.github.io/article/fname.html?fname=子集排列组合) 中讲解**。 我们在高中的时候就做过排列组合的数学题,我们也知道 `n` 个不重复的数,全排列共有 `n!` 个。那么我们当时是怎么穷举全排列的呢? @@ -97,6 +97,7 @@ def backtrack(路径, 选择列表): 再进一步,如何遍历一棵树?这个应该不难吧。回忆一下之前 [学习数据结构的框架思维](https://labuladong.github.io/article/fname.html?fname=学习数据结构和算法的高效方法) 写过,各种搜索问题其实都是树的遍历问题,而多叉树的遍历框架就是这样: + ```java void traverse(TreeNode root) { for (TreeNode child : root.childern) { @@ -138,44 +139,47 @@ for 选择 in 选择列表: 下面,直接看全排列代码: + ```java -List> res = new LinkedList<>(); - -/* 主函数,输入一组不重复的数字,返回它们的全排列 */ -List> permute(int[] nums) { - // 记录「路径」 - LinkedList track = new LinkedList<>(); - // 「路径」中的元素会被标记为 true,避免重复使用 - boolean[] used = new boolean[nums.length]; - - backtrack(nums, track, used); - return res; -} - -// 路径:记录在 track 中 -// 选择列表:nums 中不存在于 track 的那些元素(used[i] 为 false) -// 结束条件:nums 中的元素全都在 track 中出现 -void backtrack(int[] nums, LinkedList track, boolean[] used) { - // 触发结束条件 - if (track.size() == nums.length) { - res.add(new LinkedList(track)); - return; +class Solution { + List> res = new LinkedList<>(); + + /* 主函数,输入一组不重复的数字,返回它们的全排列 */ + List> permute(int[] nums) { + // 记录「路径」 + LinkedList track = new LinkedList<>(); + // 「路径」中的元素会被标记为 true,避免重复使用 + boolean[] used = new boolean[nums.length]; + + backtrack(nums, track, used); + return res; } - - for (int i = 0; i < nums.length; i++) { - // 排除不合法的选择 - if (used[i]) { - // nums[i] 已经在 track 中,跳过 - continue; + + // 路径:记录在 track 中 + // 选择列表:nums 中不存在于 track 的那些元素(used[i] 为 false) + // 结束条件:nums 中的元素全都在 track 中出现 + void backtrack(int[] nums, LinkedList track, boolean[] used) { + // 触发结束条件 + if (track.size() == nums.length) { + res.add(new LinkedList(track)); + return; + } + + for (int i = 0; i < nums.length; i++) { + // 排除不合法的选择 + if (used[i]) { + // nums[i] 已经在 track 中,跳过 + continue; + } + // 做选择 + track.add(nums[i]); + used[i] = true; + // 进入下一层决策树 + backtrack(nums, track, used); + // 取消选择 + track.removeLast(); + used[i] = false; } - // 做选择 - track.add(nums[i]); - used[i] = true; - // 进入下一层决策树 - backtrack(nums, track, used); - // 取消选择 - track.removeLast(); - used[i] = false; } } ``` @@ -194,6 +198,7 @@ void backtrack(int[] nums, LinkedList track, boolean[] used) { 力扣第 51 题「N 皇后」就是这个经典问题,简单解释一下:给你一个 `N×N` 的棋盘,让你放置 `N` 个皇后,使得它们不能互相攻击,请你计算出所有可能的放法。函数签名如下: + ```cpp vector> solveNQueens(int n); ``` @@ -215,46 +220,51 @@ vector> solveNQueens(int n); 因为 C++ 代码对字符串的操作方便一些,所以这道题我用 C++ 来写解法,直接套用回溯算法框架: + ```cpp -vector> res; - -/* 输入棋盘边长 n,返回所有合法的放置 */ -vector> solveNQueens(int n) { - // vector 代表一个棋盘 - // '.' 表示空,'Q' 表示皇后,初始化空棋盘 - vector board(n, string(n, '.')); - backtrack(board, 0); - return res; -} - -// 路径:board 中小于 row 的那些行都已经成功放置了皇后 -// 选择列表:第 row 行的所有列都是放置皇后的选择 -// 结束条件:row 超过 board 的最后一行 -void backtrack(vector& board, int row) { - // 触发结束条件 - if (row == board.size()) { - res.push_back(board); - return; +class Solution { +public: + vector> res; + + /* 输入棋盘边长 n,返回所有合法的放置 */ + vector> solveNQueens(int n) { + // vector 代表一个棋盘 + // '.' 表示空,'Q' 表示皇后,初始化空棋盘 + vector board(n, string(n, '.')); + backtrack(board, 0); + return res; } - - int n = board[row].size(); - for (int col = 0; col < n; col++) { - // 排除不合法选择 - if (!isValid(board, row, col)) { - continue; + + // 路径:board 中小于 row 的那些行都已经成功放置了皇后 + // 选择列表:第 row 行的所有列都是放置皇后的选择 + // 结束条件:row 超过 board 的最后一行 + void backtrack(vector& board, int row) { + // 触发结束条件 + if (row == board.size()) { + res.push_back(board); + return; + } + + int n = board[row].size(); + for (int col = 0; col < n; col++) { + // 排除不合法选择 + if (!isValid(board, row, col)) { + continue; + } + // 做选择 + board[row][col] = 'Q'; + // 进入下一行决策 + backtrack(board, row + 1); + // 撤销选择 + board[row][col] = '.'; } - // 做选择 - board[row][col] = 'Q'; - // 进入下一行决策 - backtrack(board, row + 1); - // 撤销选择 - board[row][col] = '.'; } } ``` 这部分主要代码,其实跟全排列问题差不多,`isValid` 函数的实现也很简单: + ```cpp /* 是否可以在 board[row][col] 放置皇后? */ bool isValid(vector& board, int row, int col) { @@ -302,6 +312,7 @@ bool isValid(vector& board, int row, int col) { 其实特别简单,只要稍微修改一下回溯算法的代码,用一个外部变量记录是否找到答案,找到答案后就停止继续递归即可: + ```cpp bool found = false; // 函数找到一个答案后就返回 true @@ -339,6 +350,7 @@ bool backtrack(vector& board, int row) { 更好的办法就是直接把 `res` 变量变成 int 类型,每次在 base case 找到一个合法答案的时候递增 `res` 变量即可: + ```cpp // 仅仅记录合法结果的数量 int res = 0; diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\246\344\271\240\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225\347\232\204\351\253\230\346\225\210\346\226\271\346\263\225.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\246\344\271\240\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225\347\232\204\351\253\230\346\225\210\346\226\271\346\263\225.md" index 6c9cd84..6dc2116 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\246\344\271\240\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225\347\232\204\351\253\230\346\225\210\346\226\271\346\263\225.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\246\344\271\240\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225\347\232\204\351\253\230\346\225\210\346\226\271\346\263\225.md" @@ -59,6 +59,7 @@ 数组遍历框架,典型的线性迭代结构: + ```java void traverse(int[] arr) { for (int i = 0; i < arr.length; i++) { @@ -69,6 +70,7 @@ void traverse(int[] arr) { 链表遍历框架,兼具迭代和递归结构: + ```java /* 基本的单链表节点 */ class ListNode { @@ -90,6 +92,7 @@ void traverse(ListNode head) { 二叉树遍历框架,典型的非线性递归遍历结构: + ```java /* 基本的二叉树节点 */ class TreeNode { @@ -107,6 +110,7 @@ void traverse(TreeNode root) { 二叉树框架可以扩展为 N 叉树的遍历框架: + ```java /* 基本的 N 叉树节点 */ class TreeNode { @@ -146,6 +150,7 @@ void traverse(TreeNode root) { **不要小看这几行破代码,几乎所有二叉树的题目都是一套这个框架就出来了**: + ```java void traverse(TreeNode root) { // 前序位置 @@ -160,6 +165,7 @@ void traverse(TreeNode root) { 力扣第 124 题,难度困难,让你求二叉树中最大路径和,主要代码如下: + ```java int res = Integer.MIN_VALUE; int oneSideMax(TreeNode root) { @@ -176,6 +182,7 @@ int oneSideMax(TreeNode root) { 力扣第 105 题,难度中等,让你根据前序遍历和中序遍历的结果还原一棵二叉树,很经典的问题吧,主要代码如下: + ```java TreeNode build(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd) { @@ -207,6 +214,7 @@ TreeNode build(int[] preorder, int preStart, int preEnd, 力扣第 230 题,难度中等,寻找二叉搜索树中的第 `k` 小的元素,主要代码如下: + ```java int res = 0; int rank = 0; @@ -240,6 +248,7 @@ void traverse(TreeNode root, int k) { ![](https://labuladong.gitee.io/pictures/动态规划详解进阶/5.jpg) + ```java int dp(int[] coins, int amount) { // base case @@ -260,6 +269,7 @@ int dp(int[] coins, int amount) { 这么多代码看不懂咋办?直接提取出框架,就能看出核心思路了: + ```python # 不过是一个 N 叉树的遍历问题而已 int dp(int amount) { @@ -279,6 +289,7 @@ int dp(int amount) { 全排列算法的主要代码如下: + ```java void backtrack(int[] nums, LinkedList track) { if (track.size() == nums.length) { @@ -299,6 +310,7 @@ void backtrack(int[] nums, LinkedList track) { 看不懂?没关系,把其中的递归部分抽取出来: + ```java /* 提取出 N 叉树遍历框架 */ void backtrack(int[] nums, LinkedList track) { diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\267\256\345\210\206\346\212\200\345\267\247.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\267\256\345\210\206\346\212\200\345\267\247.md" index 8772e36..75aa02f 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\267\256\345\210\206\346\212\200\345\267\247.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\267\256\345\210\206\346\212\200\345\267\247.md" @@ -27,6 +27,7 @@ 没看过前文没关系,这里简单介绍一下前缀和,核心代码就是下面这段: + ```java class PrefixSum { // 前缀和数组 @@ -62,6 +63,7 @@ class PrefixSum { 这里就需要差分数组的技巧,类似前缀和技巧构造的 `prefix` 数组,我们先对 `nums` 数组构造一个 `diff` 差分数组,**`diff[i]` 就是 `nums[i]` 和 `nums[i-1]` 之差**: + ```java int[] diff = new int[nums.length]; // 构造差分数组 @@ -75,6 +77,7 @@ for (int i = 1; i < nums.length; i++) { 通过这个 `diff` 差分数组是可以反推出原始数组 `nums` 的,代码逻辑如下: + ```java int[] res = new int[diff.length]; // 根据差分数组构造结果数组 @@ -94,6 +97,7 @@ for (int i = 1; i < diff.length; i++) { 现在我们把差分数组抽象成一个类,包含 `increment` 方法和 `result` 方法: + ```java // 差分数组工具类 class Difference { @@ -135,6 +139,7 @@ class Difference { 这里注意一下 `increment` 方法中的 if 语句: + ```java public void increment(int i, int j, int val) { diff[i] += val; @@ -154,6 +159,7 @@ public void increment(int i, int j, int val) { 那么我们直接复用刚才实现的 `Difference` 类就能把这道题解决掉: + ```java int[] getModifiedArray(int length, int[][] updates) { // nums 初始化为全 0 @@ -178,6 +184,7 @@ int[] getModifiedArray(int length, int[][] updates) { 函数签名如下: + ```java int[] corpFlightBookings(int[][] bookings, int n) ``` @@ -190,6 +197,7 @@ int[] corpFlightBookings(int[][] bookings, int n) 这么一看,不就是一道标准的差分数组题嘛?我们可以直接复用刚才写的类: + ```java int[] corpFlightBookings(int[][] bookings, int n) { // nums 初始化为全 0 @@ -218,6 +226,7 @@ int[] corpFlightBookings(int[][] bookings, int n) { 函数签名如下: + ```java boolean carPooling(int[][] trips, int capacity); ``` @@ -240,6 +249,7 @@ trips = [[2,1,5],[3,3,7]], capacity = 4 车站编号从 0 开始,最多到 1000,也就是最多有 1001 个车站,那么我们的差分数组长度可以直接设置为 1001,这样索引刚好能够涵盖所有车站的编号: + ```java boolean carPooling(int[][] trips, int capacity) { // 最多有 1001 个车站 diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\273\221\345\212\250\347\252\227\345\217\243\346\212\200\345\267\247\350\277\233\351\230\266.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\273\221\345\212\250\347\252\227\345\217\243\346\212\200\345\267\247\350\277\233\351\230\266.md" index dfd732f..2ead401 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\273\221\345\212\250\347\252\227\345\217\243\346\212\200\345\267\247\350\277\233\351\230\266.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\273\221\345\212\250\347\252\227\345\217\243\346\212\200\345\267\247\350\277\233\351\230\266.md" @@ -63,6 +63,7 @@ while (right < s.size()) { **所以今天我就写一套滑动窗口算法的代码框架,我连再哪里做输出 debug 都给你写好了,以后遇到相关的问题,你就默写出来如下框架然后改三个地方就行,还不会出 bug**: + ```cpp /* 滑动窗口算法框架 */ void slidingWindow(string s) { @@ -206,6 +207,7 @@ while (right < s.size()) { 下面是完整代码: + ```cpp string minWindow(string s, string t) { unordered_map need, window; @@ -276,6 +278,7 @@ string minWindow(string s, string t) { 首先,先复制粘贴之前的算法框架代码,然后明确刚才提出的几个问题,即可写出这道题的答案: + ```cpp // 判断 s 中是否存在 t 的排列 bool checkInclusion(string t, string s) { @@ -334,6 +337,7 @@ bool checkInclusion(string t, string s) { 直接默写一下框架,明确刚才讲的 4 个问题,即可秒杀这道题: + ```cpp vector findAnagrams(string s, string t) { unordered_map need, window; @@ -380,6 +384,7 @@ vector findAnagrams(string s, string t) { 这个题终于有了点新意,不是一套框架就出答案,不过反而更简单了,稍微改一改框架就行了: + ```cpp int lengthOfLongestSubstring(string s) { unordered_map window; diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\350\212\261\345\274\217\351\201\215\345\216\206.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\350\212\261\345\274\217\351\201\215\345\216\206.md" index abf5722..81f0f36 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\350\212\261\345\274\217\351\201\215\345\216\206.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\350\212\261\345\274\217\351\201\215\345\216\206.md" @@ -40,6 +40,7 @@ 题目很好理解,就是让你将一个二维矩阵顺时针旋转 90 度,**难点在于要「原地」修改**,函数签名如下: + ```java void rotate(int[][] matrix) ``` @@ -100,6 +101,7 @@ s = "labuladong world hello" 将上述思路翻译成代码,即可解决本题: + ```java // 将二维矩阵原地顺时针旋转 90 度 public void rotate(int[][] matrix) { @@ -145,6 +147,7 @@ void reverse(int[] arr) { 翻译成代码如下: + ```java // 将二维矩阵原地逆时针旋转 90 度 void rotate2(int[][] matrix) { @@ -179,6 +182,7 @@ void reverse(int[] arr) { /* 见上文 */} 函数签名如下: + ```java List spiralOrder(int[][] matrix) ``` @@ -193,6 +197,7 @@ List spiralOrder(int[][] matrix) 只要有了这个思路,翻译出代码就很容易了: + ```java List spiralOrder(int[][] matrix) { int m = matrix.length, n = matrix[0].length; @@ -247,12 +252,14 @@ List spiralOrder(int[][] matrix) { 函数签名如下: + ```java int[][] generateMatrix(int n) ``` 有了上面的铺垫,稍微改一下代码即可完成这道题: + ```java int[][] generateMatrix(int n) { int[][] matrix = new int[n][n]; diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/LRU\347\256\227\346\263\225.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/LRU\347\256\227\346\263\225.md" index e752942..628e196 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/LRU\347\256\227\346\263\225.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/LRU\347\256\227\346\263\225.md" @@ -52,6 +52,7 @@ LRU 缓存淘汰算法就是一种常用策略。LRU 的全称是 Least Recently 注意哦,`get` 和 `put` 方法必须都是 `O(1)` 的时间复杂度,我们举个具体例子来看看 LRU 算法怎么工作。 + ```java /* 缓存容量为 2 */ LRUCache cache = new LRUCache(2); @@ -121,6 +122,7 @@ LRU 缓存算法的核心数据结构就是哈希链表,双向链表和哈希 首先,我们把双链表的节点类写出来,为了简化,`key` 和 `val` 都认为是 int 类型: + ```java class Node { public int key, val; @@ -134,6 +136,7 @@ class Node { 然后依靠我们的 `Node` 类型构建一个双链表,实现几个 LRU 算法必须的 API: + ```java class DoubleList { // 头尾虚节点 @@ -188,6 +191,7 @@ class DoubleList { 有了双向链表的实现,我们只需要在 LRU 算法中把它和哈希表结合起来即可,先搭出代码框架: + ```java class LRUCache { // key -> Node(key, val) @@ -210,42 +214,48 @@ class LRUCache { 说的有点玄幻,实际上很简单,就是尽量让 LRU 的主方法 `get` 和 `put` 避免直接操作 `map` 和 `cache` 的细节。我们可以先实现下面几个函数: + ```java -/* 将某个 key 提升为最近使用的 */ -private void makeRecently(int key) { - Node x = map.get(key); - // 先从链表中删除这个节点 - cache.remove(x); - // 重新插到队尾 - cache.addLast(x); -} +class LRUCache { + // 为了节约篇幅,省略上文给出的代码部分... -/* 添加最近使用的元素 */ -private void addRecently(int key, int val) { - Node x = new Node(key, val); - // 链表尾部就是最近使用的元素 - cache.addLast(x); - // 别忘了在 map 中添加 key 的映射 - map.put(key, x); -} + /* 将某个 key 提升为最近使用的 */ + private void makeRecently(int key) { + Node x = map.get(key); + // 先从链表中删除这个节点 + cache.remove(x); + // 重新插到队尾 + cache.addLast(x); + } -/* 删除某一个 key */ -private void deleteKey(int key) { - Node x = map.get(key); - // 从链表中删除 - cache.remove(x); - // 从 map 中删除 - map.remove(key); -} + /* 添加最近使用的元素 */ + private void addRecently(int key, int val) { + Node x = new Node(key, val); + // 链表尾部就是最近使用的元素 + cache.addLast(x); + // 别忘了在 map 中添加 key 的映射 + map.put(key, x); + } -/* 删除最久未使用的元素 */ -private void removeLeastRecently() { - // 链表头部的第一个元素就是最久未使用的 - Node deletedNode = cache.removeFirst(); - // 同时别忘了从 map 中删除它的 key - int deletedKey = deletedNode.key; - map.remove(deletedKey); + /* 删除某一个 key */ + private void deleteKey(int key) { + Node x = map.get(key); + // 从链表中删除 + cache.remove(x); + // 从 map 中删除 + map.remove(key); + } + + /* 删除最久未使用的元素 */ + private void removeLeastRecently() { + // 链表头部的第一个元素就是最久未使用的 + Node deletedNode = cache.removeFirst(); + // 同时别忘了从 map 中删除它的 key + int deletedKey = deletedNode.key; + map.remove(deletedKey); + } } + ``` 这里就能回答之前的问答题「为什么要在链表中同时存储 key 和 val,而不是只存储 val」,注意 `removeLeastRecently` 函数中,我们需要用 `deletedNode` 得到 `deletedKey`。 @@ -254,14 +264,19 @@ private void removeLeastRecently() { 上述方法就是简单的操作封装,调用这些函数可以避免直接操作 `cache` 链表和 `map` 哈希表,下面我先来实现 LRU 算法的 `get` 方法: + ```java -public int get(int key) { - if (!map.containsKey(key)) { - return -1; +class LRUCache { + // 为了节约篇幅,省略上文给出的代码部分... + + public int get(int key) { + if (!map.containsKey(key)) { + return -1; + } + // 将该数据提升为最近使用的 + makeRecently(key); + return map.get(key).val; } - // 将该数据提升为最近使用的 - makeRecently(key); - return map.get(key).val; } ``` @@ -271,27 +286,33 @@ public int get(int key) { 这样我们可以轻松写出 `put` 方法的代码: + ```java -public void put(int key, int val) { - if (map.containsKey(key)) { - // 删除旧的数据 - deleteKey(key); - // 新插入的数据为最近使用的数据 - addRecently(key, val); - return; - } +class LRUCache { + // 为了节约篇幅,省略上文给出的代码部分... - if (cap == cache.size()) { - // 删除最久未使用的元素 - removeLeastRecently(); + public void put(int key, int val) { + if (map.containsKey(key)) { + // 删除旧的数据 + deleteKey(key); + // 新插入的数据为最近使用的数据 + addRecently(key, val); + return; + } + + if (cap == cache.size()) { + // 删除最久未使用的元素 + removeLeastRecently(); + } + // 添加为最近使用的元素 + addRecently(key, val); } - // 添加为最近使用的元素 - addRecently(key, val); } ``` 至此,你应该已经完全掌握 LRU 算法的原理和实现了,我们最后用 Java 的内置类型 `LinkedHashMap` 来实现 LRU 算法,逻辑和之前完全一致,我就不过多解释了: + ```java class LRUCache { int cap; diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/k\344\270\252\344\270\200\347\273\204\345\217\215\350\275\254\351\223\276\350\241\250.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/k\344\270\252\344\270\200\347\273\204\345\217\215\350\275\254\351\223\276\350\241\250.md" index 0c30ce4..04d99ba 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/k\344\270\252\344\270\200\347\273\204\345\217\215\350\275\254\351\223\276\350\241\250.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/k\344\270\252\344\270\200\347\273\204\345\217\215\350\275\254\351\223\276\350\241\250.md" @@ -69,6 +69,7 @@ 首先,我们要实现一个 `reverse` 函数反转一个区间之内的元素。在此之前我们再简化一下,给定链表头结点,如何反转整个链表? + ```java // 反转以 a 为头结点的链表 ListNode reverse(ListNode a) { @@ -97,6 +98,7 @@ ListNode reverse(ListNode a) { 只要更改函数签名,并把上面的代码中 `null` 改成 `b` 即可: + ```java /** 反转区间 [a, b) 的元素,注意是左闭右开 */ ListNode reverse(ListNode a, ListNode b) { @@ -116,6 +118,7 @@ ListNode reverse(ListNode a, ListNode b) { 现在我们迭代实现了反转部分链表的功能,接下来就按照之前的逻辑编写 `reverseKGroup` 函数即可: + ```java ListNode reverseKGroup(ListNode head, int k) { if (head == null) return null; diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\350\277\220\347\224\250.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\350\277\220\347\224\250.md" index d9870fd..24638ac 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\350\277\220\347\224\250.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\350\277\220\347\224\250.md" @@ -46,6 +46,7 @@ 「搜索左侧边界」的二分搜索算法的具体代码实现如下: + ```java // 搜索左侧边界 int left_bound(int[] nums, int target) { @@ -75,6 +76,7 @@ int left_bound(int[] nums, int target) { 「搜索右侧边界」的二分搜索算法的具体代码实现如下: + ```java // 搜索右侧边界 int right_bound(int[] nums, int target) { diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\220\215\344\272\272\351\227\256\351\242\230.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\220\215\344\272\272\351\227\256\351\242\230.md" index fd0f148..580de5c 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\220\215\344\272\272\351\227\256\351\242\230.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\220\215\344\272\272\351\227\256\351\242\230.md" @@ -59,6 +59,7 @@ 函数签名如下: + ```java int findCelebrity(int[][] graph); ``` @@ -71,6 +72,7 @@ int findCelebrity(int[][] graph); 力扣第 277 题「搜寻名人」就是这个经典问题,不过并不是直接把邻接矩阵传给你,而是只告诉你总人数 `n`,同时提供一个 API `knows` 来查询人和人之间的社交关系: + ```java // 可以直接调用,能够返回 i 是否认识 j boolean knows(int i, int j); @@ -87,6 +89,7 @@ int findCelebrity(int n) { 我们拍拍脑袋就能写出一个简单粗暴的算法: + ```java int findCelebrity(int n) { for (int cand = 0; cand < n; cand++) { @@ -159,6 +162,7 @@ int findCelebrity(int n) { 综上,只要观察任意两个之间的关系,就至少能确定一个人不是名人,上述情况判断可以用如下代码表示: + ```java if (knows(cand, other) || !knows(other, cand)) { // cand 不可能是名人 @@ -173,6 +177,7 @@ if (knows(cand, other) || !knows(other, cand)) { 这个思路的完整代码如下: + ```java int findCelebrity(int n) { if (n == 1) return 0; @@ -221,6 +226,7 @@ int findCelebrity(int n) { 如果你能够理解上面的优化解法,其实可以不需要额外的空间解决这个问题,代码如下: + ```java int findCelebrity(int n) { // 先假设 cand 是名人 diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\255\220\351\233\206\346\216\222\345\210\227\347\273\204\345\220\210.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\255\220\351\233\206\346\216\222\345\210\227\347\273\204\345\220\210.md" index 8f5014c..681ddf3 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\255\220\351\233\206\346\216\222\345\210\227\347\273\204\345\220\210.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\255\220\351\233\206\346\216\222\345\210\227\347\273\204\345\220\210.md" @@ -80,6 +80,7 @@ 函数签名如下: + ```java List> subsets(int[] nums) ``` @@ -128,31 +129,35 @@ List> subsets(int[] nums) 直接看代码: + ```java -List> res = new LinkedList<>(); -// 记录回溯算法的递归路径 -LinkedList track = new LinkedList<>(); +class Solution { -// 主函数 -public List> subsets(int[] nums) { - backtrack(nums, 0); - return res; -} + List> res = new LinkedList<>(); + // 记录回溯算法的递归路径 + LinkedList track = new LinkedList<>(); -// 回溯算法核心函数,遍历子集问题的回溯树 -void backtrack(int[] nums, int start) { + // 主函数 + public List> subsets(int[] nums) { + backtrack(nums, 0); + return res; + } - // 前序位置,每个节点的值都是一个子集 - res.add(new LinkedList<>(track)); - - // 回溯算法标准框架 - for (int i = start; i < nums.length; i++) { - // 做选择 - track.addLast(nums[i]); - // 通过 start 参数控制树枝的遍历,避免产生重复的子集 - backtrack(nums, i + 1); - // 撤销选择 - track.removeLast(); + // 回溯算法核心函数,遍历子集问题的回溯树 + void backtrack(int[] nums, int start) { + + // 前序位置,每个节点的值都是一个子集 + res.add(new LinkedList<>(track)); + + // 回溯算法标准框架 + for (int i = start; i < nums.length; i++) { + // 做选择 + track.addLast(nums[i]); + // 通过 start 参数控制树枝的遍历,避免产生重复的子集 + backtrack(nums, i + 1); + // 撤销选择 + track.removeLast(); + } } } ``` @@ -181,6 +186,7 @@ void backtrack(int[] nums, int start) { 函数签名如下: + ```java List> combine(int n, int k) ``` @@ -201,33 +207,37 @@ List> combine(int n, int k) 反映到代码上,只需要稍改 base case,控制算法仅仅收集第 `k` 层节点的值即可: + ```java -List> res = new LinkedList<>(); -// 记录回溯算法的递归路径 -LinkedList track = new LinkedList<>(); +class Solution { -// 主函数 -public List> combine(int n, int k) { - backtrack(1, n, k); - return res; -} + List> res = new LinkedList<>(); + // 记录回溯算法的递归路径 + LinkedList track = new LinkedList<>(); -void backtrack(int start, int n, int k) { - // base case - if (k == track.size()) { - // 遍历到了第 k 层,收集当前节点的值 - res.add(new LinkedList<>(track)); - return; + // 主函数 + public List> combine(int n, int k) { + backtrack(1, n, k); + return res; } - - // 回溯算法标准框架 - for (int i = start; i <= n; i++) { - // 选择 - track.addLast(i); - // 通过 start 参数控制树枝的遍历,避免产生重复的子集 - backtrack(i + 1, n, k); - // 撤销选择 - track.removeLast(); + + void backtrack(int start, int n, int k) { + // base case + if (k == track.size()) { + // 遍历到了第 k 层,收集当前节点的值 + res.add(new LinkedList<>(track)); + return; + } + + // 回溯算法标准框架 + for (int i = start; i <= n; i++) { + // 选择 + track.addLast(i); + // 通过 start 参数控制树枝的遍历,避免产生重复的子集 + backtrack(i + 1, n, k); + // 撤销选择 + track.removeLast(); + } } } ``` @@ -244,6 +254,7 @@ void backtrack(int start, int n, int k) { 函数签名如下: + ```java List> permute(int[] nums) ``` @@ -268,43 +279,47 @@ List> permute(int[] nums) 我们用 `used` 数组标记已经在路径上的元素避免重复选择,然后收集所有叶子节点上的值,就是所有全排列的结果: + ```java -List> res = new LinkedList<>(); -// 记录回溯算法的递归路径 -LinkedList track = new LinkedList<>(); -// track 中的元素会被标记为 true -boolean[] used; - -/* 主函数,输入一组不重复的数字,返回它们的全排列 */ -public List> permute(int[] nums) { - used = new boolean[nums.length]; - backtrack(nums); - return res; -} +class Solution { -// 回溯算法核心函数 -void backtrack(int[] nums) { - // base case,到达叶子节点 - if (track.size() == nums.length) { - // 收集叶子节点上的值 - res.add(new LinkedList(track)); - return; + List> res = new LinkedList<>(); + // 记录回溯算法的递归路径 + LinkedList track = new LinkedList<>(); + // track 中的元素会被标记为 true + boolean[] used; + + /* 主函数,输入一组不重复的数字,返回它们的全排列 */ + public List> permute(int[] nums) { + used = new boolean[nums.length]; + backtrack(nums); + return res; } - // 回溯算法标准框架 - for (int i = 0; i < nums.length; i++) { - // 已经存在 track 中的元素,不能重复选择 - if (used[i]) { - continue; + // 回溯算法核心函数 + void backtrack(int[] nums) { + // base case,到达叶子节点 + if (track.size() == nums.length) { + // 收集叶子节点上的值 + res.add(new LinkedList(track)); + return; + } + + // 回溯算法标准框架 + for (int i = 0; i < nums.length; i++) { + // 已经存在 track 中的元素,不能重复选择 + if (used[i]) { + continue; + } + // 做选择 + used[i] = true; + track.addLast(nums[i]); + // 进入下一层回溯树 + backtrack(nums); + // 取消选择 + track.removeLast(); + used[i] = false; } - // 做选择 - used[i] = true; - track.addLast(nums[i]); - // 进入下一层回溯树 - backtrack(nums); - // 取消选择 - track.removeLast(); - used[i] = false; } } ``` @@ -315,6 +330,7 @@ void backtrack(int[] nums) { 也很简单,改下 `backtrack` 函数的 base case,仅收集第 `k` 层的节点值即可: + ```java // 回溯算法核心函数 void backtrack(int[] nums, int k) { @@ -344,6 +360,7 @@ void backtrack(int[] nums, int k) { 函数签名如下: + ```java List> subsetsWithDup(int[] nums) ``` @@ -377,29 +394,33 @@ List> subsetsWithDup(int[] nums) **体现在代码上,需要先进行排序,让相同的元素靠在一起,如果发现 `nums[i] == nums[i-1]`,则跳过**: + ```java -List> res = new LinkedList<>(); -LinkedList track = new LinkedList<>(); +class Solution { -public List> subsetsWithDup(int[] nums) { - // 先排序,让相同的元素靠在一起 - Arrays.sort(nums); - backtrack(nums, 0); - return res; -} + List> res = new LinkedList<>(); + LinkedList track = new LinkedList<>(); -void backtrack(int[] nums, int start) { - // 前序位置,每个节点的值都是一个子集 - res.add(new LinkedList<>(track)); - - for (int i = start; i < nums.length; i++) { - // 剪枝逻辑,值相同的相邻树枝,只遍历第一条 - if (i > start && nums[i] == nums[i - 1]) { - continue; + public List> subsetsWithDup(int[] nums) { + // 先排序,让相同的元素靠在一起 + Arrays.sort(nums); + backtrack(nums, 0); + return res; + } + + void backtrack(int[] nums, int start) { + // 前序位置,每个节点的值都是一个子集 + res.add(new LinkedList<>(track)); + + for (int i = start; i < nums.length; i++) { + // 剪枝逻辑,值相同的相邻树枝,只遍历第一条 + if (i > start && nums[i] == nums[i - 1]) { + continue; + } + track.addLast(nums[i]); + backtrack(nums, i + 1); + track.removeLast(); } - track.addLast(nums[i]); - backtrack(nums, i + 1); - track.removeLast(); } } ``` @@ -420,49 +441,53 @@ void backtrack(int[] nums, int start) { 对比子集问题的解法,只要额外用一个 `trackSum` 变量记录回溯路径上的元素和,然后将 base case 改一改即可解决这道题: + ```java -List> res = new LinkedList<>(); -// 记录回溯的路径 -LinkedList track = new LinkedList<>(); -// 记录 track 中的元素之和 -int trackSum = 0; - -public List> combinationSum2(int[] candidates, int target) { - if (candidates.length == 0) { +class Solution { + + List> res = new LinkedList<>(); + // 记录回溯的路径 + LinkedList track = new LinkedList<>(); + // 记录 track 中的元素之和 + int trackSum = 0; + + public List> combinationSum2(int[] candidates, int target) { + if (candidates.length == 0) { + return res; + } + // 先排序,让相同的元素靠在一起 + Arrays.sort(candidates); + backtrack(candidates, 0, target); return res; } - // 先排序,让相同的元素靠在一起 - Arrays.sort(candidates); - backtrack(candidates, 0, target); - return res; -} -// 回溯算法主函数 -void backtrack(int[] nums, int start, int target) { - // base case,达到目标和,找到符合条件的组合 - if (trackSum == target) { - res.add(new LinkedList<>(track)); - return; - } - // base case,超过目标和,直接结束 - if (trackSum > target) { - return; - } + // 回溯算法主函数 + void backtrack(int[] nums, int start, int target) { + // base case,达到目标和,找到符合条件的组合 + if (trackSum == target) { + res.add(new LinkedList<>(track)); + return; + } + // base case,超过目标和,直接结束 + if (trackSum > target) { + return; + } - // 回溯算法标准框架 - for (int i = start; i < nums.length; i++) { - // 剪枝逻辑,值相同的树枝,只遍历第一条 - if (i > start && nums[i] == nums[i - 1]) { - continue; + // 回溯算法标准框架 + for (int i = start; i < nums.length; i++) { + // 剪枝逻辑,值相同的树枝,只遍历第一条 + if (i > start && nums[i] == nums[i - 1]) { + continue; + } + // 做选择 + track.add(nums[i]); + trackSum += nums[i]; + // 递归遍历下一层回溯树 + backtrack(nums, i + 1, target); + // 撤销选择 + track.removeLast(); + trackSum -= nums[i]; } - // 做选择 - track.add(nums[i]); - trackSum += nums[i]; - // 递归遍历下一层回溯树 - backtrack(nums, i + 1, target); - // 撤销选择 - track.removeLast(); - trackSum -= nums[i]; } } ``` @@ -473,6 +498,7 @@ void backtrack(int[] nums, int start, int target) { 给你输入一个可包含重复数字的序列 `nums`,请你写一个算法,返回所有可能的全排列,函数签名如下: + ```java List> permuteUnique(int[] nums) ``` @@ -485,38 +511,42 @@ List> permuteUnique(int[] nums) 先看解法代码: + ```java -List> res = new LinkedList<>(); -LinkedList track = new LinkedList<>(); -boolean[] used; - -public List> permuteUnique(int[] nums) { - // 先排序,让相同的元素靠在一起 - Arrays.sort(nums); - used = new boolean[nums.length]; - backtrack(nums); - return res; -} +class Solution { -void backtrack(int[] nums) { - if (track.size() == nums.length) { - res.add(new LinkedList(track)); - return; + List> res = new LinkedList<>(); + LinkedList track = new LinkedList<>(); + boolean[] used; + + public List> permuteUnique(int[] nums) { + // 先排序,让相同的元素靠在一起 + Arrays.sort(nums); + used = new boolean[nums.length]; + backtrack(nums); + return res; } - for (int i = 0; i < nums.length; i++) { - if (used[i]) { - continue; + void backtrack(int[] nums) { + if (track.size() == nums.length) { + res.add(new LinkedList(track)); + return; } - // 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置 - if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) { - continue; + + for (int i = 0; i < nums.length; i++) { + if (used[i]) { + continue; + } + // 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置 + if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) { + continue; + } + track.add(nums[i]); + used[i] = true; + backtrack(nums); + track.removeLast(); + used[i] = false; } - track.add(nums[i]); - used[i] = true; - backtrack(nums); - track.removeLast(); - used[i] = false; } } ``` @@ -600,6 +630,7 @@ if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) { 当然,关于排列去重,也有读者提出别的剪枝思路: + ```java void backtrack(int[] nums, LinkedList track) { if (track.size() == nums.length) { @@ -668,6 +699,7 @@ List> combinationSum(int[] candidates, int target) 答案在于 `backtrack` 递归时输入的参数 `start`: + ```java // 无重组合的回溯算法框架 void backtrack(int[] nums, int start) { @@ -686,6 +718,7 @@ void backtrack(int[] nums, int start) { 那么反过来,如果我想让每个元素被重复使用,我只要把 `i + 1` 改成 `i` 即可: + ```java // 可重组合的回溯算法框架 void backtrack(int[] nums, int start) { @@ -706,44 +739,48 @@ void backtrack(int[] nums, int start) { 这道题的解法代码如下: + ```java -List> res = new LinkedList<>(); -// 记录回溯的路径 -LinkedList track = new LinkedList<>(); -// 记录 track 中的路径和 -int trackSum = 0; - -public List> combinationSum(int[] candidates, int target) { - if (candidates.length == 0) { +class Solution { + + List> res = new LinkedList<>(); + // 记录回溯的路径 + LinkedList track = new LinkedList<>(); + // 记录 track 中的路径和 + int trackSum = 0; + + public List> combinationSum(int[] candidates, int target) { + if (candidates.length == 0) { + return res; + } + backtrack(candidates, 0, target); return res; } - backtrack(candidates, 0, target); - return res; -} -// 回溯算法主函数 -void backtrack(int[] nums, int start, int target) { - // base case,找到目标和,记录结果 - if (trackSum == target) { - res.add(new LinkedList<>(track)); - return; - } - // base case,超过目标和,停止向下遍历 - if (trackSum > target) { - return; - } + // 回溯算法主函数 + void backtrack(int[] nums, int start, int target) { + // base case,找到目标和,记录结果 + if (trackSum == target) { + res.add(new LinkedList<>(track)); + return; + } + // base case,超过目标和,停止向下遍历 + if (trackSum > target) { + return; + } - // 回溯算法标准框架 - for (int i = start; i < nums.length; i++) { - // 选择 nums[i] - trackSum += nums[i]; - track.add(nums[i]); - // 递归遍历下一层回溯树 - // 同一元素可重复使用,注意参数 - backtrack(nums, i, target); - // 撤销选择 nums[i] - trackSum -= nums[i]; - track.removeLast(); + // 回溯算法标准框架 + for (int i = start; i < nums.length; i++) { + // 选择 nums[i] + trackSum += nums[i]; + track.add(nums[i]); + // 递归遍历下一层回溯树 + // 同一元素可重复使用,注意参数 + backtrack(nums, i, target); + // 撤销选择 nums[i] + trackSum -= nums[i]; + track.removeLast(); + } } } ``` @@ -766,32 +803,36 @@ void backtrack(int[] nums, int start, int target) { 那这个问题就简单了,代码如下: + ```java -List> res = new LinkedList<>(); -LinkedList track = new LinkedList<>(); +class Solution { -public List> permuteRepeat(int[] nums) { - backtrack(nums); - return res; -} + List> res = new LinkedList<>(); + LinkedList track = new LinkedList<>(); -// 回溯算法核心函数 -void backtrack(int[] nums) { - // base case,到达叶子节点 - if (track.size() == nums.length) { - // 收集叶子节点上的值 - res.add(new LinkedList(track)); - return; + public List> permuteRepeat(int[] nums) { + backtrack(nums); + return res; } - // 回溯算法标准框架 - for (int i = 0; i < nums.length; i++) { - // 做选择 - track.add(nums[i]); - // 进入下一层回溯树 - backtrack(nums); - // 取消选择 - track.removeLast(); + // 回溯算法核心函数 + void backtrack(int[] nums) { + // base case,到达叶子节点 + if (track.size() == nums.length) { + // 收集叶子节点上的值 + res.add(new LinkedList(track)); + return; + } + + // 回溯算法标准框架 + for (int i = 0; i < nums.length; i++) { + // 做选择 + track.add(nums[i]); + // 进入下一层回溯树 + backtrack(nums); + // 取消选择 + track.removeLast(); + } } } ``` @@ -806,6 +847,7 @@ void backtrack(int[] nums) { **形式一、元素无重不可复选,即 `nums` 中的元素都是唯一的,每个元素最多只能被使用一次**,`backtrack` 核心代码如下: + ```java /* 组合/子集问题回溯算法框架 */ void backtrack(int[] nums, int start) { @@ -841,6 +883,7 @@ void backtrack(int[] nums) { **形式二、元素可重不可复选,即 `nums` 中的元素可以存在重复,每个元素最多只能被使用一次**,其关键在于排序和剪枝,`backtrack` 核心代码如下: + ```java Arrays.sort(nums); /* 组合/子集问题回溯算法框架 */ @@ -887,6 +930,7 @@ void backtrack(int[] nums) { **形式三、元素无重可复选,即 `nums` 中的元素都是唯一的,每个元素可以被使用若干次**,只要删掉去重逻辑即可,`backtrack` 核心代码如下: + ```java /* 组合/子集问题回溯算法框架 */ void backtrack(int[] nums, int start) { diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\256\211\346\216\222\344\274\232\350\256\256\345\256\244.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\256\211\346\216\222\344\274\232\350\256\256\345\256\244.md" index 23c8028..4f4c3fb 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\256\211\346\216\222\344\274\232\350\256\256\345\256\244.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\256\211\346\216\222\344\274\232\350\256\256\345\256\244.md" @@ -128,6 +128,7 @@ int minMeetingRooms(int[][] meetings); ![](https://labuladong.gitee.io/pictures/安排会议室/3.jpeg) + ```java int minMeetingRooms(int[][] meetings) { int n = meetings.length; diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\351\232\217\346\234\272\346\235\203\351\207\215.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\351\232\217\346\234\272\346\235\203\351\207\215.md" index 024b498..4dac72d 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\351\232\217\346\234\272\346\235\203\351\207\215.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\351\232\217\346\234\272\346\235\203\351\207\215.md" @@ -116,6 +116,7 @@ 所以要这样写代码: + ```java int n = preSum.length; // target 取值范围是闭区间 [1, preSum[n - 1]] @@ -126,6 +127,7 @@ int target = rand.nextInt(preSum[n - 1]) + 1; 实际上应该使用搜索左侧边界的二分搜索: + ```java // 搜索左侧边界的二分搜索 int left_bound(int[] nums, int target) { @@ -161,6 +163,7 @@ int left_bound(int[] nums, int target) { 综上,我们可以写出最终解法代码: + ```java class Solution { // 前缀和数组 -- GitLab