Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions 动态规划系列/LCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ title: '详解最长公共子序列问题,秒杀三道动态规划题目'

给你输入两个字符串 `s1` 和 `s2`,请你找出他们俩的最长公共子序列,返回这个子序列的长度。函数签名如下:

<!-- muliti_language -->
```java
int longestCommonSubsequence(String s1, String s2);
```
Expand Down
5 changes: 5 additions & 0 deletions 动态规划系列/动态规划之KMP字符匹配算法.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ KMP 算法(Knuth-Morris-Pratt 算法)是一个著名的字符串匹配算法

力扣第 28 题「实现 strStr」就是字符串匹配问题,暴力的字符串匹配算法很容易写,看一下它的运行逻辑:

<!-- muliti_language -->
```java
// 暴力匹配(伪码)
int search(String pat, String txt) {
Expand Down Expand Up @@ -117,6 +118,7 @@ pat = "aaab"

明白了 `dp` 数组只和 `pat` 有关,那么我们这样设计 KMP 算法就会比较漂亮:

<!-- muliti_language -->
```java
public class KMP {
private int[][] dp;
Expand Down Expand Up @@ -208,6 +210,7 @@ pat 应该转移到状态 2

根据我们这个 dp 数组的定义和刚才状态转移的过程,我们可以先写出 KMP 算法的 search 函数代码:

<!-- muliti_language -->
```java
public int search(String txt) {
int M = pat.length();
Expand Down Expand Up @@ -285,6 +288,7 @@ for 0 <= j < M:

如果之前的内容你都能理解,恭喜你,现在就剩下一个问题:影子状态 `X` 是如何得到的呢?下面先直接看完整代码吧。

<!-- muliti_language -->
```java
public class KMP {
private int[][] dp;
Expand Down Expand Up @@ -360,6 +364,7 @@ for (int i = 0; i < N; i++) {

至此,KMP 算法的核心终于写完啦啦啦啦!看下 KMP 算法的完整代码吧:

<!-- muliti_language -->
```java
public class KMP {
private int[][] dp;
Expand Down
3 changes: 2 additions & 1 deletion 动态规划系列/背包问题.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ title: '动态规划之背包问题'

举个简单的例子,输入如下:

```
```py
N = 3, W = 4
wt = [2, 1, 3]
val = [4, 2, 3]
Expand Down Expand Up @@ -124,6 +124,7 @@ return dp[N][W]

我用 Java 写的代码,把上面的思路完全翻译了一遍,并且处理了 `w - wt[i-1]` 可能小于 0 导致数组索引越界的问题:

<!-- muliti_language -->
```java
int knapsack(int W, int N, int[] wt, int[] val) {
assert N == wt.length;
Expand Down
7 changes: 7 additions & 0 deletions 数据结构系列/二叉树总结.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,12 @@ void traverse(ListNode head) {

**二叉树题目的递归解法可以分两类思路,第一类是遍历一遍二叉树得出答案,第二类是通过分解问题计算出答案,这两类思路分别对应着 [回溯算法核心框架](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版) 和 [动态规划核心框架](https://labuladong.github.io/article/fname.html?fname=动态规划详解进阶)**。

> tip:这里说一下我的函数命名习惯:二叉树中用遍历思路解题时函数签名一般是 `void traverse(...)`,没有返回值,靠更新外部变量来计算结果,而用分解问题思路解题时函数名根据该函数具体功能而定,而且一般会有返回值,返回值是子问题的计算结果。
>
> 与此对应的,你会发现我在 [回溯算法核心框架](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版) 中给出的函数签名一般也是没有返回值的 `void backtrack(...)`,而在 [动态规划核心框架](https://labuladong.github.io/article/fname.html?fname=动态规划详解进阶) 中给出的函数签名是带有返回值的 `dp` 函数。这也说明它俩和二叉树之间千丝万缕的联系。
>
> 虽然函数命名没有什么硬性的要求,但我还是建议你也遵循我的这种风格,这样更能突出函数的作用和解题的思维模式,便于你自己理解和运用。

当时我是用二叉树的最大深度这个问题来举例,重点在于把这两种思路和动态规划和回溯算法进行对比,而本文的重点在于分析这两种思路如何解决二叉树的题目。

力扣第 104 题「二叉树的最大深度」就是最大深度的题目,所谓最大深度就是根节点到「最远」叶子节点的最长路径上的节点数,比如输入这棵二叉树,算法应该返回 3:
Expand Down Expand Up @@ -710,6 +716,7 @@ class Solution {
- [两种思路解决单词拼接问题](https://labuladong.github.io/article/fname.html?fname=单词拼接)
- [二叉树(递归)专题课](https://labuladong.github.io/article/fname.html?fname=tree课程简介)
- [前缀树算法模板秒杀五道算法题](https://labuladong.github.io/article/fname.html?fname=trie)
- [后序遍历的妙用](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=解锁tree插件)
Expand Down
8 changes: 8 additions & 0 deletions 数据结构系列/单调栈.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ title: '特殊数据结构:单调栈'

现在给你出这么一道题:输入一个数组 `nums`,请你返回一个等长的结果数组,结果数组中对应索引存储着下一个更大元素,如果没有更大的元素,就存 -1。函数签名如下:

<!-- muliti_language -->
```java
int[] nextGreaterElement(int[] nums);
```
Expand All @@ -48,6 +49,7 @@ int[] nextGreaterElement(int[] nums);

这个情景很好理解吧?带着这个抽象的情景,先来看下代码。

<!-- muliti_language -->
```java
int[] nextGreaterElement(int[] nums) {
int n = nums.length;
Expand Down Expand Up @@ -83,12 +85,14 @@ int[] nextGreaterElement(int[] nums) {

这道题给你输入两个数组 `nums1` 和 `nums2`,让你求 `nums1` 中的元素在 `nums2` 中的下一个更大元素,函数签名如下:

<!-- muliti_language -->
```java
int[] nextGreaterElement(int[] nums1, int[] nums2)
```

其实和把我们刚才的代码改一改就可以解决这道题了,因为题目说 `nums1` 是 `nums2` 的子集,那么我们先把 `nums2` 中每个元素的下一个更大元素算出来存到一个映射里,然后再让 `nums1` 中的元素去查表即可:

<!-- muliti_language -->
```java
int[] nextGreaterElement(int[] nums1, int[] nums2) {
// 记录 nums2 中每个元素的下一个更大元素
Expand Down Expand Up @@ -116,6 +120,7 @@ int[] nextGreaterElement(int[] nums) {

给你一个数组 `temperatures`,这个数组存放的是近几天的天气气温,你返回一个等长的数组,计算:对于每一天,你还要至少等多少天才能等到一个更暖和的气温;如果等不到那一天,填 0。函数签名如下:

<!-- muliti_language -->
```java
int[] dailyTemperatures(int[] temperatures);
```
Expand All @@ -126,6 +131,7 @@ int[] dailyTemperatures(int[] temperatures);

相同的思路,直接调用单调栈的算法模板,稍作改动就可以,直接上代码吧:

<!-- muliti_language -->
```java
int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
Expand Down Expand Up @@ -156,6 +162,7 @@ int[] dailyTemperatures(int[] temperatures) {

我们一般是通过 % 运算符求模(余数),来模拟环形特效:

<!-- muliti_language -->
```java
int[] arr = {1,2,3,4,5};
int n = arr.length, index = 0;
Expand All @@ -176,6 +183,7 @@ while (true) {

有了思路,最简单的实现方式当然可以把这个双倍长度的数组构造出来,然后套用算法模板。但是,**我们可以不用构造新数组,而是利用循环数组的技巧来模拟数组长度翻倍的效果**。直接看代码吧:

<!-- muliti_language -->
```java
int[] nextGreaterElements(int[] nums) {
int n = nums.length;
Expand Down
3 changes: 3 additions & 0 deletions 数据结构系列/递归反转链表的一部分.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ head.next.next = head;

接下来:

<!-- muliti_language -->
```java
head.next = null;
return last;
Expand All @@ -117,6 +118,7 @@ return last;

1、递归函数要有 base case,也就是这句:

<!-- muliti_language -->
```java
if (head == null || head.next == null) {
return head;
Expand All @@ -127,6 +129,7 @@ if (head == null || head.next == null) {

2、当链表递归反转之后,新的头结点是 `last`,而之前的 `head` 变成了最后一个节点,别忘了链表的末尾要指向 null:

<!-- muliti_language -->
```java
head.next = null;
```
Expand Down
2 changes: 2 additions & 0 deletions 算法思维系列/BFS解决滑动拼图.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ title: 'BFS 算法秒杀各种益智游戏'

对于这道题,题目说输入的数组大小都是 2 x 3,所以我们可以直接手动写出来这个映射:

<!-- muliti_language -->
```java
// 记录一维字符串的相邻索引
int[][] neighbor = new int[][]{
Expand All @@ -99,6 +100,7 @@ int[][] neighbor = new int[][]{

至此,我们就把这个问题完全转化成标准的 BFS 问题了,借助前文 [BFS 算法框架](https://labuladong.github.io/article/fname.html?fname=BFS框架) 的代码框架,直接就可以套出解法代码了:

<!-- muliti_language -->
```java
public int slidingPuzzle(int[][] board) {
int m = 2, n = 3;
Expand Down
3 changes: 2 additions & 1 deletion 算法思维系列/字符串乘法.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ title: '字符串乘法'

明白了这一点,就可以用代码模仿出这个计算过程了:

```java
<!-- muliti_language -->
```cpp
string multiply(string num1, string num2) {
int m = num1.size(), n = num2.size();
// 结果最多为 m + n 位数
Expand Down
8 changes: 8 additions & 0 deletions 算法思维系列/常用的位操作.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ boolean f = ((x ^ y) < 0); // false

我在 [单调栈解题套路](https://labuladong.github.io/article/fname.html?fname=单调栈) 中介绍过环形数组,其实就是利用求模(余数)的方式让数组看起来头尾相接形成一个环形,永远都走不完:

<!-- muliti_language -->
```java
int[] arr = {1,2,3,4};
int index = 0;
Expand All @@ -117,6 +118,7 @@ while (true) {

但模运算 `%` 对计算机来说其实是一个比较昂贵的操作,所以我们可以用 `&` 运算来求余数:

<!-- muliti_language -->
```java
int[] arr = {1,2,3,4};
int index = 0;
Expand All @@ -136,6 +138,7 @@ while (true) {

答案是,如果你使用 `%` 求模的方式,那么当 `index` 小于 0 之后求模的结果也会出现负数,你需要特殊处理。但通过 `&` 与运算的方式,`index` 不会出现负数,依然可以正常工作:

<!-- muliti_language -->
```java
int[] arr = {1,2,3,4};
int index = 0;
Expand Down Expand Up @@ -167,6 +170,7 @@ while (true) {

就是让你返回 `n` 的二进制表示中有几个 1。因为 `n & (n - 1)` 可以消除最后一个 1,所以可以用一个循环不停地消除 1 同时计数,直到 `n` 变成 0 为止。

<!-- muliti_language -->
```java
int hammingWeight(int n) {
int res = 0;
Expand All @@ -192,6 +196,7 @@ int hammingWeight(int n) {

如果使用 `n & (n-1)` 的技巧就很简单了(注意运算符优先级,括号不可以省略):

<!-- muliti_language -->
```java
boolean isPowerOfTwo(int n) {
if (n <= 0) return false;
Expand All @@ -213,6 +218,7 @@ boolean isPowerOfTwo(int n) {

对于这道题目,我们只要把所有数字进行异或,成对儿的数字就会变成 0,落单的数字和 0 做异或还是它本身,所以最后异或的结果就是只出现一次的元素:

<!-- muliti_language -->
```java
int singleNumber(int[] nums) {
int res = 0;
Expand Down Expand Up @@ -241,6 +247,7 @@ int singleNumber(int[] nums) {

题目的意思可以这样理解:现在有个等差数列 `0, 1, 2,..., n`,其中少了某一个数字,请你把它找出来。那这个数字不就是 `sum(0,1,..n) - sum(nums)` 嘛?

<!-- muliti_language -->
```java
int missingNumber(int[] nums) {
int n = nums.length;
Expand Down Expand Up @@ -277,6 +284,7 @@ int missingNumber(int[] nums) {

如何找这个落单的数字呢,**只要把所有的元素和索引做异或运算,成对儿的数字都会消为 0,只有这个落单的元素会剩下**,也就达到了我们的目的:

<!-- muliti_language -->
```java
int missingNumber(int[] nums) {
int n = nums.length;
Expand Down
6 changes: 6 additions & 0 deletions 算法思维系列/洗牌算法.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ title: '洗牌算法'
我知道大家会各种花式排序算法,但是如果叫你打乱一个数组,你是否能做到胸有成竹?即便你拍脑袋想出一个算法,怎么证明你的算法就是正确的呢?乱序算法不像排序算法,结果唯一可以很容易检验,因为「乱」可以有很多种,你怎么能证明你的算法是「真的乱」呢?

所以我们面临两个问题:

1. 什么叫做「真的乱」?

2. 设计怎样的算法来打乱数组才能做到「真的乱」?

这种算法称为「随机乱置算法」或者「洗牌算法」。
Expand All @@ -37,6 +39,7 @@ title: '洗牌算法'

此类算法都是靠随机选取元素交换来获取随机性,直接看代码(伪码),该算法有 4 种形式,都是正确的:

<!-- muliti_language -->
```java
// 得到一个在闭区间 [min, max] 内的随机整数
int randInt(int min, int max);
Expand Down Expand Up @@ -71,6 +74,7 @@ void shuffle(int[] arr) {

我们先用这个准则分析一下**第一种写法**的正确性:

<!-- muliti_language -->
```java
// 假设传入这样一个 arr
int[] arr = {1,3,5,7,9};
Expand Down Expand Up @@ -113,6 +117,7 @@ for 循环第二轮迭代时,`i = 1`,`rand` 的取值范围是 `[1, 4]`,

如果读者思考过洗牌算法,可能会想出如下的算法,但是**这种写法是错误的**:

<!-- muliti_language -->
```java
void shuffle(int[] arr) {
int n = arr.length();
Expand Down Expand Up @@ -151,6 +156,7 @@ void shuffle(int[] arr) {

每次进行洗牌算法后,就把得到的打乱结果对应的频数加一,重复进行 100 万次,如果每种结果出现的总次数差不多,那就说明每种结果出现的概率应该是相等的。写一下这个思路的伪代码:

<!-- muliti_language -->
```java
void shuffle(int[] arr);

Expand Down
74 changes: 39 additions & 35 deletions 算法思维系列/烧饼排序.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ title: '烧饼排序'

为什么说这个问题有递归性质呢?比如说我们需要实现这样一个函数:

<!-- muliti_language -->
```java
// cakes 是一堆烧饼,函数会将前 n 个烧饼排序
void sort(int[] cakes, int n);
Expand Down Expand Up @@ -82,45 +83,48 @@ base case:`n == 1` 时,排序 1 个饼时不需要翻转。

只要把上述的思路用代码实现即可,唯一需要注意的是,数组索引从 0 开始,而我们要返回的结果是从 1 开始算的。

<!-- muliti_language -->
```java
// 记录反转操作序列
LinkedList<Integer> res = new LinkedList<>();
class Solution {
// 记录反转操作序列
LinkedList<Integer> res = new LinkedList<>();

List<Integer> pancakeSort(int[] cakes) {
sort(cakes, cakes.length);
return res;
}
List<Integer> pancakeSort(int[] cakes) {
sort(cakes, cakes.length);
return res;
}

void sort(int[] cakes, int n) {
// base case
if (n == 1) return;

// 寻找最大饼的索引
int maxCake = 0;
int maxCakeIndex = 0;
for (int i = 0; i < n; i++)
if (cakes[i] > maxCake) {
maxCakeIndex = i;
maxCake = cakes[i];
}

// 第一次翻转,将最大饼翻到最上面
reverse(cakes, 0, maxCakeIndex);
res.add(maxCakeIndex + 1);
// 第二次翻转,将最大饼翻到最下面
reverse(cakes, 0, n - 1);
res.add(n);

// 递归调用
sort(cakes, n - 1);
}
void sort(int[] cakes, int n) {
// base case
if (n == 1) return;

// 寻找最大饼的索引
int maxCake = 0;
int maxCakeIndex = 0;
for (int i = 0; i < n; i++)
if (cakes[i] > maxCake) {
maxCakeIndex = i;
maxCake = cakes[i];
}

// 第一次翻转,将最大饼翻到最上面
reverse(cakes, 0, maxCakeIndex);
res.add(maxCakeIndex + 1);
// 第二次翻转,将最大饼翻到最下面
reverse(cakes, 0, n - 1);
res.add(n);

void reverse(int[] arr, int i, int j) {
while (i < j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
i++; j--;
// 递归调用
sort(cakes, n - 1);
}

void reverse(int[] arr, int i, int j) {
while (i < j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
i++; j--;
}
}
}
```
Expand Down
Loading