提交 3d91fdd9 编写于 作者: L labuladong

update content

上级 9acc1b15
......@@ -38,6 +38,7 @@ title: '详解最长公共子序列问题,秒杀三道动态规划题目'
给你输入两个字符串 `s1``s2`,请你找出他们俩的最长公共子序列,返回这个子序列的长度。函数签名如下:
<!-- muliti_language -->
```java
int longestCommonSubsequence(String s1, String s2);
```
......
......@@ -43,6 +43,7 @@ KMP 算法(Knuth-Morris-Pratt 算法)是一个著名的字符串匹配算法
力扣第 28 题「实现 strStr」就是字符串匹配问题,暴力的字符串匹配算法很容易写,看一下它的运行逻辑:
<!-- muliti_language -->
```java
// 暴力匹配(伪码)
int search(String pat, String txt) {
......@@ -117,6 +118,7 @@ pat = "aaab"
明白了 `dp` 数组只和 `pat` 有关,那么我们这样设计 KMP 算法就会比较漂亮:
<!-- muliti_language -->
```java
public class KMP {
private int[][] dp;
......@@ -208,6 +210,7 @@ pat 应该转移到状态 2
根据我们这个 dp 数组的定义和刚才状态转移的过程,我们可以先写出 KMP 算法的 search 函数代码:
<!-- muliti_language -->
```java
public int search(String txt) {
int M = pat.length();
......@@ -285,6 +288,7 @@ for 0 <= j < M:
如果之前的内容你都能理解,恭喜你,现在就剩下一个问题:影子状态 `X` 是如何得到的呢?下面先直接看完整代码吧。
<!-- muliti_language -->
```java
public class KMP {
private int[][] dp;
......@@ -360,6 +364,7 @@ for (int i = 0; i < N; i++) {
至此,KMP 算法的核心终于写完啦啦啦啦!看下 KMP 算法的完整代码吧:
<!-- muliti_language -->
```java
public class KMP {
private int[][] dp;
......
......@@ -29,7 +29,7 @@ title: '动态规划之背包问题'
举个简单的例子,输入如下:
```
```py
N = 3, W = 4
wt = [2, 1, 3]
val = [4, 2, 3]
......@@ -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;
......
......@@ -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:
......@@ -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插件)
......
......@@ -34,6 +34,7 @@ title: '特殊数据结构:单调栈'
现在给你出这么一道题:输入一个数组 `nums`,请你返回一个等长的结果数组,结果数组中对应索引存储着下一个更大元素,如果没有更大的元素,就存 -1。函数签名如下:
<!-- muliti_language -->
```java
int[] nextGreaterElement(int[] nums);
```
......@@ -48,6 +49,7 @@ int[] nextGreaterElement(int[] nums);
这个情景很好理解吧?带着这个抽象的情景,先来看下代码。
<!-- muliti_language -->
```java
int[] nextGreaterElement(int[] nums) {
int n = nums.length;
......@@ -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 中每个元素的下一个更大元素
......@@ -116,6 +120,7 @@ int[] nextGreaterElement(int[] nums) {
给你一个数组 `temperatures`,这个数组存放的是近几天的天气气温,你返回一个等长的数组,计算:对于每一天,你还要至少等多少天才能等到一个更暖和的气温;如果等不到那一天,填 0。函数签名如下:
<!-- muliti_language -->
```java
int[] dailyTemperatures(int[] temperatures);
```
......@@ -126,6 +131,7 @@ int[] dailyTemperatures(int[] temperatures);
相同的思路,直接调用单调栈的算法模板,稍作改动就可以,直接上代码吧:
<!-- muliti_language -->
```java
int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
......@@ -156,6 +162,7 @@ int[] dailyTemperatures(int[] temperatures) {
我们一般是通过 % 运算符求模(余数),来模拟环形特效:
<!-- muliti_language -->
```java
int[] arr = {1,2,3,4,5};
int n = arr.length, index = 0;
......@@ -176,6 +183,7 @@ while (true) {
有了思路,最简单的实现方式当然可以把这个双倍长度的数组构造出来,然后套用算法模板。但是,**我们可以不用构造新数组,而是利用循环数组的技巧来模拟数组长度翻倍的效果**。直接看代码吧:
<!-- muliti_language -->
```java
int[] nextGreaterElements(int[] nums) {
int n = nums.length;
......
......@@ -106,6 +106,7 @@ head.next.next = head;
接下来:
<!-- muliti_language -->
```java
head.next = null;
return last;
......@@ -117,6 +118,7 @@ return last;
1、递归函数要有 base case,也就是这句:
<!-- muliti_language -->
```java
if (head == null || head.next == null) {
return head;
......@@ -127,6 +129,7 @@ if (head == null || head.next == null) {
2、当链表递归反转之后,新的头结点是 `last`,而之前的 `head` 变成了最后一个节点,别忘了链表的末尾要指向 null:
<!-- muliti_language -->
```java
head.next = null;
```
......
......@@ -75,6 +75,7 @@ title: 'BFS 算法秒杀各种益智游戏'
对于这道题,题目说输入的数组大小都是 2 x 3,所以我们可以直接手动写出来这个映射:
<!-- muliti_language -->
```java
// 记录一维字符串的相邻索引
int[][] neighbor = new int[][]{
......@@ -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;
......
......@@ -59,7 +59,8 @@ title: '字符串乘法'
明白了这一点,就可以用代码模仿出这个计算过程了:
```java
<!-- muliti_language -->
```cpp
string multiply(string num1, string num2) {
int m = num1.size(), n = num2.size();
// 结果最多为 m + n 位数
......
......@@ -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;
......@@ -117,6 +118,7 @@ while (true) {
但模运算 `%` 对计算机来说其实是一个比较昂贵的操作,所以我们可以用 `&` 运算来求余数:
<!-- muliti_language -->
```java
int[] arr = {1,2,3,4};
int index = 0;
......@@ -136,6 +138,7 @@ while (true) {
答案是,如果你使用 `%` 求模的方式,那么当 `index` 小于 0 之后求模的结果也会出现负数,你需要特殊处理。但通过 `&` 与运算的方式,`index` 不会出现负数,依然可以正常工作:
<!-- muliti_language -->
```java
int[] arr = {1,2,3,4};
int index = 0;
......@@ -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;
......@@ -192,6 +196,7 @@ int hammingWeight(int n) {
如果使用 `n & (n-1)` 的技巧就很简单了(注意运算符优先级,括号不可以省略):
<!-- muliti_language -->
```java
boolean isPowerOfTwo(int n) {
if (n <= 0) return false;
......@@ -213,6 +218,7 @@ boolean isPowerOfTwo(int n) {
对于这道题目,我们只要把所有数字进行异或,成对儿的数字就会变成 0,落单的数字和 0 做异或还是它本身,所以最后异或的结果就是只出现一次的元素:
<!-- muliti_language -->
```java
int singleNumber(int[] nums) {
int res = 0;
......@@ -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;
......@@ -277,6 +284,7 @@ int missingNumber(int[] nums) {
如何找这个落单的数字呢,**只要把所有的元素和索引做异或运算,成对儿的数字都会消为 0,只有这个落单的元素会剩下**,也就达到了我们的目的:
<!-- muliti_language -->
```java
int missingNumber(int[] nums) {
int n = nums.length;
......
......@@ -26,7 +26,9 @@ title: '洗牌算法'
我知道大家会各种花式排序算法,但是如果叫你打乱一个数组,你是否能做到胸有成竹?即便你拍脑袋想出一个算法,怎么证明你的算法就是正确的呢?乱序算法不像排序算法,结果唯一可以很容易检验,因为「乱」可以有很多种,你怎么能证明你的算法是「真的乱」呢?
所以我们面临两个问题:
1. 什么叫做「真的乱」?
2. 设计怎样的算法来打乱数组才能做到「真的乱」?
这种算法称为「随机乱置算法」或者「洗牌算法」。
......@@ -37,6 +39,7 @@ title: '洗牌算法'
此类算法都是靠随机选取元素交换来获取随机性,直接看代码(伪码),该算法有 4 种形式,都是正确的:
<!-- muliti_language -->
```java
// 得到一个在闭区间 [min, max] 内的随机整数
int randInt(int min, int max);
......@@ -71,6 +74,7 @@ void shuffle(int[] arr) {
我们先用这个准则分析一下**第一种写法**的正确性:
<!-- muliti_language -->
```java
// 假设传入这样一个 arr
int[] arr = {1,3,5,7,9};
......@@ -113,6 +117,7 @@ for 循环第二轮迭代时,`i = 1`,`rand` 的取值范围是 `[1, 4]`,
如果读者思考过洗牌算法,可能会想出如下的算法,但是**这种写法是错误的**
<!-- muliti_language -->
```java
void shuffle(int[] arr) {
int n = arr.length();
......@@ -151,6 +156,7 @@ void shuffle(int[] arr) {
每次进行洗牌算法后,就把得到的打乱结果对应的频数加一,重复进行 100 万次,如果每种结果出现的总次数差不多,那就说明每种结果出现的概率应该是相等的。写一下这个思路的伪代码:
<!-- muliti_language -->
```java
void shuffle(int[] arr);
......
......@@ -43,6 +43,7 @@ title: '烧饼排序'
为什么说这个问题有递归性质呢?比如说我们需要实现这样一个函数:
<!-- muliti_language -->
```java
// cakes 是一堆烧饼,函数会将前 n 个烧饼排序
void sort(int[] cakes, int n);
......@@ -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--;
}
}
}
```
......
......@@ -37,6 +37,7 @@ title: '经典回溯算法:集合划分问题'
函数签名如下:
<!-- muliti_language -->
```java
boolean canPartitionKSubsets(int[] nums, int k);
```
......@@ -139,6 +140,7 @@ void traverse(int[] nums, int index) {
那么回到这道题,以数字的视角,选择 `k` 个桶,用 for 循环写出来是下面这样:
<!-- muliti_language -->
```java
// k 个桶(集合),记录每个桶装的数字之和
int[] bucket = new int[k];
......@@ -155,6 +157,7 @@ for (int index = 0; index < nums.length; index++) {
如果改成递归的形式,就是下面这段代码逻辑:
<!-- muliti_language -->
```java
// k 个桶(集合),记录每个桶装的数字之和
int[] bucket = new int[k];
......@@ -179,6 +182,7 @@ void backtrack(int[] nums, int index) {
虽然上述代码仅仅是穷举逻辑,还不能解决我们的问题,但是只要略加完善即可:
<!-- muliti_language -->
```java
// 主函数
boolean canPartitionKSubsets(int[] nums, int k) {
......@@ -236,6 +240,7 @@ boolean backtrack(
主要看 `backtrack` 函数的递归部分:
<!-- muliti_language -->
```java
for (int i = 0; i < bucket.length; i++) {
// 剪枝
......@@ -257,6 +262,7 @@ for (int i = 0; i < bucket.length; i++) {
所以可以在之前的代码中再添加一些代码:
<!-- muliti_language -->
```java
boolean canPartitionKSubsets(int[] nums, int k) {
// 其他代码不变
......@@ -284,6 +290,7 @@ boolean canPartitionKSubsets(int[] nums, int k) {
这个思路可以用下面这段代码表示出来:
<!-- muliti_language -->
```java
// 装满所有桶为止
while (k > 0) {
......@@ -305,6 +312,7 @@ while (k > 0) {
那么我们也可以把这个 while 循环改写成递归函数,不过比刚才略微复杂一些,首先写一个 `backtrack` 递归函数出来:
<!-- muliti_language -->
```java
boolean backtrack(int k, int bucket,
int[] nums, int start, boolean[] used, int target);
......@@ -318,6 +326,7 @@ boolean backtrack(int k, int bucket,
根据这个函数定义,可以这样调用 `backtrack` 函数:
<!-- muliti_language -->
```java
boolean canPartitionKSubsets(int[] nums, int k) {
// 排除一些基本情况
......@@ -341,6 +350,7 @@ boolean canPartitionKSubsets(int[] nums, int k) {
下面的代码就实现了这个逻辑:
<!-- muliti_language -->
```java
boolean backtrack(int k, int bucket,
int[] nums, int start, boolean[] used, int target) {
......@@ -423,6 +433,7 @@ boolean backtrack(int k, int bucket,
看下代码实现,只要稍微改一下 `backtrack` 函数即可:
<!-- muliti_language -->
```java
// 备忘录,存储 used 数组的状态
HashMap<String, Boolean> memo = new HashMap<>();
......@@ -465,65 +476,68 @@ boolean backtrack(int k, int bucket, int[] nums, int start, boolean[] used, int
看下最终的解法代码:
<!-- muliti_language -->
```java
public boolean canPartitionKSubsets(int[] nums, int k) {
// 排除一些基本情况
if (k > nums.length) return false;
int sum = 0;
for (int v : nums) sum += v;
if (sum % k != 0) return false;
int used = 0; // 使用位图技巧
int target = sum / k;
// k 号桶初始什么都没装,从 nums[0] 开始做选择
return backtrack(k, 0, nums, 0, used, target);
}
HashMap<Integer, Boolean> memo = new HashMap<>();
boolean backtrack(int k, int bucket,
int[] nums, int start, int used, int target) {
// base case
if (k == 0) {
// 所有桶都被装满了,而且 nums 一定全部用完了
return true;
}
if (bucket == target) {
// 装满了当前桶,递归穷举下一个桶的选择
// 让下一个桶从 nums[0] 开始选数字
boolean res = backtrack(k - 1, 0, nums, 0, used, target);
// 缓存结果
memo.put(used, res);
return res;
}
if (memo.containsKey(used)) {
// 避免冗余计算
return memo.get(used);
class Solution {
public boolean canPartitionKSubsets(int[] nums, int k) {
// 排除一些基本情况
if (k > nums.length) return false;
int sum = 0;
for (int v : nums) sum += v;
if (sum % k != 0) return false;
int used = 0; // 使用位图技巧
int target = sum / k;
// k 号桶初始什么都没装,从 nums[0] 开始做选择
return backtrack(k, 0, nums, 0, used, target);
}
for (int i = start; i < nums.length; i++) {
// 剪枝
if (((used >> i) & 1) == 1) { // 判断第 i 位是否是 1
// nums[i] 已经被装入别的桶中
continue;
HashMap<Integer, Boolean> memo = new HashMap<>();
boolean backtrack(int k, int bucket,
int[] nums, int start, int used, int target) {
// base case
if (k == 0) {
// 所有桶都被装满了,而且 nums 一定全部用完了
return true;
}
if (nums[i] + bucket > target) {
continue;
if (bucket == target) {
// 装满了当前桶,递归穷举下一个桶的选择
// 让下一个桶从 nums[0] 开始选数字
boolean res = backtrack(k - 1, 0, nums, 0, used, target);
// 缓存结果
memo.put(used, res);
return res;
}
// 做选择
used |= 1 << i; // 将第 i 位置为 1
bucket += nums[i];
// 递归穷举下一个数字是否装入当前桶
if (backtrack(k, bucket, nums, i + 1, used, target)) {
return true;
if (memo.containsKey(used)) {
// 避免冗余计算
return memo.get(used);
}
// 撤销选择
used ^= 1 << i; // 使用异或运算将第 i 位恢复 0
bucket -= nums[i];
}
return false;
for (int i = start; i < nums.length; i++) {
// 剪枝
if (((used >> i) & 1) == 1) { // 判断第 i 位是否是 1
// nums[i] 已经被装入别的桶中
continue;
}
if (nums[i] + bucket > target) {
continue;
}
// 做选择
used |= 1 << i; // 将第 i 位置为 1
bucket += nums[i];
// 递归穷举下一个数字是否装入当前桶
if (backtrack(k, bucket, nums, i + 1, used, target)) {
return true;
}
// 撤销选择
used ^= 1 << i; // 使用异或运算将第 i 位恢复 0
bucket -= nums[i];
}
return false;
}
}
```
......
......@@ -51,6 +51,7 @@ title: '一行代码就能解决的算法题'
这样一直循环下去,我们发现只要踩到 4 的倍数,就落入了圈套,永远逃不出 4 的倍数,而且一定会输。所以这道题的解法非常简单:
<!-- muliti_language -->
```java
boolean canWinNim(int n) {
// 如果上来就踩到 4 的倍数,那就认输吧
......@@ -85,6 +86,7 @@ boolean canWinNim(int n) {
这道题又涉及到两人的博弈,也可以用动态规划算法暴力试,比较麻烦。但我们只要对规则深入思考,就会大惊失色:只要你足够聪明,你是必胜无疑的,因为你是先手。
<!-- muliti_language -->
```java
boolean stoneGame(int[] piles) {
return true;
......@@ -119,6 +121,7 @@ boolean stoneGame(int[] piles) {
我们当然可以用一个布尔数组表示这些灯的开关情况,然后模拟这些操作过程,最后去数一下就能出结果。但是这样显得没有灵性,最好的解法是这样的:
<!-- muliti_language -->
```java
int bulbSwitch(int n) {
return (int)Math.sqrt(n);
......
......@@ -34,9 +34,11 @@ title: '二分查找高效判定子序列'
举两个例子:
```
s = "abc", t = "**a**h**b**gd**c**", return true.
s = "axc", t = "ahbgdc", return false.
```
题目很容易理解,而且看起来很简单,但很难想到这个问题跟二分查找有关吧?
......@@ -44,6 +46,7 @@ s = "axc", t = "ahbgdc", return false.
首先,一个很简单的解法是这样的:
<!-- muliti_language -->
```java
boolean isSubsequence(String s, String t) {
int i = 0, j = 0;
......@@ -67,6 +70,7 @@ boolean isSubsequence(String s, String t) {
如果给你一系列字符串 `s1,s2,...` 和字符串 `t`,你需要判定每个串 `s` 是否是 `t` 的子序列(可以假定 `s` 较短,`t` 很长)。
<!-- muliti_language -->
```java
boolean[] isSubsequence(String[] sn, String t);
```
......@@ -79,6 +83,7 @@ boolean[] isSubsequence(String[] sn, String t);
二分思路主要是对 `t` 进行预处理,用一个字典 `index` 将每个字符出现的索引位置按顺序存储下来:
<!-- muliti_language -->
```java
int m = s.length(), n = t.length();
ArrayList<Integer>[] index = new ArrayList[256];
......@@ -111,6 +116,7 @@ for (int i = 0; i < n; i++) {
什么意思呢,就是说如果在数组 `[0,1,3,4]` 中搜索元素 2,算法会返回索引 2,也就是元素 3 的位置,元素 3 是数组中大于 2 的最小元素。所以我们可以利用二分搜索避免线性扫描。
<!-- muliti_language -->
```java
// 查找左侧边界的二分查找
int left_bound(ArrayList<Integer> arr, int target) {
......@@ -134,6 +140,7 @@ int left_bound(ArrayList<Integer> arr, int target) {
这里以单个字符串 `s` 为例,对于多个字符串 `s`,可以把预处理部分抽出来。
<!-- muliti_language -->
```java
boolean isSubsequence(String s, String t) {
int m = s.length(), n = t.length();
......@@ -173,12 +180,14 @@ boolean isSubsequence(String s, String t) {
函数签名如下:
<!-- muliti_language -->
```java
int numMatchingSubseq(String s, String[] words)
```
我们直接把上一道题的代码稍微改改即可完成这道题:
<!-- muliti_language -->
```java
int numMatchingSubseq(String s, String[] words) {
// 对 s 进行预处理
......
......@@ -28,6 +28,7 @@ title: '如何高效判断回文链表'
**寻找**回文串的核心思想是从中心向两端扩展:
<!-- muliti_language -->
```java
// 在 s 中寻找以 s[left] 和 s[right] 为中心的最长回文串
String palindrome(String s, int left, int right) {
......@@ -47,6 +48,7 @@ String palindrome(String s, int left, int right) {
**判断**一个字符串是不是回文串就简单很多,不需要考虑奇偶情况,只需要[双指针技巧](https://labuladong.github.io/article/fname.html?fname=双指针技巧),从两端向中间逼近即可:
<!-- muliti_language -->
```java
boolean isPalindrome(String s) {
// 一左一右两个指针相向而行
......@@ -94,6 +96,7 @@ boolean isPalindrome(ListNode head);
对于二叉树的几种遍历方式,我们再熟悉不过了:
<!-- muliti_language -->
```java
void traverse(TreeNode root) {
// 前序遍历代码
......@@ -106,6 +109,7 @@ void traverse(TreeNode root) {
[学习数据结构的框架思维](https://labuladong.github.io/article/fname.html?fname=学习数据结构和算法的高效方法) 中说过,链表兼具递归结构,树结构不过是链表的衍生。那么,**链表其实也可以有前序遍历和后序遍历**
<!-- muliti_language -->
```java
void traverse(ListNode head) {
// 前序遍历代码
......@@ -116,6 +120,7 @@ void traverse(ListNode head) {
这个框架有什么指导意义呢?如果我想正序打印链表中的 `val` 值,可以在前序遍历位置写代码;反之,如果想倒序遍历链表,就可以在后序遍历位置操作:
<!-- muliti_language -->
```java
/* 倒序打印单链表中的元素值 */
void traverse(ListNode head) {
......@@ -128,6 +133,7 @@ void traverse(ListNode head) {
说到这了,其实可以稍作修改,模仿双指针实现回文判断的功能:
<!-- muliti_language -->
```java
// 左侧指针
ListNode left;
......@@ -159,6 +165,7 @@ boolean traverse(ListNode right) {
**1、先通过 [双指针技巧](https://labuladong.github.io/article/fname.html?fname=链表技巧) 中的快慢指针来找到链表的中点**
<!-- muliti_language -->
```java
ListNode slow, fast;
slow = fast = head;
......@@ -182,6 +189,7 @@ if (fast != null)
**3、从`slow`开始反转后面的链表,现在就可以开始比较回文串了**
<!-- muliti_language -->
```java
ListNode left = head;
ListNode right = reverse(slow);
......@@ -199,6 +207,7 @@ return true;
至此,把上面 3 段代码合在一起就高效地解决这个问题了,其中 `reverse` 函数很容易实现:
<!-- muliti_language -->
```java
boolean isPalindrome(ListNode head) {
ListNode slow, fast;
......
......@@ -33,6 +33,7 @@ title: '扫描线技巧解决会议室安排问题'
函数签名如下:
<!-- muliti_language -->
```java
// 返回需要申请的会议室数量
int minMeetingRooms(int[][] meetings);
......@@ -152,38 +153,42 @@ int minMeetingRooms(int[][] meetings) {
然后就简单了,扫描线从左向右前进,遇到红点就对计数器加一,遇到绿点就对计数器减一,计数器 `count` 的最大值就是答案:
<!-- muliti_language -->
```java
int minMeetingRooms(int[][] meetings) {
int n = meetings.length;
int[] begin = new int[n];
int[] end = new int[n];
for(int i = 0; i < n; i++) {
begin[i] = meetings[i][0];
end[i] = meetings[i][1];
}
Arrays.sort(begin);
Arrays.sort(end);
// 扫描过程中的计数器
int count = 0;
// 双指针技巧
int res = 0, i = 0, j = 0;
while (i < n && j < n) {
if (begin[i] < end[j]) {
// 扫描到一个红点
count++;
i++;
} else {
// 扫描到一个绿点
count--;
j++;
class Solution {
int minMeetingRooms(int[][] meetings) {
int n = meetings.length;
int[] begin = new int[n];
int[] end = new int[n];
for(int i = 0; i < n; i++) {
begin[i] = meetings[i][0];
end[i] = meetings[i][1];
}
Arrays.sort(begin);
Arrays.sort(end);
// 扫描过程中的计数器
int count = 0;
// 双指针技巧
int res = 0, i = 0, j = 0;
while (i < n && j < n) {
if (begin[i] < end[j]) {
// 扫描到一个红点
count++;
i++;
} else {
// 扫描到一个绿点
count--;
j++;
}
// 记录扫描过程中的最大值
res = Math.max(res, count);
}
// 记录扫描过程中的最大值
res = Math.max(res, count);
return res;
}
return res;
}
```
这里使用的是 [双指针技巧](https://labuladong.github.io/article/fname.html?fname=双指针技巧),根据 `i, j` 的相对位置模拟扫描线前进的过程。
......
......@@ -39,6 +39,7 @@ title: 'DFS 算法秒杀岛屿系列题目'
根据 [学习数据结构和算法的框架思维](https://labuladong.github.io/article/fname.html?fname=学习数据结构和算法的高效方法),完全可以根据二叉树的遍历框架改写出二维矩阵的 DFS 代码框架:
<!-- muliti_language -->
```java
// 二叉树遍历框架
void traverse(TreeNode root) {
......@@ -70,6 +71,7 @@ void dfs(int[][] grid, int i, int j, boolean[][] visited) {
这里额外说一个处理二维数组的常用小技巧,你有时会看到使用「方向数组」来处理上下左右的遍历,和前文 [图遍历框架](https://labuladong.github.io/article/fname.html?fname=图) 的代码很类似:
<!-- muliti_language -->
```java
// 方向数组,分别代表上、下、左、右
int[][] dirs = new int[][]{{-1,0}, {1,0}, {0,-1}, {0,1}};
......@@ -105,6 +107,7 @@ void dfs(int[][] grid, int i, int j, boolean[][] visited) {
我们说连成片的陆地形成岛屿,那么请你写一个算法,计算这个矩阵 `grid` 中岛屿的个数,函数签名如下:
<!-- muliti_language -->
```java
int numIslands(char[][] grid);
```
......@@ -115,43 +118,46 @@ int numIslands(char[][] grid);
思路很简单,关键在于如何寻找并标记「岛屿」,这就要 DFS 算法发挥作用了,我们直接看解法代码:
<!-- muliti_language -->
```java
// 主函数,计算岛屿数量
int numIslands(char[][] grid) {
int res = 0;
int m = grid.length, n = grid[0].length;
// 遍历 grid
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '1') {
// 每发现一个岛屿,岛屿数量加一
res++;
// 然后使用 DFS 将岛屿淹了
dfs(grid, i, j);
class Solution {
// 主函数,计算岛屿数量
int numIslands(char[][] grid) {
int res = 0;
int m = grid.length, n = grid[0].length;
// 遍历 grid
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '1') {
// 每发现一个岛屿,岛屿数量加一
res++;
// 然后使用 DFS 将岛屿淹了
dfs(grid, i, j);
}
}
}
return res;
}
return res;
}
// 从 (i, j) 开始,将与之相邻的陆地都变成海水
void dfs(char[][] grid, int i, int j) {
int m = grid.length, n = grid[0].length;
if (i < 0 || j < 0 || i >= m || j >= n) {
// 超出索引边界
return;
}
if (grid[i][j] == '0') {
// 已经是海水了
return;
// 从 (i, j) 开始,将与之相邻的陆地都变成海水
void dfs(char[][] grid, int i, int j) {
int m = grid.length, n = grid[0].length;
if (i < 0 || j < 0 || i >= m || j >= n) {
// 超出索引边界
return;
}
if (grid[i][j] == '0') {
// 已经是海水了
return;
}
// 将 (i, j) 变成海水
grid[i][j] = '0';
// 淹没上下左右的陆地
dfs(grid, i + 1, j);
dfs(grid, i, j + 1);
dfs(grid, i - 1, j);
dfs(grid, i, j - 1);
}
// 将 (i, j) 变成海水
grid[i][j] = '0';
// 淹没上下左右的陆地
dfs(grid, i + 1, j);
dfs(grid, i, j + 1);
dfs(grid, i - 1, j);
dfs(grid, i, j - 1);
}
```
......@@ -175,6 +181,7 @@ void dfs(char[][] grid, int i, int j) {
函数签名如下:
<!-- muliti_language -->
```java
int closedIsland(int[][] grid)
```
......@@ -189,52 +196,55 @@ int closedIsland(int[][] grid)
有了这个思路,就可以直接看代码了,注意这题规定 `0` 表示陆地,用 `1` 表示海水:
<!-- muliti_language -->
```java
// 主函数:计算封闭岛屿的数量
int closedIsland(int[][] grid) {
int m = grid.length, n = grid[0].length;
for (int j = 0; j < n; j++) {
// 把靠上边的岛屿淹掉
dfs(grid, 0, j);
// 把靠下边的岛屿淹掉
dfs(grid, m - 1, j);
}
for (int i = 0; i < m; i++) {
// 把靠左边的岛屿淹掉
dfs(grid, i, 0);
// 把靠右边的岛屿淹掉
dfs(grid, i, n - 1);
}
// 遍历 grid,剩下的岛屿都是封闭岛屿
int res = 0;
for (int i = 0; i < m; i++) {
class Solution {
// 主函数:计算封闭岛屿的数量
int closedIsland(int[][] grid) {
int m = grid.length, n = grid[0].length;
for (int j = 0; j < n; j++) {
if (grid[i][j] == 0) {
res++;
dfs(grid, i, j);
// 把靠上边的岛屿淹掉
dfs(grid, 0, j);
// 把靠下边的岛屿淹掉
dfs(grid, m - 1, j);
}
for (int i = 0; i < m; i++) {
// 把靠左边的岛屿淹掉
dfs(grid, i, 0);
// 把靠右边的岛屿淹掉
dfs(grid, i, n - 1);
}
// 遍历 grid,剩下的岛屿都是封闭岛屿
int res = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 0) {
res++;
dfs(grid, i, j);
}
}
}
return res;
}
return res;
}
// 从 (i, j) 开始,将与之相邻的陆地都变成海水
void dfs(int[][] grid, int i, int j) {
int m = grid.length, n = grid[0].length;
if (i < 0 || j < 0 || i >= m || j >= n) {
return;
}
if (grid[i][j] == 1) {
// 已经是海水了
return;
// 从 (i, j) 开始,将与之相邻的陆地都变成海水
void dfs(int[][] grid, int i, int j) {
int m = grid.length, n = grid[0].length;
if (i < 0 || j < 0 || i >= m || j >= n) {
return;
}
if (grid[i][j] == 1) {
// 已经是海水了
return;
}
// 将 (i, j) 变成海水
grid[i][j] = 1;
// 淹没上下左右的陆地
dfs(grid, i + 1, j);
dfs(grid, i, j + 1);
dfs(grid, i - 1, j);
dfs(grid, i, j - 1);
}
// 将 (i, j) 变成海水
grid[i][j] = 1;
// 淹没上下左右的陆地
dfs(grid, i + 1, j);
dfs(grid, i, j + 1);
dfs(grid, i - 1, j);
dfs(grid, i, j - 1);
}
```
......@@ -246,36 +256,40 @@ void dfs(int[][] grid, int i, int j) {
其实思路都是一样的,先把靠边的陆地淹掉,然后去数剩下的陆地数量就行了,注意第 1020 题中 `1` 代表陆地,`0` 代表海水:
<!-- muliti_language -->
```java
int numEnclaves(int[][] grid) {
int m = grid.length, n = grid[0].length;
// 淹掉靠边的陆地
for (int i = 0; i < m; i++) {
dfs(grid, i, 0);
dfs(grid, i, n - 1);
}
for (int j = 0; j < n; j++) {
dfs(grid, 0, j);
dfs(grid, m - 1, j);
}
// 数一数剩下的陆地
int res = 0;
for (int i = 0; i < m; i++) {
class Solution {
int numEnclaves(int[][] grid) {
int m = grid.length, n = grid[0].length;
// 淹掉靠边的陆地
for (int i = 0; i < m; i++) {
dfs(grid, i, 0);
dfs(grid, i, n - 1);
}
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) {
res += 1;
dfs(grid, 0, j);
dfs(grid, m - 1, j);
}
// 数一数剩下的陆地
int res = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) {
res += 1;
}
}
}
return res;
}
return res;
// 和之前的实现类似
void dfs(int[][] grid, int i, int j) {
// ...
}
}
// 和之前的实现类似
void dfs(int[][] grid, int i, int j) {
// ...
}
```
篇幅所限,具体代码我就不写了,我们继续看其他的岛屿题目。
......@@ -284,6 +298,7 @@ void dfs(int[][] grid, int i, int j) {
这是力扣第 695 题「岛屿的最大面积」,`0` 表示海水,`1` 表示陆地,现在不让你计算岛屿的个数了,而是让你计算最大的那个岛屿的面积,函数签名如下:
<!-- muliti_language -->
```java
int maxAreaOfIsland(int[][] grid)
```
......@@ -298,40 +313,43 @@ int maxAreaOfIsland(int[][] grid)
我们可以给 `dfs` 函数设置返回值,记录每次淹没的陆地的个数,直接看解法吧:
<!-- muliti_language -->
```java
int maxAreaOfIsland(int[][] grid) {
// 记录岛屿的最大面积
int res = 0;
int m = grid.length, n = grid[0].length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) {
// 淹没岛屿,并更新最大岛屿面积
res = Math.max(res, dfs(grid, i, j));
class Solution {
int maxAreaOfIsland(int[][] grid) {
// 记录岛屿的最大面积
int res = 0;
int m = grid.length, n = grid[0].length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) {
// 淹没岛屿,并更新最大岛屿面积
res = Math.max(res, dfs(grid, i, j));
}
}
}
return res;
}
return res;
}
// 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积
int dfs(int[][] grid, int i, int j) {
int m = grid.length, n = grid[0].length;
if (i < 0 || j < 0 || i >= m || j >= n) {
// 超出索引边界
return 0;
}
if (grid[i][j] == 0) {
// 已经是海水了
return 0;
}
// 将 (i, j) 变成海水
grid[i][j] = 0;
// 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积
int dfs(int[][] grid, int i, int j) {
int m = grid.length, n = grid[0].length;
if (i < 0 || j < 0 || i >= m || j >= n) {
// 超出索引边界
return 0;
}
if (grid[i][j] == 0) {
// 已经是海水了
return 0;
}
// 将 (i, j) 变成海水
grid[i][j] = 0;
return dfs(grid, i + 1, j)
+ dfs(grid, i, j + 1)
+ dfs(grid, i - 1, j)
+ dfs(grid, i, j - 1) + 1;
return dfs(grid, i + 1, j)
+ dfs(grid, i, j + 1)
+ dfs(grid, i - 1, j)
+ dfs(grid, i, j - 1) + 1;
}
}
```
......@@ -355,45 +373,48 @@ int dfs(int[][] grid, int i, int j) {
依据这个思路,可以直接写出下面的代码:
<!-- muliti_language -->
```java
int countSubIslands(int[][] grid1, int[][] grid2) {
int m = grid1.length, n = grid1[0].length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid1[i][j] == 0 && grid2[i][j] == 1) {
// 这个岛屿肯定不是子岛,淹掉
dfs(grid2, i, j);
class Solution {
int countSubIslands(int[][] grid1, int[][] grid2) {
int m = grid1.length, n = grid1[0].length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid1[i][j] == 0 && grid2[i][j] == 1) {
// 这个岛屿肯定不是子岛,淹掉
dfs(grid2, i, j);
}
}
}
}
// 现在 grid2 中剩下的岛屿都是子岛,计算岛屿数量
int res = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid2[i][j] == 1) {
res++;
dfs(grid2, i, j);
// 现在 grid2 中剩下的岛屿都是子岛,计算岛屿数量
int res = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid2[i][j] == 1) {
res++;
dfs(grid2, i, j);
}
}
}
return res;
}
return res;
}
// 从 (i, j) 开始,将与之相邻的陆地都变成海水
void dfs(int[][] grid, int i, int j) {
int m = grid.length, n = grid[0].length;
if (i < 0 || j < 0 || i >= m || j >= n) {
return;
}
if (grid[i][j] == 0) {
return;
}
// 从 (i, j) 开始,将与之相邻的陆地都变成海水
void dfs(int[][] grid, int i, int j) {
int m = grid.length, n = grid[0].length;
if (i < 0 || j < 0 || i >= m || j >= n) {
return;
}
if (grid[i][j] == 0) {
return;
}
grid[i][j] = 0;
dfs(grid, i + 1, j);
dfs(grid, i, j + 1);
dfs(grid, i - 1, j);
dfs(grid, i, j - 1);
grid[i][j] = 0;
dfs(grid, i + 1, j);
dfs(grid, i, j + 1);
dfs(grid, i - 1, j);
dfs(grid, i, j - 1);
}
}
```
......@@ -405,6 +426,7 @@ void dfs(int[][] grid, int i, int j) {
力扣第 694 题「不同的岛屿数量」,题目还是输入一个二维矩阵,`0` 表示海水,`1` 表示陆地,这次让你计算 **不同的 (distinct)** 岛屿数量,函数签名如下:
<!-- muliti_language -->
```java
int numDistinctIslands(int[][] grid)
```
......@@ -423,6 +445,7 @@ int numDistinctIslands(int[][] grid)
因为遍历顺序是写死在你的递归函数里面的,不会动态改变:
<!-- muliti_language -->
```java
void dfs(int[][] grid, int i, int j) {
// 递归顺序:
......@@ -453,6 +476,7 @@ void dfs(int[][] grid, int i, int j) {
所以我们需要稍微改造 `dfs` 函数,添加一些函数参数以便记录遍历顺序:
<!-- muliti_language -->
```java
void dfs(int[][] grid, int i, int j, StringBuilder sb, int dir) {
int m = grid.length, n = grid[0].length;
......@@ -476,6 +500,7 @@ void dfs(int[][] grid, int i, int j, StringBuilder sb, int dir) {
`dir` 记录方向,`dfs` 函数递归结束后,`sb` 记录着整个遍历顺序。有了这个 `dfs` 函数就好办了,我们可以直接写出最后的解法代码:
<!-- muliti_language -->
```java
int numDistinctIslands(int[][] grid) {
int m = grid.length, n = grid[0].length;
......
......@@ -33,6 +33,7 @@ title: '如何调度考生的座位'
也就是请你实现下面这样一个类:
<!-- muliti_language -->
```java
class ExamRoom {
// 构造函数,传入座位总数 N
......@@ -87,47 +88,52 @@ class ExamRoom {
这个问题还用到一个常用的编程技巧,就是使用一个「虚拟线段」让算法正确启动,这就和链表相关的算法需要「虚拟头结点」一个道理。
<!-- muliti_language -->
```java
// 将端点 p 映射到以 p 为左端点的线段
private Map<Integer, int[]> startMap;
// 将端点 p 映射到以 p 为右端点的线段
private Map<Integer, int[]> endMap;
// 根据线段长度从小到大存放所有线段
private TreeSet<int[]> pq;
private int N;
public ExamRoom(int N) {
this.N = N;
startMap = new HashMap<>();
endMap = new HashMap<>();
pq = new TreeSet<>((a, b) -> {
// 算出两个线段的长度
int distA = distance(a);
int distB = distance(b);
// 长度更长的更大,排后面
return distA - distB;
});
// 在有序集合中先放一个虚拟线段
addInterval(new int[] {-1, N});
}
class ExamRoom {
// 将端点 p 映射到以 p 为左端点的线段
private Map<Integer, int[]> startMap;
// 将端点 p 映射到以 p 为右端点的线段
private Map<Integer, int[]> endMap;
// 根据线段长度从小到大存放所有线段
private TreeSet<int[]> pq;
private int N;
public ExamRoom(int N) {
this.N = N;
startMap = new HashMap<>();
endMap = new HashMap<>();
pq = new TreeSet<>((a, b) -> {
// 算出两个线段的长度
int distA = distance(a);
int distB = distance(b);
// 长度更长的更大,排后面
return distA - distB;
});
// 在有序集合中先放一个虚拟线段
addInterval(new int[] {-1, N});
}
/* 去除一个线段 */
private void removeInterval(int[] intv) {
pq.remove(intv);
startMap.remove(intv[0]);
endMap.remove(intv[1]);
}
/* 去除一个线段 */
private void removeInterval(int[] intv) {
pq.remove(intv);
startMap.remove(intv[0]);
endMap.remove(intv[1]);
}
/* 增加一个线段 */
private void addInterval(int[] intv) {
pq.add(intv);
startMap.put(intv[0], intv);
endMap.put(intv[1], intv);
}
/* 增加一个线段 */
private void addInterval(int[] intv) {
pq.add(intv);
startMap.put(intv[0], intv);
endMap.put(intv[1], intv);
}
/* 计算一个线段的长度 */
private int distance(int[] intv) {
return intv[1] - intv[0] - 1;
/* 计算一个线段的长度 */
private int distance(int[] intv) {
return intv[1] - intv[0] - 1;
}
// ...
}
```
......@@ -138,37 +144,41 @@ private int distance(int[] intv) {
有了上述铺垫,主要 API `seat``leave` 就可以写了:
```java
public int seat() {
// 从有序集合拿出最长的线段
int[] longest = pq.last();
int x = longest[0];
int y = longest[1];
int seat;
if (x == -1) { // 情况一
seat = 0;
} else if (y == N) { // 情况二
seat = N - 1;
} else { // 情况三
seat = (y - x) / 2 + x;
class ExamRoom {
// ...
public int seat() {
// 从有序集合拿出最长的线段
int[] longest = pq.last();
int x = longest[0];
int y = longest[1];
int seat;
if (x == -1) { // 情况一
seat = 0;
} else if (y == N) { // 情况二
seat = N - 1;
} else { // 情况三
seat = (y - x) / 2 + x;
}
// 将最长的线段分成两段
int[] left = new int[] {x, seat};
int[] right = new int[] {seat, y};
removeInterval(longest);
addInterval(left);
addInterval(right);
return seat;
}
// 将最长的线段分成两段
int[] left = new int[] {x, seat};
int[] right = new int[] {seat, y};
removeInterval(longest);
addInterval(left);
addInterval(right);
return seat;
}
public void leave(int p) {
// 将 p 左右的线段找出来
int[] right = startMap.get(p);
int[] left = endMap.get(p);
// 合并两个线段成为一个线段
int[] merged = new int[] {left[0], right[1]};
removeInterval(left);
removeInterval(right);
addInterval(merged);
public void leave(int p) {
// 将 p 左右的线段找出来
int[] right = startMap.get(p);
int[] left = endMap.get(p);
// 合并两个线段成为一个线段
int[] merged = new int[] {left[0], right[1]};
removeInterval(left);
removeInterval(right);
addInterval(merged);
}
}
```
......@@ -201,14 +211,19 @@ pq = new TreeSet<>((a, b) -> {
除此之外,还要改变 `distance` 函数,**不能简单地让它计算一个线段两个端点间的长度,而是让它计算该线段中点和端点之间的长度**
<!-- muliti_language -->
```java
private int distance(int[] intv) {
int x = intv[0];
int y = intv[1];
if (x == -1) return y;
if (y == N) return N - 1 - x;
// 中点和端点之间的长度
return (y - x) / 2;
class ExamRoom {
// ...
private int distance(int[] intv) {
int x = intv[0];
int y = intv[1];
if (x == -1) return y;
if (y == N) return N - 1 - x;
// 中点和端点之间的长度
return (y - x) / 2;
}
}
```
......
......@@ -29,6 +29,7 @@ title: '如何高效寻找素数'
比如力扣第 204 题「计数质数」,让你写这样一个函数:
<!-- muliti_language -->
```java
// 返回区间 [2, n) 中有几个素数
int countPrimes(int n)
......@@ -39,6 +40,7 @@ int countPrimes(int n)
你会如何写这个函数?我想大家应该会这样写:
<!-- muliti_language -->
```java
int countPrimes(int n) {
int count = 0;
......@@ -100,6 +102,7 @@ Wikipedia 的这个 GIF 很形象:
看到这里,你是否有点明白这个排除法的逻辑了呢?先看我们的第一版代码:
<!-- muliti_language -->
```java
int countPrimes(int n) {
boolean[] isPrime = new boolean[n];
......@@ -150,21 +153,25 @@ for (int j = i * i; j < n; j += i)
这样,素数计数的算法就高效实现了,其实这个算法有一个名字,叫做 Sieve of Eratosthenes。看下完整的最终代码:
<!-- muliti_language -->
```java
int countPrimes(int n) {
boolean[] isPrime = new boolean[n];
Arrays.fill(isPrime, true);
for (int i = 2; i * i < n; i++)
if (isPrime[i])
for (int j = i * i; j < n; j += i)
isPrime[j] = false;
int count = 0;
for (int i = 2; i < n; i++)
if (isPrime[i]) count++;
return count;
class Solution {
public int countPrimes(int n) {
boolean[] isPrime = new boolean[n];
Arrays.fill(isPrime, true);
for (int i = 2; i * i < n; i++)
if (isPrime[i])
for (int j = i * i; j < n; j += i)
isPrime[j] = false;
int count = 0;
for (int i = 2; i < n; i++)
if (isPrime[i]) count++;
return count;
}
}
```
**该算法的时间复杂度比较难算**,显然时间跟这两个嵌套的 for 循环有关,其操作数应该是:
......
......@@ -34,6 +34,7 @@ title: '接雨水问题详解'
就是用一个数组表示一个条形图,问你这个条形图最多能接多少水。
<!-- muliti_language -->
```java
int trap(int[] height);
```
......@@ -70,6 +71,7 @@ water[i] = min(
这就是本问题的核心思路,我们可以简单写一个暴力算法:
<!-- muliti_language -->
```java
int trap(int[] height) {
int n = height.length;
......@@ -98,29 +100,32 @@ int trap(int[] height) {
**我们开两个数组 `r_max` 和 `l_max` 充当备忘录,`l_max[i]` 表示位置 `i` 左边最高的柱子高度,`r_max[i]` 表示位置 `i` 右边最高的柱子高度**。预先把这两个数组计算好,避免重复计算:
<!-- muliti_language -->
```java
int trap(int[] height) {
if (height.length == 0) {
return 0;
class Solution {
int trap(int[] height) {
if (height.length == 0) {
return 0;
}
int n = height.length;
int res = 0;
// 数组充当备忘录
int[] l_max = new int[n];
int[] r_max = new int[n];
// 初始化 base case
l_max[0] = height[0];
r_max[n - 1] = height[n - 1];
// 从左向右计算 l_max
for (int i = 1; i < n; i++)
l_max[i] = Math.max(height[i], l_max[i - 1]);
// 从右向左计算 r_max
for (int i = n - 2; i >= 0; i--)
r_max[i] = Math.max(height[i], r_max[i + 1]);
// 计算答案
for (int i = 1; i < n - 1; i++)
res += Math.min(l_max[i], r_max[i]) - height[i];
return res;
}
int n = height.length;
int res = 0;
// 数组充当备忘录
int[] l_max = new int[n];
int[] r_max = new int[n];
// 初始化 base case
l_max[0] = height[0];
r_max[n - 1] = height[n - 1];
// 从左向右计算 l_max
for (int i = 1; i < n; i++)
l_max[i] = Math.max(height[i], l_max[i - 1]);
// 从右向左计算 r_max
for (int i = n - 2; i >= 0; i--)
r_max[i] = Math.max(height[i], r_max[i + 1]);
// 计算答案
for (int i = 1; i < n - 1; i++)
res += Math.min(l_max[i], r_max[i]) - height[i];
return res;
}
```
......@@ -132,6 +137,7 @@ int trap(int[] height) {
首先,看一部分代码:
<!-- muliti_language -->
```java
int trap(int[] height) {
int left = 0, right = height.length - 1;
......@@ -152,26 +158,29 @@ int trap(int[] height) {
明白了这一点,直接看解法:
<!-- muliti_language -->
```java
int trap(int[] height) {
int left = 0, right = height.length - 1;
int l_max = 0, r_max = 0;
class Solution {
int trap(int[] height) {
int left = 0, right = height.length - 1;
int l_max = 0, r_max = 0;
int res = 0;
while (left < right) {
l_max = Math.max(l_max, height[left]);
r_max = Math.max(r_max, height[right]);
int res = 0;
while (left < right) {
l_max = Math.max(l_max, height[left]);
r_max = Math.max(r_max, height[right]);
// res += min(l_max, r_max) - height[i]
if (l_max < r_max) {
res += l_max - height[left];
left++;
} else {
res += r_max - height[right];
right--;
// res += min(l_max, r_max) - height[i]
if (l_max < r_max) {
res += l_max - height[left];
left++;
} else {
res += r_max - height[right];
right--;
}
}
return res;
}
return res;
}
```
......@@ -212,6 +221,7 @@ if (l_max < r_max) {
函数签名如下:
<!-- muliti_language -->
```java
int maxArea(int[] height);
```
......@@ -242,22 +252,25 @@ min(height[left], height[right]) * (right - left)
先直接看解法代码吧:
<!-- muliti_language -->
```java
int maxArea(int[] height) {
int left = 0, right = height.length - 1;
int res = 0;
while (left < right) {
// [left, right] 之间的矩形面积
int cur_area = Math.min(height[left], height[right]) * (right - left);
res = Math.max(res, cur_area);
// 双指针技巧,移动较低的一边
if (height[left] < height[right]) {
left++;
} else {
right--;
class Solution {
public int maxArea(int[] height) {
int left = 0, right = height.length - 1;
int res = 0;
while (left < right) {
// [left, right] 之间的矩形面积
int cur_area = Math.min(height[left], height[right]) * (right - left);
res = Math.max(res, cur_area);
// 双指针技巧,移动较低的一边
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return res;
}
return res;
}
```
......
......@@ -40,6 +40,7 @@ title: '随机算法之水塘抽样算法'
**先说结论,当你遇到第 `i` 个元素时,应该有 `1/i` 的概率选择该元素,`1 - 1/i` 的概率保持原有的选择**。看代码容易理解这个思路:
<!-- muliti_language -->
```java
/* 返回链表中一个随机节点的值 */
int getRandom(ListNode head) {
......@@ -72,6 +73,7 @@ int getRandom(ListNode head) {
**同理,如果要随机选择 `k` 个数,只要在第 `i` 个元素处以 `k/i` 的概率选择该元素,以 `1 - k/i` 的概率保持原有选择即可**。代码如下:
<!-- muliti_language -->
```java
/* 返回链表中 k 个随机节点的值 */
int[] getRandom(ListNode head, int k) {
......
......@@ -29,6 +29,7 @@ title: '如何寻找缺失和重复的元素'
给一个长度为 `N` 的数组 `nums`,其中本来装着 `[1..N]``N` 个元素,无序。但是现在出现了一些错误,`nums` 中的一个元素出现了重复,也就同时导致了另一个元素的缺失。请你写一个算法,找到 `nums` 中的重复元素和缺失元素的值。
<!-- muliti_language -->
```java
// 返回两个数字,分别是 {dup, missing}
int[] findErrorNums(int[] nums);
......@@ -70,6 +71,7 @@ O(N) 的时间复杂度遍历数组是无法避免的,所以我们可以想想
对于这个现象,我们就可以翻译成代码了:
<!-- muliti_language -->
```java
int[] findErrorNums(int[] nums) {
int n = nums.length;
......@@ -95,6 +97,7 @@ int[] findErrorNums(int[] nums) {
这个问题就基本解决了,别忘了我们刚才为了方便分析,假设元素是 `[0..N-1]`,但题目要求是 `[1..N]`,所以只要简单修改两处地方即可得到原题的答案:
<!-- muliti_language -->
```java
int[] findErrorNums(int[] nums) {
int n = nums.length;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册