提交 70088737 编写于 作者: L labuladong

update content

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