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

update content

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