Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
Debris丶
Fucking Algorithm
提交
70088737
F
Fucking Algorithm
项目概览
Debris丶
/
Fucking Algorithm
与 Fork 源项目一致
从无法访问的项目Fork
通知
1
Star
1
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
DevOps
流水线
流水线任务
计划
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
F
Fucking Algorithm
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
DevOps
DevOps
流水线
流水线任务
计划
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
流水线任务
提交
Issue看板
前往新版Gitcode,体验更适合开发者的 AI 搜索 >>
提交
70088737
编写于
3月 05, 2023
作者:
L
labuladong
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
update content
上级
06350ad8
变更
46
显示空白变更内容
内联
并排
Showing
46 changed file
with
784 addition
and
247 deletion
+784
-247
README.md
README.md
+0
-1
动态规划系列/动态规划之博弈问题.md
动态规划系列/动态规划之博弈问题.md
+4
-0
动态规划系列/动态规划之四键键盘.md
动态规划系列/动态规划之四键键盘.md
+4
-0
动态规划系列/动态规划之正则表达.md
动态规划系列/动态规划之正则表达.md
+2
-0
动态规划系列/动态规划设计:最长递增子序列.md
动态规划系列/动态规划设计:最长递增子序列.md
+4
-0
动态规划系列/动态规划详解进阶.md
动态规划系列/动态规划详解进阶.md
+19
-7
动态规划系列/单词拼接.md
动态规划系列/单词拼接.md
+69
-45
动态规划系列/团灭股票问题.md
动态规划系列/团灭股票问题.md
+11
-0
动态规划系列/抢房子.md
动态规划系列/抢房子.md
+1
-0
动态规划系列/最优子结构.md
动态规划系列/最优子结构.md
+6
-0
动态规划系列/状态压缩技巧.md
动态规划系列/状态压缩技巧.md
+5
-1
动态规划系列/编辑距离.md
动态规划系列/编辑距离.md
+15
-7
动态规划系列/贪心算法之区间调度问题.md
动态规划系列/贪心算法之区间调度问题.md
+6
-0
动态规划系列/魔塔.md
动态规划系列/魔塔.md
+16
-7
技术/刷题技巧.md
技术/刷题技巧.md
+6
-0
数据结构系列/BST1.md
数据结构系列/BST1.md
+7
-0
数据结构系列/BST2.md
数据结构系列/BST2.md
+9
-0
数据结构系列/dijkstra算法.md
数据结构系列/dijkstra算法.md
+24
-0
数据结构系列/二叉堆详解实现优先级队列.md
数据结构系列/二叉堆详解实现优先级队列.md
+26
-4
数据结构系列/二叉树总结.md
数据结构系列/二叉树总结.md
+48
-22
数据结构系列/二叉树系列1.md
数据结构系列/二叉树系列1.md
+10
-0
数据结构系列/二叉树系列2.md
数据结构系列/二叉树系列2.md
+18
-0
数据结构系列/单调队列.md
数据结构系列/单调队列.md
+19
-2
数据结构系列/图.md
数据结构系列/图.md
+17
-6
数据结构系列/实现计算器.md
数据结构系列/实现计算器.md
+5
-0
数据结构系列/拓扑排序.md
数据结构系列/拓扑排序.md
+45
-26
数据结构系列/设计Twitter.md
数据结构系列/设计Twitter.md
+12
-1
数据结构系列/递归反转链表的一部分.md
数据结构系列/递归反转链表的一部分.md
+7
-0
数据结构系列/队列实现栈栈实现队列.md
数据结构系列/队列实现栈栈实现队列.md
+53
-14
算法思维系列/BFS框架.md
算法思维系列/BFS框架.md
+13
-0
算法思维系列/UnionFind算法详解.md
算法思维系列/UnionFind算法详解.md
+56
-13
算法思维系列/二分查找详解.md
算法思维系列/二分查找详解.md
+11
-0
算法思维系列/前缀和技巧.md
算法思维系列/前缀和技巧.md
+4
-0
算法思维系列/双指针技巧.md
算法思维系列/双指针技巧.md
+15
-0
算法思维系列/回溯算法详解修订版.md
算法思维系列/回溯算法详解修订版.md
+29
-17
算法思维系列/学习数据结构和算法的高效方法.md
算法思维系列/学习数据结构和算法的高效方法.md
+12
-0
算法思维系列/差分技巧.md
算法思维系列/差分技巧.md
+10
-0
算法思维系列/滑动窗口技巧进阶.md
算法思维系列/滑动窗口技巧进阶.md
+5
-0
算法思维系列/花式遍历.md
算法思维系列/花式遍历.md
+7
-0
高频面试系列/LRU算法.md
高频面试系列/LRU算法.md
+34
-13
高频面试系列/k个一组反转链表.md
高频面试系列/k个一组反转链表.md
+3
-0
高频面试系列/二分运用.md
高频面试系列/二分运用.md
+2
-0
高频面试系列/名人问题.md
高频面试系列/名人问题.md
+6
-0
高频面试系列/子集排列组合.md
高频面试系列/子集排列组合.md
+105
-61
高频面试系列/安排会议室.md
高频面试系列/安排会议室.md
+1
-0
高频面试系列/随机权重.md
高频面试系列/随机权重.md
+3
-0
未找到文件。
README.md
浏览文件 @
70088737
...
...
@@ -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/)
...
...
动态规划系列/动态规划之博弈问题.md
浏览文件 @
70088737
...
...
@@ -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
)
{
...
...
动态规划系列/动态规划之四键键盘.md
浏览文件 @
70088737
...
...
@@ -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
];
...
...
动态规划系列/动态规划之正则表达.md
浏览文件 @
70088737
...
...
@@ -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
;
...
...
动态规划系列/动态规划设计:最长递增子序列.md
浏览文件 @
70088737
...
...
@@ -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
)
{
...
...
动态规划系列/动态规划详解进阶.md
浏览文件 @
70088737
...
...
@@ -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,18 +342,20 @@ int dp(int[] coins, int amount) {
类似之前斐波那契数列的例子,只需要稍加修改,就可以通过备忘录消除子问题:
<!-- muliti_language -->
```
java
int
[]
memo
;
class
Solution
{
int
[]
memo
;
int
coinChange
(
int
[]
coins
,
int
amount
)
{
int
coinChange
(
int
[]
coins
,
int
amount
)
{
memo
=
new
int
[
amount
+
1
];
// 备忘录初始化为一个不会被取到的特殊值,代表还未被计算
Arrays
.
fill
(
memo
,
-
666
);
return
dp
(
coins
,
amount
);
}
}
int
dp
(
int
[]
coins
,
int
amount
)
{
int
dp
(
int
[]
coins
,
int
amount
)
{
if
(
amount
==
0
)
return
0
;
if
(
amount
<
0
)
return
-
1
;
// 查备忘录,防止重复计算
...
...
@@ -364,6 +374,7 @@ int dp(int[] coins, int 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
];
...
...
动态规划系列/单词拼接.md
浏览文件 @
70088737
...
...
@@ -42,6 +42,7 @@
函数签名如下:
<!-- muliti_language -->
```
java
boolean
wordBreak
(
String
s
,
List
<
String
>
wordDict
);
```
...
...
@@ -54,18 +55,20 @@ 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
)
{
// 元素无重可复选的全排列
public
List
<
List
<
Integer
>>
permuteRepeat
(
int
[]
nums
)
{
backtrack
(
nums
);
return
res
;
}
}
// 回溯算法核心函数
void
backtrack
(
int
[]
nums
)
{
// 回溯算法核心函数
void
backtrack
(
int
[]
nums
)
{
// base case,到达叶子节点
if
(
track
.
size
()
==
nums
.
length
)
{
// 收集根到叶子节点路径上的值
...
...
@@ -82,7 +85,9 @@ void backtrack(int[] nums) {
// 取消选择
track
.
removeLast
();
}
}
}
```
给这个函数输入
`nums = [1,2,3]`
,输出是 3^3 = 27 种可能的组合:
...
...
@@ -107,23 +112,25 @@ 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
)
{
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
;
}
}
// 回溯算法框架
void
backtrack
(
String
s
,
int
i
)
{
// 回溯算法框架
void
backtrack
(
String
s
,
int
i
)
{
// base case
if
(
found
)
{
// 如果已经找到答案,就不要再递归搜索了
...
...
@@ -150,6 +157,7 @@ void backtrack(String s, int i) {
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,24 +288,26 @@ 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
)
{
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..] 是否能够被拼出
boolean
dp
(
String
s
,
int
i
)
{
// 定义:s[i..] 是否能够被拼出
boolean
dp
(
String
s
,
int
i
)
{
// base case
if
(
i
==
s
.
length
())
{
return
true
;
...
...
@@ -319,7 +334,9 @@ boolean dp(String s, int i) {
// s[i..] 无法被拼出
memo
[
i
]
=
0
;
return
false
;
}
}
```
这个解法能够通过所有测试用例,我们根据
[
算法时空复杂度使用指南
](
https://labuladong.github.io/article/fname.html?fname=时间复杂度
)
来算一下它的时间复杂度:
...
...
@@ -336,23 +353,25 @@ boolean dp(String s, int i) {
上一道题的回溯算法维护一个
`found`
变量,只要找到一种拼接方案就提前结束遍历回溯树,那么在这道题中我们不要提前结束遍历,并把所有可行的拼接方案收集起来就能得到答案:
<!-- muliti_language -->
```
java
// 记录结果
List
<
String
>
res
=
new
LinkedList
<>();
// 记录回溯算法的路径
LinkedList
<
String
>
track
=
new
LinkedList
<>();
List
<
String
>
wordDict
;
class
Solution
{
// 记录结果
List
<
String
>
res
=
new
LinkedList
<>();
// 记录回溯算法的路径
LinkedList
<
String
>
track
=
new
LinkedList
<>();
List
<
String
>
wordDict
;
// 主函数
public
List
<
String
>
wordBreak
(
String
s
,
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
)
{
// 回溯算法框架
void
backtrack
(
String
s
,
int
i
)
{
// base case
if
(
i
==
s
.
length
())
{
// 找到一个合法组合拼出整个 s,转化成字符串
...
...
@@ -375,28 +394,32 @@ void backtrack(String s, int i) {
track
.
removeLast
();
}
}
}
}
```
这个解法的时间复杂度和前一道题类似,依然是
`O(2^N * MN)`
,但由于这道题给的数据规模较小,所以可以通过所有测试用例。
类似的,这个问题也可以用分解问题的思维解决,把上一道题的
`dp`
函数稍作修改即可:
<!-- muliti_language -->
```
java
HashSet
<
String
>
wordDict
;
// 备忘录
List
<
String
>[]
memo
;
class
Solution
{
HashSet
<
String
>
wordDict
;
// 备忘录
List
<
String
>[]
memo
;
public
List
<
String
>
wordBreak
(
String
s
,
List
<
String
>
wordDict
)
{
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
)
{
// 定义:返回用 wordDict 构成 s[i..] 的所有可能
List
<
String
>
dp
(
String
s
,
int
i
)
{
List
<
String
>
res
=
new
LinkedList
<>();
if
(
i
==
s
.
length
())
{
res
.
add
(
""
);
...
...
@@ -430,6 +453,7 @@ List<String> dp(String s, int i) {
memo
[
i
]
=
res
;
return
res
;
}
}
```
...
...
动态规划系列/团灭股票问题.md
浏览文件 @
70088737
...
...
@@ -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
)
{
...
...
动态规划系列/抢房子.md
浏览文件 @
70088737
...
...
@@ -39,6 +39,7 @@
请你写一个算法,计算在不触动报警器的前提下,最多能够盗窃多少现金呢?函数签名如下:
<!-- muliti_language -->
```
java
int
rob
(
int
[]
nums
);
```
...
...
动态规划系列/最优子结构.md
浏览文件 @
70088737
...
...
@@ -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=动态规划之博弈问题
)
...
...
动态规划系列/状态压缩技巧.md
浏览文件 @
70088737
...
...
@@ -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
();
...
...
动态规划系列/编辑距离.md
浏览文件 @
70088737
...
...
@@ -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,11 +200,13 @@ int dp(i, j) {
备忘录很好加,原来的代码稍加修改即可:
<!-- muliti_language -->
```
java
// 备忘录
int
[][]
memo
;
class
Solution
{
// 备忘录
int
[][]
memo
;
public
int
minDistance
(
String
s1
,
String
s2
)
{
public
int
minDistance
(
String
s1
,
String
s2
)
{
int
m
=
s1
.
length
(),
n
=
s2
.
length
();
// 备忘录初始化为特殊值,代表还未计算
memo
=
new
int
[
m
][
n
];
...
...
@@ -209,9 +214,9 @@ public int minDistance(String s1, String s2) {
Arrays
.
fill
(
row
,
-
1
);
}
return
dp
(
s1
,
m
-
1
,
s2
,
n
-
1
);
}
}
int
dp
(
String
s1
,
int
i
,
String
s2
,
int
j
)
{
int
dp
(
String
s1
,
int
i
,
String
s2
,
int
j
)
{
if
(
i
==
-
1
)
return
j
+
1
;
if
(
j
==
-
1
)
return
i
+
1
;
// 查备忘录,避免重叠子问题
...
...
@@ -229,10 +234,11 @@ int dp(String s1, int i, String s2, int j) {
);
}
return
memo
[
i
][
j
];
}
}
int
min
(
int
a
,
int
b
,
int
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
;
...
...
动态规划系列/贪心算法之区间调度问题.md
浏览文件 @
70088737
...
...
@@ -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
)
{
// ...
...
...
动态规划系列/魔塔.md
浏览文件 @
70088737
...
...
@@ -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,9 +190,11 @@ dp(i, j) = res <= 0 ? 1 : res;
根据这个核心逻辑,加一个备忘录消除重叠子问题,就可以直接写出最终的代码了:
<!-- muliti_language -->
```
java
/* 主函数 */
int
calculateMinimumHP
(
int
[][]
grid
)
{
class
Solution
{
/* 主函数 */
public
int
calculateMinimumHP
(
int
[][]
grid
)
{
int
m
=
grid
.
length
;
int
n
=
grid
[
0
].
length
;
// 备忘录中都初始化为 -1
...
...
@@ -197,13 +204,13 @@ int calculateMinimumHP(int[][] grid) {
}
return
dp
(
grid
,
0
,
0
);
}
}
// 备忘录,消除重叠子问题
int
[][]
memo
;
// 备忘录,消除重叠子问题
int
[][]
memo
;
/* 定义:从 (i, j) 到达右下角,需要的初始血量至少是多少 */
int
dp
(
int
[][]
grid
,
int
i
,
int
j
)
{
/* 定义:从 (i, j) 到达右下角,需要的初始血量至少是多少 */
int
dp
(
int
[][]
grid
,
int
i
,
int
j
)
{
int
m
=
grid
.
length
;
int
n
=
grid
[
0
].
length
;
// base case
...
...
@@ -226,7 +233,9 @@ int dp(int[][] grid, int i, int j) {
memo
[
i
][
j
]
=
res
<=
0
?
1
:
res
;
return
memo
[
i
][
j
];
}
}
```
这就是自顶向下带备忘录的动态规划解法,参考前文
[
动态规划套路详解
](
https://labuladong.github.io/article/fname.html?fname=动态规划详解进阶
)
很容易就可以改写成
`dp`
数组的迭代解法,这里就不写了,读者可以尝试自己写一写。
...
...
技术/刷题技巧.md
浏览文件 @
70088737
...
...
@@ -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
)
{
...
...
数据结构系列/BST1.md
浏览文件 @
70088737
...
...
@@ -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
);
...
...
数据结构系列/BST2.md
浏览文件 @
70088737
...
...
@@ -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
;
...
...
数据结构系列/dijkstra算法.md
浏览文件 @
70088737
...
...
@@ -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
];
...
...
数据结构系列/二叉堆详解实现优先级队列.md
浏览文件 @
70088737
...
...
@@ -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,8 +134,12 @@ public class MaxPQ
**上浮的代码实现:**
<!-- muliti_language -->
```
java
private
void
swim
(
int
x
)
{
public
class
MaxPQ
<
Key
extends
Comparable
<
Key
>>
{
// 为了节约篇幅,省略上文给出的代码部分...
private
void
swim
(
int
x
)
{
// 如果浮到堆顶,就不能再上浮了
while
(
x
>
1
&&
less
(
parent
(
x
),
x
))
{
// 如果第 x 个元素比上层大
...
...
@@ -141,6 +147,7 @@ private void swim(int x) {
swap
(
parent
(
x
),
x
);
x
=
parent
(
x
);
}
}
}
```
...
...
@@ -152,8 +159,12 @@ private void swim(int x) {
下沉比上浮略微复杂一点,因为上浮某个节点 A,只需要 A 和其父节点比较大小即可;但是下沉某个节点 A,需要 A 和其
**两个子节点**
比较大小,如果 A 不是最大的就需要调整位置,要把较大的那个子节点和 A 交换。
<!-- muliti_language -->
```
java
private
void
sink
(
int
x
)
{
public
class
MaxPQ
<
Key
extends
Comparable
<
Key
>>
{
// 为了节约篇幅,省略上文给出的代码部分...
private
void
sink
(
int
x
)
{
// 如果沉到堆底,就沉不下去了
while
(
left
(
x
)
<=
size
)
{
// 先假设左边节点较大
...
...
@@ -167,6 +178,7 @@ private void sink(int x) {
swap
(
x
,
max
);
x
=
max
;
}
}
}
```
...
...
@@ -184,20 +196,29 @@ private void sink(int x) {
![](
https://labuladong.gitee.io/pictures/heap/insert.gif
)
<!-- muliti_language -->
```
java
public
void
insert
(
Key
e
)
{
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
()
{
public
class
MaxPQ
<
Key
extends
Comparable
<
Key
>>
{
// 为了节约篇幅,省略上文给出的代码部分...
public
Key
delMax
()
{
// 最大堆的堆顶就是最大元素
Key
max
=
pq
[
1
];
// 把这个最大元素换到最后,删除之
...
...
@@ -207,6 +228,7 @@ public Key delMax() {
// 让 pq[1] 下沉到正确位置
sink
(
1
);
return
max
;
}
}
```
...
...
数据结构系列/二叉树总结.md
浏览文件 @
70088737
...
...
@@ -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,18 +465,20 @@ int count(TreeNode root) {
最大深度的算法我们刚才实现过了,上述思路就可以写出以下代码:
<!-- muliti_language -->
```
java
// 记录最大直径的长度
int
maxDiameter
=
0
;
class
Solution
{
// 记录最大直径的长度
int
maxDiameter
=
0
;
public
int
diameterOfBinaryTree
(
TreeNode
root
)
{
public
int
diameterOfBinaryTree
(
TreeNode
root
)
{
// 对每个节点计算直径,求最大直径
traverse
(
root
);
return
maxDiameter
;
}
}
// 遍历二叉树
void
traverse
(
TreeNode
root
)
{
// 遍历二叉树
void
traverse
(
TreeNode
root
)
{
if
(
root
==
null
)
{
return
;
}
...
...
@@ -478,16 +491,17 @@ void traverse(TreeNode root) {
traverse
(
root
.
left
);
traverse
(
root
.
right
);
}
}
// 计算二叉树的最大深度
int
maxDepth
(
TreeNode
root
)
{
// 计算二叉树的最大深度
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
);
}
}
```
...
...
@@ -499,16 +513,18 @@ int maxDepth(TreeNode root) {
所以,稍微改一下代码逻辑即可得到更好的解法:
<!-- muliti_language -->
```
java
// 记录最大直径的长度
int
maxDiameter
=
0
;
class
Solution
{
// 记录最大直径的长度
int
maxDiameter
=
0
;
public
int
diameterOfBinaryTree
(
TreeNode
root
)
{
public
int
diameterOfBinaryTree
(
TreeNode
root
)
{
maxDepth
(
root
);
return
maxDiameter
;
}
}
int
maxDepth
(
TreeNode
root
)
{
int
maxDepth
(
TreeNode
root
)
{
if
(
root
==
null
)
{
return
0
;
}
...
...
@@ -519,6 +535,7 @@ int maxDepth(TreeNode root) {
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,19 +609,21 @@ 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
)
{
List
<
List
<
Integer
>>
levelTraverse
(
TreeNode
root
)
{
if
(
root
==
null
)
{
return
res
;
}
// root 视为第 0 层
traverse
(
root
,
0
);
return
res
;
}
}
void
traverse
(
TreeNode
root
,
int
depth
)
{
void
traverse
(
TreeNode
root
,
int
depth
)
{
if
(
root
==
null
)
{
return
;
}
...
...
@@ -616,7 +636,9 @@ void traverse(TreeNode root, int depth) {
res
.
get
(
depth
).
add
(
root
.
val
);
traverse
(
root
.
left
,
depth
+
1
);
traverse
(
root
.
right
,
depth
+
1
);
}
}
```
这种思路从结果上说确实可以得到层序遍历结果,但其本质还是二叉树的前序遍历,或者说 DFS 的思路,而不是层序遍历,或者说 BFS 的思路。因为这个解法是依赖前序遍历自顶向下、自左向右的顺序特点得到了正确的结果。
...
...
@@ -625,10 +647,13 @@ void traverse(TreeNode root, int depth) {
还有优秀读者评论了这样一种递归进行层序遍历的思路:
<!-- muliti_language -->
```
java
List
<
List
<
Integer
>>
res
=
new
LinkedList
<>();
class
Solution
{
List
<
List
<
Integer
>>
levelTraverse
(
TreeNode
root
)
{
List
<
List
<
Integer
>>
res
=
new
LinkedList
<>();
List
<
List
<
Integer
>>
levelTraverse
(
TreeNode
root
)
{
if
(
root
==
null
)
{
return
res
;
}
...
...
@@ -636,9 +661,9 @@ List<List<Integer>> levelTraverse(TreeNode root) {
nodes
.
add
(
root
);
traverse
(
nodes
);
return
res
;
}
}
void
traverse
(
List
<
TreeNode
>
curLevelNodes
)
{
void
traverse
(
List
<
TreeNode
>
curLevelNodes
)
{
// base case
if
(
curLevelNodes
.
isEmpty
())
{
return
;
...
...
@@ -660,6 +685,7 @@ void traverse(List<TreeNode> curLevelNodes) {
traverse
(
nextLevelNodes
);
// 后序位置添加结果,可以得到自底向上的层序遍历结果
// res.add(nodeValues);
}
}
```
...
...
数据结构系列/二叉树系列1.md
浏览文件 @
70088737
...
...
@@ -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
)
{
...
...
数据结构系列/二叉树系列2.md
浏览文件 @
70088737
...
...
@@ -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 中值到索引的映射
...
...
数据结构系列/单调队列.md
浏览文件 @
70088737
...
...
@@ -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,20 +152,30 @@ public void push(int n) {
如果每个元素被加入时都这样操作,最终单调队列中的元素大小就会保持一个
**单调递减**
的顺序,因此我们的
`max`
方法可以可以这样写:
<!-- muliti_language -->
```
java
public
int
max
()
{
class
MonotonicQueue
{
// 为了节约篇幅,省略上文给出的代码部分...
public
int
max
()
{
// 队头的元素肯定是最大的
return
maxq
.
getFirst
();
}
}
```
`pop`
方法在队头删除元素
`n`
,也很好写:
<!-- muliti_language -->
```
java
public
void
pop
(
int
n
)
{
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
>>
{
...
...
数据结构系列/图.md
浏览文件 @
70088737
...
...
@@ -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,19 +276,21 @@ List<List<Integer>> allPathsSourceTarget(int[][] graph);
既然输入的图是无环的,我们就不需要
`visited`
数组辅助了,直接套用图的遍历框架:
<!-- muliti_language -->
```
java
// 记录所有路径
List
<
List
<
Integer
>>
res
=
new
LinkedList
<>();
class
Solution
{
// 记录所有路径
List
<
List
<
Integer
>>
res
=
new
LinkedList
<>();
public
List
<
List
<
Integer
>>
allPathsSourceTarget
(
int
[][]
graph
)
{
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
)
{
/* 图的遍历框架 */
void
traverse
(
int
[][]
graph
,
int
s
,
LinkedList
<
Integer
>
path
)
{
// 添加节点 s 到路径
path
.
addLast
(
s
);
...
...
@@ -301,6 +311,7 @@ void traverse(int[][] graph, int s, LinkedList<Integer> path) {
// 从路径移出节点 s
path
.
removeLast
();
}
}
```
...
...
数据结构系列/实现计算器.md
浏览文件 @
70088737
...
...
@@ -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
:
...
...
数据结构系列/拓扑排序.md
浏览文件 @
70088737
...
...
@@ -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,15 +192,17 @@ void traverse(List<Integer>[] graph, int s) {
这样,就可以在遍历图的过程中顺便判断是否存在环了,完整代码如下:
<!-- muliti_language -->
```
java
// 记录一次递归堆栈中的节点
boolean
[]
onPath
;
// 记录遍历过的节点,防止走回头路
boolean
[]
visited
;
// 记录图中是否有环
boolean
hasCycle
=
false
;
boolean
canFinish
(
int
numCourses
,
int
[][]
prerequisites
)
{
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
];
...
...
@@ -206,9 +214,9 @@ boolean canFinish(int numCourses, int[][] prerequisites) {
}
// 只要没有循环依赖可以完成所有课程
return
!
hasCycle
;
}
}
void
traverse
(
List
<
Integer
>[]
graph
,
int
s
)
{
void
traverse
(
List
<
Integer
>[]
graph
,
int
s
)
{
if
(
onPath
[
s
])
{
// 出现环
hasCycle
=
true
;
...
...
@@ -226,11 +234,13 @@ void traverse(List<Integer>[] graph, int s) {
}
// 后序代码位置
onPath
[
s
]
=
false
;
}
}
List
<
Integer
>[]
buildGraph
(
int
numCourses
,
int
[][]
prerequisites
)
{
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,15 +313,17 @@ 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
)
{
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
];
...
...
@@ -328,10 +342,10 @@ public int[] findOrder(int numCourses, int[][] prerequisites) {
res
[
i
]
=
postorder
.
get
(
i
);
}
return
res
;
}
}
// 图遍历函数
void
traverse
(
List
<
Integer
>[]
graph
,
int
s
)
{
// 图遍历函数
void
traverse
(
List
<
Integer
>[]
graph
,
int
s
)
{
if
(
onPath
[
s
])
{
// 发现环
hasCycle
=
true
;
...
...
@@ -348,12 +362,14 @@ void traverse(List<Integer>[] graph, int s) {
// 后序遍历位置
postorder
.
add
(
s
);
onPath
[
s
]
=
false
;
}
}
// 建图函数
List
<
Integer
>[]
buildGraph
(
int
numCourses
,
int
[][]
prerequisites
)
{
// 建图函数
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
)
{
...
...
数据结构系列/设计Twitter.md
浏览文件 @
70088737
...
...
@@ -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,8 +245,12 @@ while pq not empty:
借助这种牛逼的数据结构支持,我们就很容易实现这个核心功能了。注意我们把优先级队列设为按 time 属性
**从大到小降序排列**
,因为 time 越大意味着时间越近,应该排在前面:
<!-- muliti_language -->
```
java
public
List
<
Integer
>
getNewsFeed
(
int
userId
)
{
class
Twitter
{
// 为了节约篇幅,省略上文给出的代码部分...
public
List
<
Integer
>
getNewsFeed
(
int
userId
)
{
List
<
Integer
>
res
=
new
ArrayList
<>();
if
(!
userMap
.
containsKey
(
userId
))
return
res
;
// 关注列表的用户 Id
...
...
@@ -267,6 +277,7 @@ public List<Integer> getNewsFeed(int userId) {
pq
.
add
(
twt
.
next
);
}
return
res
;
}
}
```
...
...
数据结构系列/递归反转链表的一部分.md
浏览文件 @
70088737
...
...
@@ -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
...
...
数据结构系列/队列实现栈栈实现队列.md
浏览文件 @
70088737
...
...
@@ -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
)
{
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
()
{
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
()
{
class
MyQueue
{
// 为了节约篇幅,省略上文给出的代码部分...
/** 删除队头的元素并返回 */
public
int
pop
()
{
// 先调用 peek 保证 s2 非空
peek
();
return
s2
.
pop
();
}
}
```
最后,如何判断队列是否为空呢?如果两个栈都为空的话,就说明队列为空:
<!-- muliti_language -->
```
java
/** 判断队列是否为空 */
public
boolean
empty
()
{
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,9 +196,13 @@ class MyStack {
![](
https://labuladong.gitee.io/pictures/栈队列/6.jpg
)
<!-- muliti_language -->
```
java
/** 删除栈顶的元素并返回 */
public
int
pop
()
{
class
MyStack
{
// 为了节约篇幅,省略上文给出的代码部分...
/** 删除栈顶的元素并返回 */
public
int
pop
()
{
int
size
=
q
.
size
();
while
(
size
>
1
)
{
q
.
offer
(
q
.
poll
());
...
...
@@ -182,14 +210,19 @@ public int pop() {
}
// 之前的队尾元素已经到了队头
return
q
.
poll
();
}
}
```
这样实现还有一点小问题就是,原来的队尾元素被提到队头并删除了,但是
`top_elem`
变量没有更新,我们还需要一点小修改:
<!-- muliti_language -->
```
java
/** 删除栈顶的元素并返回 */
public
int
pop
()
{
class
MyStack
{
// 为了节约篇幅,省略上文给出的代码部分...
/** 删除栈顶的元素并返回 */
public
int
pop
()
{
int
size
=
q
.
size
();
// 留下队尾 2 个元素
while
(
size
>
2
)
{
...
...
@@ -201,15 +234,21 @@ public int pop() {
q
.
offer
(
q
.
poll
());
// 删除之前的队尾元素
return
q
.
poll
();
}
}
```
最后,API
`empty`
就很容易实现了,只要看底层的队列是否为空即可:
<!-- muliti_language -->
```
java
/** 判断栈是否为空 */
public
boolean
empty
()
{
class
MyStack
{
// 为了节约篇幅,省略上文给出的代码部分...
/** 判断栈是否为空 */
public
boolean
empty
()
{
return
q
.
isEmpty
();
}
}
```
...
...
算法思维系列/BFS框架.md
浏览文件 @
70088737
...
...
@@ -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
())
{
...
...
算法思维系列/UnionFind算法详解.md
浏览文件 @
70088737
...
...
@@ -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,8 +113,12 @@ class UF {
![](
https://labuladong.gitee.io/pictures/unionfind/4.jpg
)
<!-- muliti_language -->
```
java
public
void
union
(
int
p
,
int
q
)
{
class
UF
{
// 为了节约篇幅,省略上文给出的代码部分...
public
void
union
(
int
p
,
int
q
)
{
int
rootP
=
find
(
p
);
int
rootQ
=
find
(
q
);
if
(
rootP
==
rootQ
)
...
...
@@ -121,19 +127,20 @@ public void union(int p, int q) {
parent
[
rootP
]
=
rootQ
;
// parent[rootQ] = rootP 也一样
count
--;
// 两个分量合二为一
}
}
/* 返回某个节点 x 的根节点 */
private
int
find
(
int
x
)
{
/* 返回某个节点 x 的根节点 */
private
int
find
(
int
x
)
{
// 根节点的 parent[x] == x
while
(
parent
[
x
]
!=
x
)
x
=
parent
[
x
];
return
x
;
}
}
/* 返回当前的连通分量个数 */
public
int
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
)
{
class
UF
{
// 为了节约篇幅,省略上文给出的代码部分...
public
boolean
connected
(
int
p
,
int
q
)
{
int
rootP
=
find
(
p
);
int
rootQ
=
find
(
q
);
return
rootP
==
rootQ
;
}
}
```
...
...
@@ -165,8 +177,12 @@ public boolean connected(int p, int q) {
我们要知道哪种情况下可能出现不平衡现象,关键在于
`union`
过程:
<!-- muliti_language -->
```
java
public
void
union
(
int
p
,
int
q
)
{
class
UF
{
// 为了节约篇幅,省略上文给出的代码部分...
public
void
union
(
int
p
,
int
q
)
{
int
rootP
=
find
(
p
);
int
rootQ
=
find
(
q
);
if
(
rootP
==
rootQ
)
...
...
@@ -175,6 +191,8 @@ public void union(int p, int q) {
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,8 +226,12 @@ class UF {
比如说
`size[3] = 5`
表示,以节点
`3`
为根的那棵树,总共有
`5`
个节点。这样我们可以修改一下
`union`
方法:
<!-- muliti_language -->
```
java
public
void
union
(
int
p
,
int
q
)
{
class
UF
{
// 为了节约篇幅,省略上文给出的代码部分...
public
void
union
(
int
p
,
int
q
)
{
int
rootP
=
find
(
p
);
int
rootQ
=
find
(
q
);
if
(
rootP
==
rootQ
)
...
...
@@ -223,7 +246,9 @@ public void union(int p, int q) {
size
[
rootQ
]
+=
size
[
rootP
];
}
count
--;
}
}
```
这样,通过比较树的重量,就可以保证树的生长相对平衡,树的高度大致在
`logN`
这个数量级,极大提升执行效率。
...
...
@@ -246,14 +271,19 @@ public void union(int p, int q) {
第一种是在
`find`
中加一行代码:
<!-- muliti_language -->
```
java
private
int
find
(
int
x
)
{
class
UF
{
// 为了节约篇幅,省略上文给出的代码部分...
private
int
find
(
int
x
)
{
while
(
parent
[
x
]
!=
x
)
{
// 这行代码进行路径压缩
parent
[
x
]
=
parent
[
parent
[
x
]];
x
=
parent
[
x
];
}
return
x
;
}
}
```
...
...
@@ -265,20 +295,27 @@ private int find(int x) {
路径压缩的第二种写法是这样:
<!-- muliti_language -->
```
java
// 第二种路径压缩的 find 方法
public
int
find
(
int
x
)
{
class
UF
{
// 为了节约篇幅,省略上文给出的代码部分...
// 第二种路径压缩的 find 方法
public
int
find
(
int
x
)
{
if
(
parent
[
x
]
!=
x
)
{
parent
[
x
]
=
find
(
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 个英文字母
...
...
算法思维系列/二分查找详解.md
浏览文件 @
70088737
...
...
@@ -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
;
...
...
算法思维系列/前缀和技巧.md
浏览文件 @
70088737
...
...
@@ -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] 的元素和
...
...
算法思维系列/双指针技巧.md
浏览文件 @
70088737
...
...
@@ -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
=
""
;
...
...
算法思维系列/回溯算法详解修订版.md
浏览文件 @
70088737
...
...
@@ -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,11 +139,13 @@ for 选择 in 选择列表:
下面,直接看全排列代码:
<!-- muliti_language -->
```
java
List
<
List
<
Integer
>>
res
=
new
LinkedList
<>();
class
Solution
{
List
<
List
<
Integer
>>
res
=
new
LinkedList
<>();
/* 主函数,输入一组不重复的数字,返回它们的全排列 */
List
<
List
<
Integer
>>
permute
(
int
[]
nums
)
{
/* 主函数,输入一组不重复的数字,返回它们的全排列 */
List
<
List
<
Integer
>>
permute
(
int
[]
nums
)
{
// 记录「路径」
LinkedList
<
Integer
>
track
=
new
LinkedList
<>();
// 「路径」中的元素会被标记为 true,避免重复使用
...
...
@@ -150,12 +153,12 @@ List<List<Integer>> permute(int[] nums) {
backtrack
(
nums
,
track
,
used
);
return
res
;
}
}
// 路径:记录在 track 中
// 选择列表:nums 中不存在于 track 的那些元素(used[i] 为 false)
// 结束条件:nums 中的元素全都在 track 中出现
void
backtrack
(
int
[]
nums
,
LinkedList
<
Integer
>
track
,
boolean
[]
used
)
{
// 路径:记录在 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
));
...
...
@@ -177,6 +180,7 @@ void backtrack(int[] nums, LinkedList<Integer> track, boolean[] 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,22 +220,25 @@ vector<vector<string>> solveNQueens(int n);
因为 C++ 代码对字符串的操作方便一些,所以这道题我用 C++ 来写解法,直接套用回溯算法框架:
<!-- muliti_language -->
```
cpp
vector
<
vector
<
string
>>
res
;
class
Solution
{
public:
vector
<
vector
<
string
>>
res
;
/* 输入棋盘边长 n,返回所有合法的放置 */
vector
<
vector
<
string
>>
solveNQueens
(
int
n
)
{
/* 输入棋盘边长 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
)
{
// 路径:board 中小于 row 的那些行都已经成功放置了皇后
// 选择列表:第 row 行的所有列都是放置皇后的选择
// 结束条件:row 超过 board 的最后一行
void
backtrack
(
vector
<
string
>&
board
,
int
row
)
{
// 触发结束条件
if
(
row
==
board
.
size
())
{
res
.
push_back
(
board
);
...
...
@@ -250,11 +258,13 @@ void backtrack(vector<string>& board, int row) {
// 撤销选择
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
;
...
...
算法思维系列/学习数据结构和算法的高效方法.md
浏览文件 @
70088737
...
...
@@ -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
)
{
...
...
算法思维系列/差分技巧.md
浏览文件 @
70088737
...
...
@@ -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 个车站
...
...
算法思维系列/滑动窗口技巧进阶.md
浏览文件 @
70088737
...
...
@@ -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
;
...
...
算法思维系列/花式遍历.md
浏览文件 @
70088737
...
...
@@ -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
];
...
...
高频面试系列/LRU算法.md
浏览文件 @
70088737
...
...
@@ -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
)
{
class
LRUCache
{
// 为了节约篇幅,省略上文给出的代码部分...
/* 将某个 key 提升为最近使用的 */
private
void
makeRecently
(
int
key
)
{
Node
x
=
map
.
get
(
key
);
// 先从链表中删除这个节点
cache
.
remove
(
x
);
// 重新插到队尾
cache
.
addLast
(
x
);
}
}
/* 添加最近使用的元素 */
private
void
addRecently
(
int
key
,
int
val
)
{
/* 添加最近使用的元素 */
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
deleteKey
(
int
key
)
{
/* 删除某一个 key */
private
void
deleteKey
(
int
key
)
{
Node
x
=
map
.
get
(
key
);
// 从链表中删除
cache
.
remove
(
x
);
// 从 map 中删除
map
.
remove
(
key
);
}
}
/* 删除最久未使用的元素 */
private
void
removeLeastRecently
()
{
/* 删除最久未使用的元素 */
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
)
{
class
LRUCache
{
// 为了节约篇幅,省略上文给出的代码部分...
public
int
get
(
int
key
)
{
if
(!
map
.
containsKey
(
key
))
{
return
-
1
;
}
// 将该数据提升为最近使用的
makeRecently
(
key
);
return
map
.
get
(
key
).
val
;
}
}
```
...
...
@@ -271,8 +286,12 @@ public int get(int key) {
这样我们可以轻松写出
`put`
方法的代码:
<!-- muliti_language -->
```
java
public
void
put
(
int
key
,
int
val
)
{
class
LRUCache
{
// 为了节约篇幅,省略上文给出的代码部分...
public
void
put
(
int
key
,
int
val
)
{
if
(
map
.
containsKey
(
key
))
{
// 删除旧的数据
deleteKey
(
key
);
...
...
@@ -287,11 +306,13 @@ public void put(int key, int val) {
}
// 添加为最近使用的元素
addRecently
(
key
,
val
);
}
}
```
至此,你应该已经完全掌握 LRU 算法的原理和实现了,我们最后用 Java 的内置类型
`LinkedHashMap`
来实现 LRU 算法,逻辑和之前完全一致,我就不过多解释了:
<!-- muliti_language -->
```
java
class
LRUCache
{
int
cap
;
...
...
高频面试系列/k个一组反转链表.md
浏览文件 @
70088737
...
...
@@ -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
;
...
...
高频面试系列/二分运用.md
浏览文件 @
70088737
...
...
@@ -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
)
{
...
...
高频面试系列/名人问题.md
浏览文件 @
70088737
...
...
@@ -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 是名人
...
...
高频面试系列/子集排列组合.md
浏览文件 @
70088737
...
...
@@ -80,6 +80,7 @@
函数签名如下:
<!-- muliti_language -->
```
java
List
<
List
<
Integer
>>
subsets
(
int
[]
nums
)
```
...
...
@@ -128,19 +129,22 @@ List<List<Integer>> subsets(int[] nums)
直接看代码:
<!-- 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
>>
subsets
(
int
[]
nums
)
{
// 主函数
public
List
<
List
<
Integer
>>
subsets
(
int
[]
nums
)
{
backtrack
(
nums
,
0
);
return
res
;
}
}
// 回溯算法核心函数,遍历子集问题的回溯树
void
backtrack
(
int
[]
nums
,
int
start
)
{
// 回溯算法核心函数,遍历子集问题的回溯树
void
backtrack
(
int
[]
nums
,
int
start
)
{
// 前序位置,每个节点的值都是一个子集
res
.
add
(
new
LinkedList
<>(
track
));
...
...
@@ -154,6 +158,7 @@ void backtrack(int[] nums, int start) {
// 撤销选择
track
.
removeLast
();
}
}
}
```
...
...
@@ -181,6 +186,7 @@ void backtrack(int[] nums, int start) {
函数签名如下:
<!-- muliti_language -->
```
java
List
<
List
<
Integer
>>
combine
(
int
n
,
int
k
)
```
...
...
@@ -201,18 +207,21 @@ 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
)
{
List
<
List
<
Integer
>>
res
=
new
LinkedList
<>();
// 记录回溯算法的递归路径
LinkedList
<
Integer
>
track
=
new
LinkedList
<>();
// 主函数
public
List
<
List
<
Integer
>>
combine
(
int
n
,
int
k
)
{
backtrack
(
1
,
n
,
k
);
return
res
;
}
}
void
backtrack
(
int
start
,
int
n
,
int
k
)
{
void
backtrack
(
int
start
,
int
n
,
int
k
)
{
// base case
if
(
k
==
track
.
size
())
{
// 遍历到了第 k 层,收集当前节点的值
...
...
@@ -229,6 +238,7 @@ void backtrack(int start, int n, int 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,22 +279,25 @@ 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
)
{
class
Solution
{
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
;
}
}
// 回溯算法核心函数
void
backtrack
(
int
[]
nums
)
{
// 回溯算法核心函数
void
backtrack
(
int
[]
nums
)
{
// base case,到达叶子节点
if
(
track
.
size
()
==
nums
.
length
)
{
// 收集叶子节点上的值
...
...
@@ -306,6 +320,7 @@ void backtrack(int[] 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,18 +394,21 @@ 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
{
List
<
List
<
Integer
>>
res
=
new
LinkedList
<>();
LinkedList
<
Integer
>
track
=
new
LinkedList
<>();
public
List
<
List
<
Integer
>>
subsetsWithDup
(
int
[]
nums
)
{
public
List
<
List
<
Integer
>>
subsetsWithDup
(
int
[]
nums
)
{
// 先排序,让相同的元素靠在一起
Arrays
.
sort
(
nums
);
backtrack
(
nums
,
0
);
return
res
;
}
}
void
backtrack
(
int
[]
nums
,
int
start
)
{
void
backtrack
(
int
[]
nums
,
int
start
)
{
// 前序位置,每个节点的值都是一个子集
res
.
add
(
new
LinkedList
<>(
track
));
...
...
@@ -401,6 +421,7 @@ void backtrack(int[] nums, int start) {
backtrack
(
nums
,
i
+
1
);
track
.
removeLast
();
}
}
}
```
...
...
@@ -420,14 +441,17 @@ 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
;
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
)
{
public
List
<
List
<
Integer
>>
combinationSum2
(
int
[]
candidates
,
int
target
)
{
if
(
candidates
.
length
==
0
)
{
return
res
;
}
...
...
@@ -435,10 +459,10 @@ public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays
.
sort
(
candidates
);
backtrack
(
candidates
,
0
,
target
);
return
res
;
}
}
// 回溯算法主函数
void
backtrack
(
int
[]
nums
,
int
start
,
int
target
)
{
// 回溯算法主函数
void
backtrack
(
int
[]
nums
,
int
start
,
int
target
)
{
// base case,达到目标和,找到符合条件的组合
if
(
trackSum
==
target
)
{
res
.
add
(
new
LinkedList
<>(
track
));
...
...
@@ -464,6 +488,7 @@ void backtrack(int[] nums, int start, int 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,20 +511,23 @@ List<List<Integer>> permuteUnique(int[] nums)
先看解法代码:
<!-- muliti_language -->
```
java
List
<
List
<
Integer
>>
res
=
new
LinkedList
<>();
LinkedList
<
Integer
>
track
=
new
LinkedList
<>();
boolean
[]
used
;
class
Solution
{
List
<
List
<
Integer
>>
res
=
new
LinkedList
<>();
LinkedList
<
Integer
>
track
=
new
LinkedList
<>();
boolean
[]
used
;
public
List
<
List
<
Integer
>>
permuteUnique
(
int
[]
nums
)
{
public
List
<
List
<
Integer
>>
permuteUnique
(
int
[]
nums
)
{
// 先排序,让相同的元素靠在一起
Arrays
.
sort
(
nums
);
used
=
new
boolean
[
nums
.
length
];
backtrack
(
nums
);
return
res
;
}
}
void
backtrack
(
int
[]
nums
)
{
void
backtrack
(
int
[]
nums
)
{
if
(
track
.
size
()
==
nums
.
length
)
{
res
.
add
(
new
LinkedList
(
track
));
return
;
...
...
@@ -518,6 +547,7 @@ void backtrack(int[] 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,23 +739,26 @@ void backtrack(int[] nums, int start) {
这道题的解法代码如下:
<!-- muliti_language -->
```
java
List
<
List
<
Integer
>>
res
=
new
LinkedList
<>();
// 记录回溯的路径
LinkedList
<
Integer
>
track
=
new
LinkedList
<>();
// 记录 track 中的路径和
int
trackSum
=
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
)
{
public
List
<
List
<
Integer
>>
combinationSum
(
int
[]
candidates
,
int
target
)
{
if
(
candidates
.
length
==
0
)
{
return
res
;
}
backtrack
(
candidates
,
0
,
target
);
return
res
;
}
}
// 回溯算法主函数
void
backtrack
(
int
[]
nums
,
int
start
,
int
target
)
{
// 回溯算法主函数
void
backtrack
(
int
[]
nums
,
int
start
,
int
target
)
{
// base case,找到目标和,记录结果
if
(
trackSum
==
target
)
{
res
.
add
(
new
LinkedList
<>(
track
));
...
...
@@ -745,6 +781,7 @@ void backtrack(int[] nums, int start, int target) {
trackSum
-=
nums
[
i
];
track
.
removeLast
();
}
}
}
```
...
...
@@ -766,17 +803,20 @@ 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
)
{
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
)
{
// 回溯算法核心函数
void
backtrack
(
int
[]
nums
)
{
// base case,到达叶子节点
if
(
track
.
size
()
==
nums
.
length
)
{
// 收集叶子节点上的值
...
...
@@ -793,6 +833,7 @@ void backtrack(int[] 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
)
{
...
...
高频面试系列/安排会议室.md
浏览文件 @
70088737
...
...
@@ -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
;
...
...
高频面试系列/随机权重.md
浏览文件 @
70088737
...
...
@@ -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.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录