valToIndex = new HashMap<>();
+
+ public TreeNode constructFromPrePost(int[] preorder, int[] postorder) {
+ for (int i = 0; i < postorder.length; i++) {
+ valToIndex.put(postorder[i], i);
+ }
+ return build(preorder, 0, preorder.length - 1,
+ postorder, 0, postorder.length - 1);
+ }
+
+ // 定义:根据 preorder[preStart..preEnd] 和 postorder[postStart..postEnd]
+ // 构建二叉树,并返回根节点。
+ TreeNode build(int[] preorder, int preStart, int preEnd,
+ int[] postorder, int postStart, int postEnd) {
+ if (preStart > preEnd) {
+ return null;
+ }
+ if (preStart == preEnd) {
+ return new TreeNode(preorder[preStart]);
+ }
+
+ // root 节点对应的值就是前序遍历数组的第一个元素
+ int rootVal = preorder[preStart];
+ // root.left 的值是前序遍历第二个元素
+ // 通过前序和后序遍历构造二叉树的关键在于通过左子树的根节点
+ // 确定 preorder 和 postorder 中左右子树的元素区间
+ int leftRootVal = preorder[preStart + 1];
+ // leftRootVal 在后序遍历数组中的索引
+ int index = valToIndex.get(leftRootVal);
+ // 左子树的元素个数
+ int leftSize = index - postStart + 1;
+
+ // 先构造出当前根节点
+ TreeNode root = new TreeNode(rootVal);
+ // 递归构造左右子树
+ // 根据左子树的根节点索引和元素个数推导左右子树的索引边界
+ root.left = build(preorder, preStart + 1, preStart + leftSize,
+ postorder, postStart, index);
+ root.right = build(preorder, preStart + leftSize + 1, preEnd,
+ postorder, index + 1, postEnd - 1);
+
+ return root;
+ }
+}
+```
+
+代码和前两道题非常类似,我们可以看着代码思考一下,为什么通过前序遍历和后序遍历结果还原的二叉树可能不唯一呢?
+
+关键在这一句:
+
+```java
+int leftRootVal = preorder[preStart + 1];
+```
+
+我们假设前序遍历的第二个元素是左子树的根节点,但实际上左子树有可能是空指针,那么这个元素就应该是右子树的根节点。由于这里无法确切进行判断,所以导致了最终答案的不唯一。
+
+至此,通过前序和后序遍历结果还原二叉树的问题也解决了。
+
+最后呼应下前文,**二叉树的构造问题一般都是使用「分解问题」的思路:构造整棵树 = 根节点 + 构造左子树 + 构造右子树**。先找出根节点,然后根据根节点的值找到左右子树的元素,进而递归构建出左右子树。
+
+现在你是否明白其中的玄妙了呢?
+
+接下来可阅读:
+
+* [手把手刷二叉树(第三期)](https://labuladong.github.io/article/fname.html?fname=二叉树系列3)
+
+
+
+
+
+引用本文的文章
+
+ - [东哥带你刷二叉搜索树(特性篇)](https://labuladong.github.io/article/fname.html?fname=BST1)
+ - [东哥带你刷二叉树(序列化篇)](https://labuladong.github.io/article/fname.html?fname=二叉树的序列化)
+ - [东哥带你刷二叉树(思路篇)](https://labuladong.github.io/article/fname.html?fname=二叉树系列1)
+ - [二叉树的递归转迭代的代码框架](https://labuladong.github.io/article/fname.html?fname=迭代遍历二叉树)
+ - [我的刷题心得](https://labuladong.github.io/article/fname.html?fname=算法心得)
+
+
+
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| [1008. Construct Binary Search Tree from Preorder Traversal](https://leetcode.com/problems/construct-binary-search-tree-from-preorder-traversal/?show=1) | [1008. 前序遍历构造二叉搜索树](https://leetcode.cn/problems/construct-binary-search-tree-from-preorder-traversal/?show=1) |
+| - | [剑指 Offer 07. 重建二叉树](https://leetcode.cn/problems/zhong-jian-er-cha-shu-lcof/?show=1) |
+
+
+
+
+
+**_____________**
+
+**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
+
+![](https://labuladong.github.io/algo/images/souyisou2.png)
\ No newline at end of file
diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\346\240\210.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\346\240\210.md"
index 3320be90256844f0ac5a2a417001401a0204494c..d468ed2c3d9dd70b09319b475c8257279e0d32eb 100644
--- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\346\240\210.md"
+++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\346\240\210.md"
@@ -1,11 +1,5 @@
# 特殊数据结构:单调栈
-
-
-
-
-
-
@@ -15,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -203,6 +197,43 @@ int[] nextGreaterElements(int[] nums) {
我会在 [单调栈的几种变体](https://appktavsiei5995.pc.xiaoe-tech.com/detail/i_628dc1ace4b09dda126cf793/1) 对比单调栈的几种其他形式,并在 [单调栈的运用](https://appktavsiei5995.pc.xiaoe-tech.com/detail/i_628dc2d7e4b0cedf38b67734/1) 中给出单调栈的经典例题。
+
+
+
+
+引用本文的文章
+
+ - [一个方法团灭 LeetCode 打家劫舍问题](https://labuladong.github.io/article/fname.html?fname=抢房子)
+ - [一道数组去重的算法题把我整不会了](https://labuladong.github.io/article/fname.html?fname=单调栈去重)
+ - [单调栈代码模板的几种变体](https://appktavsiei5995.pc.xiaoe-tech.com/detail/i_628dc1ace4b09dda126cf793/1)
+ - [单调队列的通用实现及经典习题](https://appktavsiei5995.pc.xiaoe-tech.com/detail/i_62a692efe4b01a48520b9b9b/1)
+ - [单调队列结构解决滑动窗口问题](https://labuladong.github.io/article/fname.html?fname=单调队列)
+ - [数据结构设计:最大栈](https://labuladong.github.io/article/fname.html?fname=最大栈)
+
+
+
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| [1019. Next Greater Node In Linked List](https://leetcode.com/problems/next-greater-node-in-linked-list/?show=1) | [1019. 链表中的下一个更大节点](https://leetcode.cn/problems/next-greater-node-in-linked-list/?show=1) |
+| [1944. Number of Visible People in a Queue](https://leetcode.com/problems/number-of-visible-people-in-a-queue/?show=1) | [1944. 队列中可以看到的人数](https://leetcode.cn/problems/number-of-visible-people-in-a-queue/?show=1) |
+| [402. Remove K Digits](https://leetcode.com/problems/remove-k-digits/?show=1) | [402. 移掉 K 位数字](https://leetcode.cn/problems/remove-k-digits/?show=1) |
+| [42. Trapping Rain Water](https://leetcode.com/problems/trapping-rain-water/?show=1) | [42. 接雨水](https://leetcode.cn/problems/trapping-rain-water/?show=1) |
+| [901. Online Stock Span](https://leetcode.com/problems/online-stock-span/?show=1) | [901. 股票价格跨度](https://leetcode.cn/problems/online-stock-span/?show=1) |
+| [918. Maximum Sum Circular Subarray](https://leetcode.com/problems/maximum-sum-circular-subarray/?show=1) | [918. 环形子数组的最大和](https://leetcode.cn/problems/maximum-sum-circular-subarray/?show=1) |
+
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\351\230\237\345\210\227.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\351\230\237\345\210\227.md"
index 13a6c31df46ca3357d8ee84db2db3602eeceece7..0ac6df5988784c13d0374ae4681e73c4b574915f 100644
--- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\351\230\237\345\210\227.md"
+++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\351\230\237\345\210\227.md"
@@ -1,11 +1,5 @@
# 特殊数据结构:单调队列
-
-
-
-
-
-
@@ -15,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -270,6 +264,38 @@ class MonotonicQueue> {
我将在 [单调队列通用实现及应用](https://appktavsiei5995.pc.xiaoe-tech.com/detail/i_62a692efe4b01a48520b9b9b/1) 中给出单调队列的通用实现和经典习题。
+
+
+
+
+引用本文的文章
+
+ - [数据结构设计:最大栈](https://labuladong.github.io/article/fname.html?fname=最大栈)
+ - [算法时空复杂度分析实用指南](https://labuladong.github.io/article/fname.html?fname=时间复杂度)
+
+
+
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| [1425. Constrained Subsequence Sum](https://leetcode.com/problems/constrained-subsequence-sum/?show=1) | [1425. 带限制的子序列和](https://leetcode.cn/problems/constrained-subsequence-sum/?show=1) |
+| [1696. Jump Game VI](https://leetcode.com/problems/jump-game-vi/?show=1) | [1696. 跳跃游戏 VI](https://leetcode.cn/problems/jump-game-vi/?show=1) |
+| [862. Shortest Subarray with Sum at Least K](https://leetcode.com/problems/shortest-subarray-with-sum-at-least-k/?show=1) | [862. 和至少为 K 的最短子数组](https://leetcode.cn/problems/shortest-subarray-with-sum-at-least-k/?show=1) |
+| [918. Maximum Sum Circular Subarray](https://leetcode.com/problems/maximum-sum-circular-subarray/?show=1) | [918. 环形子数组的最大和](https://leetcode.cn/problems/maximum-sum-circular-subarray/?show=1) |
+| - | [剑指 Offer 59 - I. 滑动窗口的最大值](https://leetcode.cn/problems/hua-dong-chuang-kou-de-zui-da-zhi-lcof/?show=1) |
+
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\233\276.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\233\276.md"
new file mode 100644
index 0000000000000000000000000000000000000000..d31d28ec99e853755d4050b6285a962eb64432cf
--- /dev/null
+++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\233\276.md"
@@ -0,0 +1,359 @@
+# 图论算法基础
+
+
+
+
+
+
+
+
+![](https://labuladong.github.io/algo/images/souyisou1.png)
+
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
+
+
+
+读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
+
+| LeetCode | 力扣 | 难度 |
+| :----: | :----: | :----: |
+| [797. All Paths From Source to Target](https://leetcode.com/problems/all-paths-from-source-to-target/) | [797. 所有可能的路径](https://leetcode.cn/problems/all-paths-from-source-to-target/) | 🟠
+| - | [剑指 Offer II 110. 所有路径](https://leetcode.cn/problems/bP4bmD/) | 🟠
+
+**-----------**
+
+> 本文有视频版:[图论基础及遍历算法](https://www.bilibili.com/video/BV19G41187cL/)
+
+经常有读者问我「图」这种数据结构,其实我在 [学习数据结构和算法的框架思维](https://labuladong.github.io/article/fname.html?fname=学习数据结构和算法的高效方法) 中说过,虽然图可以玩出更多的算法,解决更复杂的问题,但本质上图可以认为是多叉树的延伸。
+
+面试笔试很少出现图相关的问题,就算有,大多也是简单的遍历问题,基本上可以完全照搬多叉树的遍历。
+
+那么,本文依然秉持我们号的风格,只讲「图」最实用的,离我们最近的部分,让你心里对图有个直观的认识,文末我给出了其他经典图论算法,理解本文后应该都可以拿下的。
+
+### 图的逻辑结构和具体实现
+
+一幅图是由**节点**和**边**构成的,逻辑结构如下:
+
+![](https://labuladong.github.io/algo/images/图/0.jpg)
+
+**什么叫「逻辑结构」?就是说为了方便研究,我们把图抽象成这个样子**。
+
+根据这个逻辑结构,我们可以认为每个节点的实现如下:
+
+```java
+/* 图节点的逻辑结构 */
+class Vertex {
+ int id;
+ Vertex[] neighbors;
+}
+```
+
+看到这个实现,你有没有很熟悉?它和我们之前说的多叉树节点几乎完全一样:
+
+```java
+/* 基本的 N 叉树节点 */
+class TreeNode {
+ int val;
+ TreeNode[] children;
+}
+```
+
+所以说,图真的没啥高深的,本质上就是个高级点的多叉树而已,适用于树的 DFS/BFS 遍历算法,全部适用于图。
+
+不过呢,上面的这种实现是「逻辑上的」,实际上我们很少用这个 `Vertex` 类实现图,而是用常说的**邻接表和邻接矩阵**来实现。
+
+比如还是刚才那幅图:
+
+![](https://labuladong.github.io/algo/images/图/0.jpg)
+
+用邻接表和邻接矩阵的存储方式如下:
+
+![](https://labuladong.github.io/algo/images/图/2.jpeg)
+
+邻接表很直观,我把每个节点 `x` 的邻居都存到一个列表里,然后把 `x` 和这个列表关联起来,这样就可以通过一个节点 `x` 找到它的所有相邻节点。
+
+邻接矩阵则是一个二维布尔数组,我们权且称为 `matrix`,如果节点 `x` 和 `y` 是相连的,那么就把 `matrix[x][y]` 设为 `true`(上图中绿色的方格代表 `true`)。如果想找节点 `x` 的邻居,去扫一圈 `matrix[x][..]` 就行了。
+
+如果用代码的形式来表现,邻接表和邻接矩阵大概长这样:
+
+```java
+// 邻接表
+// graph[x] 存储 x 的所有邻居节点
+List[] graph;
+
+// 邻接矩阵
+// matrix[x][y] 记录 x 是否有一条指向 y 的边
+boolean[][] matrix;
+```
+
+**那么,为什么有这两种存储图的方式呢?肯定是因为他们各有优劣**。
+
+对于邻接表,好处是占用的空间少。
+
+你看邻接矩阵里面空着那么多位置,肯定需要更多的存储空间。
+
+但是,邻接表无法快速判断两个节点是否相邻。
+
+比如说我想判断节点 `1` 是否和节点 `3` 相邻,我要去邻接表里 `1` 对应的邻居列表里查找 `3` 是否存在。但对于邻接矩阵就简单了,只要看看 `matrix[1][3]` 就知道了,效率高。
+
+所以说,使用哪一种方式实现图,要看具体情况。
+
+> PS:在常规的算法题中,邻接表的使用会更频繁一些,主要是因为操作起来较为简单,但这不意味着邻接矩阵应该被轻视。矩阵是一个强有力的数学工具,图的一些隐晦性质可以借助精妙的矩阵运算展现出来。不过本文不准备引入数学内容,所以有兴趣的读者可以自行搜索学习。
+
+最后,我们再明确一个图论中特有的**度**(degree)的概念,在无向图中,「度」就是每个节点相连的边的条数。
+
+由于有向图的边有方向,所以有向图中每个节点「度」被细分为**入度**(indegree)和**出度**(outdegree),比如下图:
+
+![](https://labuladong.github.io/algo/images/图/0.jpg)
+
+其中节点 `3` 的入度为 3(有三条边指向它),出度为 1(它有 1 条边指向别的节点)。
+
+好了,对于「图」这种数据结构,能看懂上面这些就绰绰够用了。
+
+那你可能会问,我们上面说的这个图的模型仅仅是「有向无权图」,不是还有什么加权图,无向图,等等……
+
+**其实,这些更复杂的模型都是基于这个最简单的图衍生出来的**。
+
+**有向加权图怎么实现**?很简单呀:
+
+如果是邻接表,我们不仅仅存储某个节点 `x` 的所有邻居节点,还存储 `x` 到每个邻居的权重,不就实现加权有向图了吗?
+
+如果是邻接矩阵,`matrix[x][y]` 不再是布尔值,而是一个 int 值,0 表示没有连接,其他值表示权重,不就变成加权有向图了吗?
+
+如果用代码的形式来表现,大概长这样:
+
+```java
+// 邻接表
+// graph[x] 存储 x 的所有邻居节点以及对应的权重
+List[] graph;
+
+// 邻接矩阵
+// matrix[x][y] 记录 x 指向 y 的边的权重,0 表示不相邻
+int[][] matrix;
+```
+
+**无向图怎么实现**?也很简单,所谓的「无向」,是不是等同于「双向」?
+
+![](https://labuladong.github.io/algo/images/图/3.jpeg)
+
+如果连接无向图中的节点 `x` 和 `y`,把 `matrix[x][y]` 和 `matrix[y][x]` 都变成 `true` 不就行了;邻接表也是类似的操作,在 `x` 的邻居列表里添加 `y`,同时在 `y` 的邻居列表里添加 `x`。
+
+把上面的技巧合起来,就变成了无向加权图……
+
+好了,关于图的基本介绍就到这里,现在不管来什么乱七八糟的图,你心里应该都有底了。
+
+下面来看看所有数据结构都逃不过的问题:遍历。
+
+### 图的遍历
+
+**[学习数据结构和算法的框架思维](https://labuladong.github.io/article/fname.html?fname=学习数据结构和算法的高效方法) 说过,各种数据结构被发明出来无非就是为了遍历和访问,所以「遍历」是所有数据结构的基础**。
+
+图怎么遍历?还是那句话,参考多叉树,多叉树的 DFS 遍历框架如下:
+
+```java
+/* 多叉树遍历框架 */
+void traverse(TreeNode root) {
+ if (root == null) return;
+ // 前序位置
+ for (TreeNode child : root.children) {
+ traverse(child);
+ }
+ // 后序位置
+}
+```
+
+图和多叉树最大的区别是,图是可能包含环的,你从图的某一个节点开始遍历,有可能走了一圈又回到这个节点,而树不会出现这种情况,从某个节点出发必然走到叶子节点,绝不可能回到它自身。
+
+所以,如果图包含环,遍历框架就要一个 `visited` 数组进行辅助:
+
+```java
+// 记录被遍历过的节点
+boolean[] visited;
+// 记录从起点到当前节点的路径
+boolean[] onPath;
+
+/* 图遍历框架 */
+void traverse(Graph graph, int s) {
+ if (visited[s]) return;
+ // 经过节点 s,标记为已遍历
+ visited[s] = true;
+ // 做选择:标记节点 s 在路径上
+ onPath[s] = true;
+ for (int neighbor : graph.neighbors(s)) {
+ traverse(graph, neighbor);
+ }
+ // 撤销选择:节点 s 离开路径
+ onPath[s] = false;
+}
+```
+
+注意 `visited` 数组和 `onPath` 数组的区别,因为二叉树算是特殊的图,所以用遍历二叉树的过程来理解下这两个数组的区别:
+
+![](https://labuladong.github.io/algo/images/迭代遍历二叉树/1.gif)
+
+**上述 GIF 描述了递归遍历二叉树的过程,在 `visited` 中被标记为 true 的节点用灰色表示,在 `onPath` 中被标记为 true 的节点用绿色表示**,类比贪吃蛇游戏,`visited` 记录蛇经过过的格子,而 `onPath` 仅仅记录蛇身。在图的遍历过程中,`onPath` 用于判断是否成环,类比当贪吃蛇自己咬到自己(成环)的场景,这下你可以理解它们二者的区别了吧。
+
+如果让你处理路径相关的问题,这个 `onPath` 变量是肯定会被用到的,比如 [拓扑排序](https://labuladong.github.io/article/fname.html?fname=拓扑排序) 中就有运用。
+
+另外,你应该注意到了,这个 `onPath` 数组的操作很像前文 [回溯算法核心套路](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版) 中做「做选择」和「撤销选择」,区别在于位置:回溯算法的「做选择」和「撤销选择」在 for 循环里面,而对 `onPath` 数组的操作在 for 循环外面。
+
+为什么有这个区别呢?这就是前文 [回溯算法核心套路](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版) 中讲到的回溯算法和 DFS 算法的区别所在:回溯算法关注的不是节点,而是树枝。不信你看前文画的回溯树,我们需要在「树枝」上做选择和撤销选择:
+
+![](https://labuladong.github.io/algo/images/backtracking/5.jpg)
+
+他们的区别可以这样反应到代码上:
+
+```java
+// DFS 算法,关注点在节点
+void traverse(TreeNode root) {
+ if (root == null) return;
+ printf("进入节点 %s", root);
+ for (TreeNode child : root.children) {
+ traverse(child);
+ }
+ printf("离开节点 %s", root);
+}
+
+// 回溯算法,关注点在树枝
+void backtrack(TreeNode root) {
+ if (root == null) return;
+ for (TreeNode child : root.children) {
+ // 做选择
+ printf("从 %s 到 %s", root, child);
+ backtrack(child);
+ // 撤销选择
+ printf("从 %s 到 %s", child, root);
+ }
+}
+```
+
+如果执行这段代码,你会发现根节点被漏掉了:
+
+```java
+void traverse(TreeNode root) {
+ if (root == null) return;
+ for (TreeNode child : root.children) {
+ printf("进入节点 %s", child);
+ traverse(child);
+ printf("离开节点 %s", child);
+ }
+}
+```
+
+所以对于这里「图」的遍历,我们应该用 DFS 算法,即把 `onPath` 的操作放到 for 循环外面,否则会漏掉记录起始点的遍历。
+
+说了这么多 `onPath` 数组,再说下 `visited` 数组,其目的很明显了,由于图可能含有环,`visited` 数组就是防止递归重复遍历同一个节点进入死循环的。
+
+当然,如果题目告诉你图中不含环,可以把 `visited` 数组都省掉,基本就是多叉树的遍历。
+
+### 题目实践
+
+下面我们来看力扣第 797 题「所有可能路径」,函数签名如下:
+
+```java
+List> allPathsSourceTarget(int[][] graph);
+```
+
+题目输入一幅**有向无环图**,这个图包含 `n` 个节点,标号为 `0, 1, 2,..., n - 1`,请你计算所有从节点 `0` 到节点 `n - 1` 的路径。
+
+输入的这个 `graph` 其实就是「邻接表」表示的一幅图,`graph[i]` 存储这节点 `i` 的所有邻居节点。
+
+比如输入 `graph = [[1,2],[3],[3],[]]`,就代表下面这幅图:
+
+![](https://labuladong.github.io/algo/images/图/1.jpg)
+
+算法应该返回 `[[0,1,3],[0,2,3]]`,即 `0` 到 `3` 的所有路径。
+
+**解法很简单,以 `0` 为起点遍历图,同时记录遍历过的路径,当遍历到终点时将路径记录下来即可**。
+
+既然输入的图是无环的,我们就不需要 `visited` 数组辅助了,直接套用图的遍历框架:
+
+```java
+// 记录所有路径
+List> res = new LinkedList<>();
+
+public List> allPathsSourceTarget(int[][] graph) {
+ // 维护递归过程中经过的路径
+ LinkedList path = new LinkedList<>();
+ traverse(graph, 0, path);
+ return res;
+}
+
+/* 图的遍历框架 */
+void traverse(int[][] graph, int s, LinkedList path) {
+ // 添加节点 s 到路径
+ path.addLast(s);
+
+ int n = graph.length;
+ if (s == n - 1) {
+ // 到达终点
+ res.add(new LinkedList<>(path));
+ // 可以在这直接 return,但要 removeLast 正确维护 path
+ // path.removeLast();
+ // return;
+ // 不 return 也可以,因为图中不包含环,不会出现无限递归
+ }
+
+ // 递归每个相邻节点
+ for (int v : graph[s]) {
+ traverse(graph, v, path);
+ }
+
+ // 从路径移出节点 s
+ path.removeLast();
+}
+```
+
+这道题就这样解决了,注意 Java 的语言特性,因为 Java 函数参数传的是对象引用,所以向 `res` 中添加 `path` 时需要拷贝一个新的列表,否则最终 `res` 中的列表都是空的。
+
+最后总结一下,图的存储方式主要有邻接表和邻接矩阵,无论什么花里胡哨的图,都可以用这两种方式存储。
+
+在笔试中,最常考的算法是图的遍历,和多叉树的遍历框架是非常类似的。
+
+当然,图还会有很多其他的有趣算法,比如 [二分图判定](https://labuladong.github.io/article/fname.html?fname=二分图),[环检测和拓扑排序](https://labuladong.github.io/article/fname.html?fname=拓扑排序)(编译器循环引用检测就是类似的算法),[最小生成树](https://labuladong.github.io/article/fname.html?fname=kruskal),[Dijkstra 最短路径算法](https://labuladong.github.io/article/fname.html?fname=dijkstra算法) 等等,有兴趣的读者可以去看看,本文就到这了。
+
+
+
+
+
+引用本文的文章
+
+ - [Dijkstra 算法模板及应用](https://labuladong.github.io/article/fname.html?fname=dijkstra算法)
+ - [Prim 最小生成树算法](https://labuladong.github.io/article/fname.html?fname=prim算法)
+ - [一文秒杀所有岛屿题目](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=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=心流)
+
+
+
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| [133. Clone Graph](https://leetcode.com/problems/clone-graph/?show=1) | [133. 克隆图](https://leetcode.cn/problems/clone-graph/?show=1) |
+| [200. Number of Islands](https://leetcode.com/problems/number-of-islands/?show=1) | [200. 岛屿数量](https://leetcode.cn/problems/number-of-islands/?show=1) |
+| [2049. Count Nodes With the Highest Score](https://leetcode.com/problems/count-nodes-with-the-highest-score/?show=1) | [2049. 统计最高分的节点数目](https://leetcode.cn/problems/count-nodes-with-the-highest-score/?show=1) |
+| - | [剑指 Offer II 110. 所有路径](https://leetcode.cn/problems/bP4bmD/?show=1) |
+
+
+
+
+
+**_____________**
+
+**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
+
+![](https://labuladong.github.io/algo/images/souyisou2.png)
\ No newline at end of file
diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\256\236\347\216\260\350\256\241\347\256\227\345\231\250.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\256\236\347\216\260\350\256\241\347\256\227\345\231\250.md"
index 74437a16a435c5fa20abfe55b28f3c6b6b9d8495..bcca3a6afac0a99101d5aa1d1802995167b3c448 100644
--- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\256\236\347\216\260\350\256\241\347\256\227\345\231\250.md"
+++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\256\236\347\216\260\350\256\241\347\256\227\345\231\250.md"
@@ -1,9 +1,5 @@
# 拆解复杂问题:实现计算器
-
-
-
-
@@ -13,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -309,6 +305,20 @@ def calculate(s: str) -> int:
**退而求其次是一种很聪明策略**。你想想啊,假设这是一道考试题,你不会实现这个计算器,但是你写了字符串转整数的算法并指出了容易溢出的陷阱,那起码可以得 20 分吧;如果你能够处理加减法,那可以得 40 分吧;如果你能处理加减乘除四则运算,那起码够 70 分了;再加上处理空格字符,80 有了吧。我就是不会处理括号,那就算了,80 已经很 OK 了好不好。
+
+
+
+
+引用本文的文章
+
+ - [算法笔试「骗分」套路](https://labuladong.github.io/article/fname.html?fname=刷题技巧)
+
+
+
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\346\213\223\346\211\221\346\216\222\345\272\217.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\346\213\223\346\211\221\346\216\222\345\272\217.md"
new file mode 100644
index 0000000000000000000000000000000000000000..e8957b135f4f7fdc26ef03dd34081286c9c89d7c
--- /dev/null
+++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\346\213\223\346\211\221\346\216\222\345\272\217.md"
@@ -0,0 +1,607 @@
+# 拓扑排序详解及运用
+
+
+
+
+
+
+
+
+![](https://labuladong.github.io/algo/images/souyisou1.png)
+
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
+
+
+
+读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
+
+| LeetCode | 力扣 | 难度 |
+| :----: | :----: | :----: |
+| [207. Course Schedule](https://leetcode.com/problems/course-schedule/) | [207. 课程表](https://leetcode.cn/problems/course-schedule/) | 🟠
+| [210. Course Schedule II](https://leetcode.com/problems/course-schedule-ii/) | [210. 课程表 II](https://leetcode.cn/problems/course-schedule-ii/) | 🟠
+| - | [剑指 Offer II 113. 课程顺序](https://leetcode.cn/problems/QA2IGt/) | 🟠
+
+**-----------**
+
+> 本文有视频版:[拓扑排序详解及应用](https://www.bilibili.com/video/BV1kW4y1y7Ew/)
+
+图这种数据结构有一些比较特殊的算法,比如二分图判断,有环图无环图的判断,拓扑排序,以及最经典的最小生成树,单源最短路径问题,更难的就是类似网络流这样的问题。
+
+不过以我的经验呢,像网络流这种问题,你又不是打竞赛的,没时间的话就没必要学了;像 [最小生成树](https://labuladong.github.io/article/fname.html?fname=prim算法) 和 [最短路径问题](https://labuladong.github.io/article/fname.html?fname=dijkstra算法),虽然从刷题的角度用到的不多,但它们属于经典算法,学有余力可以掌握一下;像 [二分图判定](https://labuladong.github.io/article/fname.html?fname=二分图)、拓扑排序这一类,属于比较基本且有用的算法,应该比较熟练地掌握。
+
+**那么本文就结合具体的算法题,来说两个图论算法:有向图的环检测、拓扑排序算法**。
+
+这两个算法既可以用 DFS 思路解决,也可以用 BFS 思路解决,相对而言 BFS 解法从代码实现上看更简洁一些,但 DFS 解法有助于你进一步理解递归遍历数据结构的奥义,所以本文中我先讲 DFS 遍历的思路,再讲 BFS 遍历的思路。
+
+### 环检测算法(DFS 版本)
+
+先来看看力扣第 207 题「课程表」:
+
+![](https://labuladong.github.io/algo/images/拓扑排序/title1.jpg)
+
+函数签名如下:
+
+```java
+boolean canFinish(int numCourses, int[][] prerequisites);
+```
+
+题目应该不难理解,什么时候无法修完所有课程?当存在循环依赖的时候。
+
+其实这种场景在现实生活中也十分常见,比如我们写代码 import 包也是一个例子,必须合理设计代码目录结构,否则会出现循环依赖,编译器会报错,所以编译器实际上也使用了类似算法来判断你的代码是否能够成功编译。
+
+**看到依赖问题,首先想到的就是把问题转化成「有向图」这种数据结构,只要图中存在环,那就说明存在循环依赖**。
+
+具体来说,我们首先可以把课程看成「有向图」中的节点,节点编号分别是 `0, 1, ..., numCourses-1`,把课程之间的依赖关系看做节点之间的有向边。
+
+比如说必须修完课程 `1` 才能去修课程 `3`,那么就有一条有向边从节点 `1` 指向 `3`。
+
+所以我们可以根据题目输入的 `prerequisites` 数组生成一幅类似这样的图:
+
+![](https://labuladong.github.io/algo/images/拓扑排序/1.jpeg)
+
+**如果发现这幅有向图中存在环,那就说明课程之间存在循环依赖,肯定没办法全部上完;反之,如果没有环,那么肯定能上完全部课程**。
+
+好,那么想解决这个问题,首先我们要把题目的输入转化成一幅有向图,然后再判断图中是否存在环。
+
+如何转换成图呢?我们前文 [图论基础](https://labuladong.github.io/article/fname.html?fname=图) 写过图的两种存储形式,邻接矩阵和邻接表。
+
+以我刷题的经验,常见的存储方式是使用邻接表,比如下面这种结构:
+
+```java
+List[] graph;
+```
+
+**`graph[s]` 是一个列表,存储着节点 `s` 所指向的节点**。
+
+所以我们首先可以写一个建图函数:
+
+```java
+List[] buildGraph(int numCourses, int[][] prerequisites) {
+ // 图中共有 numCourses 个节点
+ List[] graph = new LinkedList[numCourses];
+ for (int i = 0; i < numCourses; i++) {
+ graph[i] = new LinkedList<>();
+ }
+ for (int[] edge : prerequisites) {
+ int from = edge[1], to = edge[0];
+ // 添加一条从 from 指向 to 的有向边
+ // 边的方向是「被依赖」关系,即修完课程 from 才能修课程 to
+ graph[from].add(to);
+ }
+ return graph;
+}
+```
+
+图建出来了,怎么判断图中有没有环呢?
+
+**先不要急,我们先来思考如何遍历这幅图,只要会遍历,就可以判断图中是否存在环了**。
+
+前文 [图论基础](https://labuladong.github.io/article/fname.html?fname=图) 写了 DFS 算法遍历图的框架,无非就是从多叉树遍历框架扩展出来的,加了个 `visited` 数组罢了:
+
+```java
+// 防止重复遍历同一个节点
+boolean[] visited;
+// 从节点 s 开始 DFS 遍历,将遍历过的节点标记为 true
+void traverse(List[] graph, int s) {
+ if (visited[s]) {
+ return;
+ }
+ /* 前序遍历代码位置 */
+ // 将当前节点标记为已遍历
+ visited[s] = true;
+ for (int t : graph[s]) {
+ traverse(graph, t);
+ }
+ /* 后序遍历代码位置 */
+}
+```
+
+那么我们就可以直接套用这个遍历代码:
+
+```java
+// 防止重复遍历同一个节点
+boolean[] visited;
+
+boolean canFinish(int numCourses, int[][] prerequisites) {
+ List[] graph = buildGraph(numCourses, prerequisites);
+
+ visited = new boolean[numCourses];
+ for (int i = 0; i < numCourses; i++) {
+ traverse(graph, i);
+ }
+}
+
+void traverse(List[] graph, int s) {
+ // 代码见上文
+}
+```
+
+注意图中并不是所有节点都相连,所以要用一个 for 循环将所有节点都作为起点调用一次 DFS 搜索算法。
+
+这样,就能遍历这幅图中的所有节点了,你打印一下 `visited` 数组,应该全是 true。
+
+前文 [学习数据结构和算法的框架思维](https://labuladong.github.io/article/fname.html?fname=学习数据结构和算法的高效方法) 说过,图的遍历和遍历多叉树差不多,所以到这里你应该都能很容易理解。
+
+现在可以思考如何判断这幅图中是否存在环。
+
+我们前文 [回溯算法核心套路详解](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版) 说过,你可以把递归函数看成一个在递归树上游走的指针,这里也是类似的:
+
+你也可以把 `traverse` 看做在图中节点上游走的指针,只需要再添加一个布尔数组 `onPath` 记录当前 `traverse` 经过的路径:
+
+```java
+boolean[] onPath;
+boolean[] visited;
+
+boolean hasCycle = false;
+
+void traverse(List[] graph, int s) {
+ if (onPath[s]) {
+ // 发现环!!!
+ hasCycle = true;
+ }
+ if (visited[s] || hasCycle) {
+ return;
+ }
+ // 将节点 s 标记为已遍历
+ visited[s] = true;
+ // 开始遍历节点 s
+ onPath[s] = true;
+ for (int t : graph[s]) {
+ traverse(graph, t);
+ }
+ // 节点 s 遍历完成
+ onPath[s] = false;
+}
+```
+
+这里就有点回溯算法的味道了,在进入节点 `s` 的时候将 `onPath[s]` 标记为 true,离开时标记回 false,如果发现 `onPath[s]` 已经被标记,说明出现了环。
+
+注意 `visited` 数组和 `onPath` 数组的区别,因为二叉树算是特殊的图,所以用遍历二叉树的过程来理解下这两个数组的区别:
+
+![](https://labuladong.github.io/algo/images/迭代遍历二叉树/1.gif)
+
+**上述 GIF 描述了递归遍历二叉树的过程,在 `visited` 中被标记为 true 的节点用灰色表示,在 `onPath` 中被标记为 true 的节点用绿色表示**。
+
+> PS:类比贪吃蛇游戏,`visited` 记录蛇经过过的格子,而 `onPath` 仅仅记录蛇身。`onPath` 用于判断是否成环,类比当贪吃蛇自己咬到自己(成环)的场景。
+
+这样,就可以在遍历图的过程中顺便判断是否存在环了,完整代码如下:
+
+```java
+// 记录一次递归堆栈中的节点
+boolean[] onPath;
+// 记录遍历过的节点,防止走回头路
+boolean[] visited;
+// 记录图中是否有环
+boolean hasCycle = false;
+
+boolean canFinish(int numCourses, int[][] prerequisites) {
+ List[] graph = buildGraph(numCourses, prerequisites);
+
+ visited = new boolean[numCourses];
+ onPath = new boolean[numCourses];
+
+ for (int i = 0; i < numCourses; i++) {
+ // 遍历图中的所有节点
+ traverse(graph, i);
+ }
+ // 只要没有循环依赖可以完成所有课程
+ return !hasCycle;
+}
+
+void traverse(List[] graph, int s) {
+ if (onPath[s]) {
+ // 出现环
+ hasCycle = true;
+ }
+
+ if (visited[s] || hasCycle) {
+ // 如果已经找到了环,也不用再遍历了
+ return;
+ }
+ // 前序代码位置
+ visited[s] = true;
+ onPath[s] = true;
+ for (int t : graph[s]) {
+ traverse(graph, t);
+ }
+ // 后序代码位置
+ onPath[s] = false;
+}
+
+List[] buildGraph(int numCourses, int[][] prerequisites) {
+ // 代码见前文
+}
+```
+
+这道题就解决了,核心就是判断一幅有向图中是否存在环。
+
+不过如果出题人继续恶心你,让你不仅要判断是否存在环,还要返回这个环具体有哪些节点,怎么办?
+
+你可能说,`onPath` 里面为 true 的索引,不就是组成环的节点编号吗?
+
+不是的,假设下图中绿色的节点是递归的路径,它们在 `onPath` 中的值都是 true,但显然成环的节点只是其中的一部分:
+
+![](https://labuladong.github.io/algo/images/拓扑排序/4.jpeg)
+
+这个问题留给大家思考,我会在公众号留言区置顶正确的答案。
+
+**那么接下来,我们来再讲一个经典的图算法:拓扑排序**。
+
+### 拓扑排序算法(DFS 版本)
+
+看下力扣第 210 题「课程表 II」:
+
+![](https://labuladong.github.io/algo/images/拓扑排序/title2.jpg)
+
+这道题就是上道题的进阶版,不是仅仅让你判断是否可以完成所有课程,而是进一步让你返回一个合理的上课顺序,保证开始修每个课程时,前置的课程都已经修完。
+
+函数签名如下:
+
+```java
+int[] findOrder(int numCourses, int[][] prerequisites);
+```
+
+这里我先说一下拓扑排序(Topological Sorting)这个名词,网上搜出来的定义很数学,这里干脆用百度百科的一幅图来让你直观地感受下:
+
+![](https://labuladong.github.io/algo/images/拓扑排序/top.jpg)
+
+> PS:图片中拓扑排序的结果有误,`C7->C8->C6` 应该改为 `C6->C7->C8`。
+
+**直观地说就是,让你把一幅图「拉平」,而且这个「拉平」的图里面,所有箭头方向都是一致的**,比如上图所有箭头都是朝右的。
+
+很显然,如果一幅有向图中存在环,是无法进行拓扑排序的,因为肯定做不到所有箭头方向一致;反过来,如果一幅图是「有向无环图」,那么一定可以进行拓扑排序。
+
+但是我们这道题和拓扑排序有什么关系呢?
+
+**其实也不难看出来,如果把课程抽象成节点,课程之间的依赖关系抽象成有向边,那么这幅图的拓扑排序结果就是上课顺序**。
+
+首先,我们先判断一下题目输入的课程依赖是否成环,成环的话是无法进行拓扑排序的,所以我们可以复用上一道题的主函数:
+
+```java
+public int[] findOrder(int numCourses, int[][] prerequisites) {
+ if (!canFinish(numCourses, prerequisites)) {
+ // 不可能完成所有课程
+ return new int[]{};
+ }
+ // ...
+}
+```
+
+那么关键问题来了,如何进行拓扑排序?是不是又要秀什么高大上的技巧了?
+
+**其实特别简单,将后序遍历的结果进行反转,就是拓扑排序的结果**。
+
+> PS:有的读者提到,他在网上看到的拓扑排序算法不用对后序遍历结果进行反转,这是为什么呢?
+
+你确实可以看到这样的解法,原因是他建图的时候对边的定义和我不同。我建的图中箭头方向是「被依赖」关系,比如节点 `1` 指向 `2`,含义是节点 `1` 被节点 `2` 依赖,即做完 `1` 才能去做 `2`,
+
+如果你反过来,把有向边定义为「依赖」关系,那么整幅图中边全部反转,就可以不对后序遍历结果反转。具体来说,就是把我的解法代码中 `graph[from].add(to);` 改成 `graph[to].add(from);` 就可以不反转了。
+
+**不过呢,现实中一般都是从初级任务指向进阶任务,所以像我这样把边定义为「被依赖」关系可能比较符合我们的认知习惯**。
+
+直接看解法代码吧,在上一题环检测的代码基础上添加了记录后序遍历结果的逻辑:
+
+```java
+// 记录后序遍历结果
+List postorder = new ArrayList<>();
+// 记录是否存在环
+boolean hasCycle = false;
+boolean[] visited, onPath;
+
+// 主函数
+public int[] findOrder(int numCourses, int[][] prerequisites) {
+ List[] graph = buildGraph(numCourses, prerequisites);
+ visited = new boolean[numCourses];
+ onPath = new boolean[numCourses];
+ // 遍历图
+ for (int i = 0; i < numCourses; i++) {
+ traverse(graph, i);
+ }
+ // 有环图无法进行拓扑排序
+ if (hasCycle) {
+ return new int[]{};
+ }
+ // 逆后序遍历结果即为拓扑排序结果
+ Collections.reverse(postorder);
+ int[] res = new int[numCourses];
+ for (int i = 0; i < numCourses; i++) {
+ res[i] = postorder.get(i);
+ }
+ return res;
+}
+
+// 图遍历函数
+void traverse(List[] graph, int s) {
+ if (onPath[s]) {
+ // 发现环
+ hasCycle = true;
+ }
+ if (visited[s] || hasCycle) {
+ return;
+ }
+ // 前序遍历位置
+ onPath[s] = true;
+ visited[s] = true;
+ for (int t : graph[s]) {
+ traverse(graph, t);
+ }
+ // 后序遍历位置
+ postorder.add(s);
+ onPath[s] = false;
+}
+
+// 建图函数
+List[] buildGraph(int numCourses, int[][] prerequisites) {
+ // 代码见前文
+}
+```
+
+代码虽然看起来多,但是逻辑应该是很清楚的,只要图中无环,那么我们就调用 `traverse` 函数对图进行 DFS 遍历,记录后序遍历结果,最后把后序遍历结果反转,作为最终的答案。
+
+**那么为什么后序遍历的反转结果就是拓扑排序呢**?
+
+我这里也避免数学证明,用一个直观地例子来解释,我们就说二叉树,这是我们说过很多次的二叉树遍历框架:
+
+```java
+void traverse(TreeNode root) {
+ // 前序遍历代码位置
+ traverse(root.left)
+ // 中序遍历代码位置
+ traverse(root.right)
+ // 后序遍历代码位置
+}
+```
+
+二叉树的后序遍历是什么时候?遍历完左右子树之后才会执行后序遍历位置的代码。换句话说,当左右子树的节点都被装到结果列表里面了,根节点才会被装进去。
+
+**后序遍历的这一特点很重要,之所以拓扑排序的基础是后序遍历,是因为一个任务必须等到它依赖的所有任务都完成之后才能开始开始执行**。
+
+你把二叉树理解成一幅有向图,边的方向是由父节点指向子节点,那么就是下图这样:
+
+![](https://labuladong.github.io/algo/images/拓扑排序/2.jpeg)
+
+按照我们的定义,边的含义是「被依赖」关系,那么上图的拓扑排序应该首先是节点 `1`,然后是 `2, 3`,以此类推。
+
+但显然标准的后序遍历结果不满足拓扑排序,而如果把后序遍历结果反转,就是拓扑排序结果了:
+
+![](https://labuladong.github.io/algo/images/拓扑排序/3.jpeg)
+
+以上,我直观解释了一下为什么「拓扑排序的结果就是反转之后的后序遍历结果」,当然,我的解释并没有严格的数学证明,有兴趣的读者可以自己查一下。
+
+### 环检测算法(BFS 版本)
+
+刚才讲了用 DFS 算法利用 `onPath` 数组判断是否存在环;也讲了用 DFS 算法利用逆后序遍历进行拓扑排序。
+
+其实 BFS 算法借助 `indegree` 数组记录每个节点的「入度」,也可以实现这两个算法。不熟悉 BFS 算法的读者可阅读前文 [BFS 算法核心框架](https://labuladong.github.io/article/fname.html?fname=BFS框架)。
+
+所谓「出度」和「入度」是「有向图」中的概念,很直观:如果一个节点 `x` 有 `a` 条边指向别的节点,同时被 `b` 条边所指,则称节点 `x` 的出度为 `a`,入度为 `b`。
+
+先说环检测算法,直接看 BFS 的解法代码:
+
+```java
+// 主函数
+public boolean canFinish(int numCourses, int[][] prerequisites) {
+ // 建图,有向边代表「被依赖」关系
+ List[] graph = buildGraph(numCourses, prerequisites);
+ // 构建入度数组
+ int[] indegree = new int[numCourses];
+ for (int[] edge : prerequisites) {
+ int from = edge[1], to = edge[0];
+ // 节点 to 的入度加一
+ indegree[to]++;
+ }
+
+ // 根据入度初始化队列中的节点
+ Queue q = new LinkedList<>();
+ for (int i = 0; i < numCourses; i++) {
+ if (indegree[i] == 0) {
+ // 节点 i 没有入度,即没有依赖的节点
+ // 可以作为拓扑排序的起点,加入队列
+ q.offer(i);
+ }
+ }
+
+ // 记录遍历的节点个数
+ int count = 0;
+ // 开始执行 BFS 循环
+ while (!q.isEmpty()) {
+ // 弹出节点 cur,并将它指向的节点的入度减一
+ int cur = q.poll();
+ count++;
+ for (int next : graph[cur]) {
+ indegree[next]--;
+ if (indegree[next] == 0) {
+ // 如果入度变为 0,说明 next 依赖的节点都已被遍历
+ q.offer(next);
+ }
+ }
+ }
+
+ // 如果所有节点都被遍历过,说明不成环
+ return count == numCourses;
+}
+
+
+// 建图函数
+List[] buildGraph(int n, int[][] edges) {
+ // 见前文
+}
+```
+
+我先总结下这段 BFS 算法的思路:
+
+1、构建邻接表,和之前一样,边的方向表示「被依赖」关系。
+
+2、构建一个 `indegree` 数组记录每个节点的入度,即 `indegree[i]` 记录节点 `i` 的入度。
+
+3、对 BFS 队列进行初始化,将入度为 0 的节点首先装入队列。
+
+**4、开始执行 BFS 循环,不断弹出队列中的节点,减少相邻节点的入度,并将入度变为 0 的节点加入队列**。
+
+**5、如果最终所有节点都被遍历过(`count` 等于节点数),则说明不存在环,反之则说明存在环**。
+
+我画个图你就容易理解了,比如下面这幅图,节点中的数字代表该节点的入度:
+
+![](https://labuladong.github.io/algo/images/拓扑排序/5.jpeg)
+
+队列进行初始化后,入度为 0 的节点首先被加入队列:
+
+![](https://labuladong.github.io/algo/images/拓扑排序/6.jpeg)
+
+开始执行 BFS 循环,从队列中弹出一个节点,减少相邻节点的入度,同时将新产生的入度为 0 的节点加入队列:
+
+![](https://labuladong.github.io/algo/images/拓扑排序/7.jpeg)
+
+继续从队列弹出节点,并减少相邻节点的入度,这一次没有新产生的入度为 0 的节点:
+
+![](https://labuladong.github.io/algo/images/拓扑排序/8.jpeg)
+
+继续从队列弹出节点,并减少相邻节点的入度,同时将新产生的入度为 0 的节点加入队列:
+
+![](https://labuladong.github.io/algo/images/拓扑排序/9.jpeg)
+
+继续弹出节点,直到队列为空:
+
+![](https://labuladong.github.io/algo/images/拓扑排序/10.jpeg)
+
+这时候,所有节点都被遍历过一遍,也就说明图中不存在环。
+
+反过来说,如果按照上述逻辑执行 BFS 算法,存在节点没有被遍历,则说明成环。
+
+比如下面这种情况,队列中最初只有一个入度为 0 的节点:
+
+![](https://labuladong.github.io/algo/images/拓扑排序/11.jpeg)
+
+当弹出这个节点并减小相邻节点的入度之后队列为空,但并没有产生新的入度为 0 的节点加入队列,所以 BFS 算法终止:
+
+![](https://labuladong.github.io/algo/images/拓扑排序/12.jpeg)
+
+你看到了,如果存在节点没有被遍历,那么说明图中存在环,现在回头去看 BFS 的代码,你应该就很容易理解其中的逻辑了。
+
+### 拓扑排序算法(BFS 版本)
+
+**如果你能看懂 BFS 版本的环检测算法,那么就很容易得到 BFS 版本的拓扑排序算法,因为节点的遍历顺序就是拓扑排序的结果**。
+
+比如刚才举的第一个例子,下图每个节点中的值即入队的顺序:
+
+![](https://labuladong.github.io/algo/images/拓扑排序/13.jpeg)
+
+显然,这个顺序就是一个可行的拓扑排序结果。
+
+所以,我们稍微修改一下 BFS 版本的环检测算法,记录节点的遍历顺序即可得到拓扑排序的结果:
+
+```java
+// 主函数
+public int[] findOrder(int numCourses, int[][] prerequisites) {
+ // 建图,和环检测算法相同
+ List[] graph = buildGraph(numCourses, prerequisites);
+ // 计算入度,和环检测算法相同
+ int[] indegree = new int[numCourses];
+ for (int[] edge : prerequisites) {
+ int from = edge[1], to = edge[0];
+ indegree[to]++;
+ }
+
+ // 根据入度初始化队列中的节点,和环检测算法相同
+ Queue q = new LinkedList<>();
+ for (int i = 0; i < numCourses; i++) {
+ if (indegree[i] == 0) {
+ q.offer(i);
+ }
+ }
+
+ // 记录拓扑排序结果
+ int[] res = new int[numCourses];
+ // 记录遍历节点的顺序(索引)
+ int count = 0;
+ // 开始执行 BFS 算法
+ while (!q.isEmpty()) {
+ int cur = q.poll();
+ // 弹出节点的顺序即为拓扑排序结果
+ res[count] = cur;
+ count++;
+ for (int next : graph[cur]) {
+ indegree[next]--;
+ if (indegree[next] == 0) {
+ q.offer(next);
+ }
+ }
+ }
+
+ if (count != numCourses) {
+ // 存在环,拓扑排序不存在
+ return new int[]{};
+ }
+
+ return res;
+}
+
+// 建图函数
+List[] buildGraph(int n, int[][] edges) {
+ // 见前文
+}
+```
+
+按道理,[图的遍历](https://labuladong.github.io/article/fname.html?fname=图) 都需要 `visited` 数组防止走回头路,这里的 BFS 算法其实是通过 `indegree` 数组实现的 `visited` 数组的作用,只有入度为 0 的节点才能入队,从而保证不会出现死循环。
+
+好了,到这里环检测算法、拓扑排序算法的 BFS 实现也讲完了,继续留一个思考题:
+
+对于 BFS 的环检测算法,如果问你形成环的节点具体是哪些,你应该如何实现呢?
+
+
+
+
+
+引用本文的文章
+
+ - [Dijkstra 算法模板及应用](https://labuladong.github.io/article/fname.html?fname=dijkstra算法)
+ - [Kruskal 最小生成树算法](https://labuladong.github.io/article/fname.html?fname=kruskal)
+ - [Prim 最小生成树算法](https://labuladong.github.io/article/fname.html?fname=prim算法)
+ - [二分图判定算法](https://labuladong.github.io/article/fname.html?fname=二分图)
+ - [图论基础及遍历算法](https://labuladong.github.io/article/fname.html?fname=图)
+ - [我的刷题心得](https://labuladong.github.io/article/fname.html?fname=算法心得)
+
+
+
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| - | [剑指 Offer II 113. 课程顺序](https://leetcode.cn/problems/QA2IGt/?show=1) |
+
+
+
+
+
+**_____________**
+
+**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
+
+![](https://labuladong.github.io/algo/images/souyisou2.png)
\ No newline at end of file
diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\350\256\276\350\256\241Twitter.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\350\256\276\350\256\241Twitter.md"
index c853e263239374c4c21f362f3a318e436cd82ad8..321eb725e9de0cc344c5c62a9ff43b24a2436c67 100644
--- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\350\256\276\350\256\241Twitter.md"
+++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\350\256\276\350\256\241Twitter.md"
@@ -1,9 +1,5 @@
# 设计Twitter
-
-
-
-
@@ -13,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -294,6 +290,20 @@ public List getNewsFeed(int userId) {
最后,Github 上有一个优秀的开源项目,专门收集了很多大型系统设计的案例和解析,而且有中文版本,上面这个图也出自该项目。对系统设计感兴趣的读者可以点击 [这里](https://github.com/donnemartin/system-design-primer) 查看。
+
+
+
+
+引用本文的文章
+
+ - [数据结构设计:最大栈](https://labuladong.github.io/article/fname.html?fname=最大栈)
+
+
+
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\200\222\345\275\222\345\217\215\350\275\254\351\223\276\350\241\250\347\232\204\344\270\200\351\203\250\345\210\206.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\200\222\345\275\222\345\217\215\350\275\254\351\223\276\350\241\250\347\232\204\344\270\200\351\203\250\345\210\206.md"
index bfd19e0858bb19cb4cef41d5e517c4caf19b50ad..9854f0e755ce73804c4484fefd9ca13c375ab8cf 100644
--- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\200\222\345\275\222\345\217\215\350\275\254\351\223\276\350\241\250\347\232\204\344\270\200\351\203\250\345\210\206.md"
+++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\200\222\345\275\222\345\217\215\350\275\254\351\223\276\350\241\250\347\232\204\344\270\200\351\203\250\345\210\206.md"
@@ -1,9 +1,5 @@
# 递归反转链表的一部分
-
-
-
-
@@ -13,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -225,6 +221,35 @@ ListNode reverseBetween(ListNode head, int m, int n) {
> 最后打个广告,我亲自制作了一门 [数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO),以视频课为主,手把手带你实现常用的数据结构及相关算法,旨在帮助算法基础较为薄弱的读者深入理解常用数据结构的底层原理,在算法学习中少走弯路。
+
+
+
+
+引用本文的文章
+
+ - [如何判断回文链表](https://labuladong.github.io/article/fname.html?fname=判断回文链表)
+ - [烧饼排序算法](https://labuladong.github.io/article/fname.html?fname=烧饼排序)
+
+
+
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| - | [剑指 Offer 24. 反转链表](https://leetcode.cn/problems/fan-zhuan-lian-biao-lcof/?show=1) |
+| - | [剑指 Offer II 024. 反转链表](https://leetcode.cn/problems/UHnkqh/?show=1) |
+
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md"
index 0648eccbd92ea734e478c6c77968fb73a8b00a34..9946b33dba5440324dd952575313b85d87b120f3 100644
--- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md"
+++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md"
@@ -1,9 +1,5 @@
# 队列实现栈|栈实现队列
-
-
-
-
@@ -13,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -227,6 +223,23 @@ public boolean empty() {
希望本文对你有帮助。
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| - | [剑指 Offer 09. 用两个栈实现队列](https://leetcode.cn/problems/yong-liang-ge-zhan-shi-xian-dui-lie-lcof/?show=1) |
+
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/BFS\346\241\206\346\236\266.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/BFS\346\241\206\346\236\266.md"
new file mode 100644
index 0000000000000000000000000000000000000000..01541bf1d9b6432581f42c5870b62b04de3bf453
--- /dev/null
+++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/BFS\346\241\206\346\236\266.md"
@@ -0,0 +1,429 @@
+# BFS 算法框架套路详解
+
+
+
+
+
+
+
+
+![](https://labuladong.github.io/algo/images/souyisou1.png)
+
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
+
+
+
+读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
+
+| LeetCode | 力扣 | 难度 |
+| :----: | :----: | :----: |
+| [111. Minimum Depth of Binary Tree](https://leetcode.com/problems/minimum-depth-of-binary-tree/) | [111. 二叉树的最小深度](https://leetcode.cn/problems/minimum-depth-of-binary-tree/) | 🟢
+| [752. Open the Lock](https://leetcode.com/problems/open-the-lock/) | [752. 打开转盘锁](https://leetcode.cn/problems/open-the-lock/) | 🟠
+| - | [剑指 Offer II 109. 开密码锁](https://leetcode.cn/problems/zlDJc7/) | 🟠
+
+**-----------**
+
+> 本文有视频版:[BFS 算法核心框架套路](https://www.bilibili.com/video/BV1oT411u7Vn/)
+
+后台有很多人问起 BFS 和 DFS 的框架,今天就来说说吧。
+
+首先,你要说我没写过 BFS 框架,这话没错,今天写个框架你背住就完事儿了。但要是说没写过 DFS 框架,那你还真是说错了,**其实 DFS 算法就是回溯算法**,我们前文 [回溯算法框架套路详解](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版) 就写过了,而且写得不是一般得好,建议好好复习,嘿嘿嘿~
+
+BFS 的核心思想应该不难理解的,就是把一些问题抽象成图,从一个点开始,向四周开始扩散。一般来说,我们写 BFS 算法都是用「队列」这种数据结构,每次将一个节点周围的所有节点加入队列。
+
+BFS 相对 DFS 的最主要的区别是:**BFS 找到的路径一定是最短的,但代价就是空间复杂度可能比 DFS 大很多**,至于为什么,我们后面介绍了框架就很容易看出来了。
+
+本文就由浅入深写两道 BFS 的典型题目,分别是「二叉树的最小高度」和「打开密码锁的最少步数」,手把手教你怎么写 BFS 算法。
+
+## 一、算法框架
+
+要说框架的话,我们先举例一下 BFS 出现的常见场景好吧,**问题的本质就是让你在一幅「图」中找到从起点 `start` 到终点 `target` 的最近距离,这个例子听起来很枯燥,但是 BFS 算法问题其实都是在干这个事儿**,把枯燥的本质搞清楚了,再去欣赏各种问题的包装才能胸有成竹嘛。
+
+这个广义的描述可以有各种变体,比如走迷宫,有的格子是围墙不能走,从起点到终点的最短距离是多少?如果这个迷宫带「传送门」可以瞬间传送呢?
+
+再比如说两个单词,要求你通过某些替换,把其中一个变成另一个,每次只能替换一个字符,最少要替换几次?
+
+再比如说连连看游戏,两个方块消除的条件不仅仅是图案相同,还得保证两个方块之间的最短连线不能多于两个拐点。你玩连连看,点击两个坐标,游戏是如何判断它俩的最短连线有几个拐点的?
+
+再比如……
+
+净整些花里胡哨的,这些问题都没啥奇技淫巧,本质上就是一幅「图」,让你从一个起点,走到终点,问最短路径。这就是 BFS 的本质,框架搞清楚了直接默写就好。
+
+![](https://labuladong.github.io/algo/images/BFS/0.jpeg)
+
+记住下面这个框架就 OK 了:
+
+```java
+// 计算从起点 start 到终点 target 的最近距离
+int BFS(Node start, Node target) {
+ Queue q; // 核心数据结构
+ Set visited; // 避免走回头路
+
+ q.offer(start); // 将起点加入队列
+ visited.add(start);
+ int step = 0; // 记录扩散的步数
+
+ while (q not empty) {
+ int sz = q.size();
+ /* 将当前队列中的所有节点向四周扩散 */
+ for (int i = 0; i < sz; i++) {
+ Node cur = q.poll();
+ /* 划重点:这里判断是否到达终点 */
+ if (cur is target)
+ return step;
+ /* 将 cur 的相邻节点加入队列 */
+ for (Node x : cur.adj()) {
+ if (x not in visited) {
+ q.offer(x);
+ visited.add(x);
+ }
+ }
+ }
+ /* 划重点:更新步数在这里 */
+ step++;
+ }
+}
+```
+
+队列 `q` 就不说了,BFS 的核心数据结构;`cur.adj()` 泛指 `cur` 相邻的节点,比如说二维数组中,`cur` 上下左右四面的位置就是相邻节点;`visited` 的主要作用是防止走回头路,大部分时候都是必须的,但是像一般的二叉树结构,没有子节点到父节点的指针,不会走回头路就不需要 `visited`。
+
+### 二、二叉树的最小高度
+
+先来个简单的问题实践一下 BFS 框架吧,判断一棵二叉树的**最小**高度,这也是力扣第 111 题「二叉树的最小深度」:
+
+![](https://labuladong.github.io/algo/images/BFS/title1.jpg)
+
+怎么套到 BFS 的框架里呢?首先明确一下起点 `start` 和终点 `target` 是什么,怎么判断到达了终点?
+
+**显然起点就是 `root` 根节点,终点就是最靠近根节点的那个「叶子节点」嘛**,叶子节点就是两个子节点都是 `null` 的节点:
+
+```java
+if (cur.left == null && cur.right == null)
+ // 到达叶子节点
+```
+
+那么,按照我们上述的框架稍加改造来写解法即可:
+
+```java
+int minDepth(TreeNode root) {
+ if (root == null) return 0;
+ Queue q = new LinkedList<>();
+ q.offer(root);
+ // root 本身就是一层,depth 初始化为 1
+ int depth = 1;
+
+ while (!q.isEmpty()) {
+ int sz = q.size();
+ /* 将当前队列中的所有节点向四周扩散 */
+ for (int i = 0; i < sz; i++) {
+ TreeNode cur = q.poll();
+ /* 判断是否到达终点 */
+ if (cur.left == null && cur.right == null)
+ return depth;
+ /* 将 cur 的相邻节点加入队列 */
+ if (cur.left != null)
+ q.offer(cur.left);
+ if (cur.right != null)
+ q.offer(cur.right);
+ }
+ /* 这里增加步数 */
+ depth++;
+ }
+ return depth;
+}
+```
+
+这里注意这个 `while` 循环和 `for` 循环的配合,**`while` 循环控制一层一层往下走,`for` 循环利用 `sz` 变量控制从左到右遍历每一层二叉树节点**:
+
+![](https://labuladong.github.io/algo/images/dijkstra/1.jpeg)
+
+这一点很重要,这个形式在普通 BFS 问题中都很常见,但是在 [Dijkstra 算法模板框架](https://labuladong.github.io/article/fname.html?fname=dijkstra算法) 中我们修改了这种代码模式,读完并理解本文后你可以去看看 BFS 算法是如何演变成 Dijkstra 算法在加权图中寻找最短路径的。
+
+话说回来,二叉树本身是很简单的数据结构,我想上述代码你应该可以理解的,其实其他复杂问题都是这个框架的变形,再探讨复杂问题之前,我们解答两个问题:
+
+**1、为什么 BFS 可以找到最短距离,DFS 不行吗**?
+
+首先,你看 BFS 的逻辑,`depth` 每增加一次,队列中的所有节点都向前迈一步,这保证了第一次到达终点的时候,走的步数是最少的。
+
+DFS 不能找最短路径吗?其实也是可以的,但是时间复杂度相对高很多。你想啊,DFS 实际上是靠递归的堆栈记录走过的路径,你要找到最短路径,肯定得把二叉树中所有树杈都探索完才能对比出最短的路径有多长对不对?而 BFS 借助队列做到一次一步「齐头并进」,是可以在不遍历完整棵树的条件下找到最短距离的。
+
+形象点说,DFS 是线,BFS 是面;DFS 是单打独斗,BFS 是集体行动。这个应该比较容易理解吧。
+
+**2、既然 BFS 那么好,为啥 DFS 还要存在**?
+
+BFS 可以找到最短距离,但是空间复杂度高,而 DFS 的空间复杂度较低。
+
+还是拿刚才我们处理二叉树问题的例子,假设给你的这个二叉树是满二叉树,节点数为 `N`,对于 DFS 算法来说,空间复杂度无非就是递归堆栈,最坏情况下顶多就是树的高度,也就是 `O(logN)`。
+
+但是你想想 BFS 算法,队列中每次都会储存着二叉树一层的节点,这样的话最坏情况下空间复杂度应该是树的最底层节点的数量,也就是 `N/2`,用 Big O 表示的话也就是 `O(N)`。
+
+由此观之,BFS 还是有代价的,一般来说在找最短路径的时候使用 BFS,其他时候还是 DFS 使用得多一些(主要是递归代码好写)。
+
+好了,现在你对 BFS 了解得足够多了,下面来一道难一点的题目,深化一下框架的理解吧。
+
+### 三、解开密码锁的最少次数
+
+这是力扣第 752 题「打开转盘锁」,比较有意思:
+
+![](https://labuladong.github.io/algo/images/BFS/title2.jpg)
+
+题目中描述的就是我们生活中常见的那种密码锁,如果没有任何约束,最少的拨动次数很好算,就像我们平时开密码锁那样直奔密码拨就行了。
+
+但现在的难点就在于,不能出现 `deadends`,应该如何计算出最少的转动次数呢?
+
+**第一步,我们不管所有的限制条件,不管 `deadends` 和 `target` 的限制,就思考一个问题:如果让你设计一个算法,穷举所有可能的密码组合,你怎么做**?
+
+穷举呗,再简单一点,如果你只转一下锁,有几种可能?总共有 4 个位置,每个位置可以向上转,也可以向下转,也就是有 8 种可能对吧。
+
+比如说从 `"0000"` 开始,转一次,可以穷举出 `"1000", "9000", "0100", "0900"...` 共 8 种密码。然后,再以这 8 种密码作为基础,对每个密码再转一下,穷举出所有可能...
+
+**仔细想想,这就可以抽象成一幅图,每个节点有 8 个相邻的节点**,又让你求最短距离,这不就是典型的 BFS 嘛,框架就可以派上用场了,先写出一个「简陋」的 BFS 框架代码再说别的:
+
+```java
+// 将 s[j] 向上拨动一次
+String plusOne(String s, int j) {
+ char[] ch = s.toCharArray();
+ if (ch[j] == '9')
+ ch[j] = '0';
+ else
+ ch[j] += 1;
+ return new String(ch);
+}
+// 将 s[i] 向下拨动一次
+String minusOne(String s, int j) {
+ char[] ch = s.toCharArray();
+ if (ch[j] == '0')
+ ch[j] = '9';
+ else
+ ch[j] -= 1;
+ return new String(ch);
+}
+
+// BFS 框架,打印出所有可能的密码
+void BFS(String target) {
+ Queue q = new LinkedList<>();
+ q.offer("0000");
+
+ while (!q.isEmpty()) {
+ int sz = q.size();
+ /* 将当前队列中的所有节点向周围扩散 */
+ for (int i = 0; i < sz; i++) {
+ String cur = q.poll();
+ /* 判断是否到达终点 */
+ System.out.println(cur);
+
+ /* 将一个节点的相邻节点加入队列 */
+ for (int j = 0; j < 4; j++) {
+ String up = plusOne(cur, j);
+ String down = minusOne(cur, j);
+ q.offer(up);
+ q.offer(down);
+ }
+ }
+ /* 在这里增加步数 */
+ }
+ return;
+}
+```
+
+> PS:这段代码当然有很多问题,但是我们做算法题肯定不是一蹴而就的,而是从简陋到完美的。不要完美主义,咱要慢慢来,好不。
+
+**这段 BFS 代码已经能够穷举所有可能的密码组合了,但是显然不能完成题目,有如下问题需要解决**:
+
+1、会走回头路。比如说我们从 `"0000"` 拨到 `"1000"`,但是等从队列拿出 `"1000"` 时,还会拨出一个 `"0000"`,这样的话会产生死循环。
+
+2、没有终止条件,按照题目要求,我们找到 `target` 就应该结束并返回拨动的次数。
+
+3、没有对 `deadends` 的处理,按道理这些「死亡密码」是不能出现的,也就是说你遇到这些密码的时候需要跳过。
+
+如果你能够看懂上面那段代码,真得给你鼓掌,只要按照 BFS 框架在对应的位置稍作修改即可修复这些问题:
+
+```java
+int openLock(String[] deadends, String target) {
+ // 记录需要跳过的死亡密码
+ Set deads = new HashSet<>();
+ for (String s : deadends) deads.add(s);
+ // 记录已经穷举过的密码,防止走回头路
+ Set visited = new HashSet<>();
+ Queue q = new LinkedList<>();
+ // 从起点开始启动广度优先搜索
+ int step = 0;
+ q.offer("0000");
+ visited.add("0000");
+
+ while (!q.isEmpty()) {
+ int sz = q.size();
+ /* 将当前队列中的所有节点向周围扩散 */
+ for (int i = 0; i < sz; i++) {
+ String cur = q.poll();
+
+ /* 判断是否到达终点 */
+ if (deads.contains(cur))
+ continue;
+ if (cur.equals(target))
+ return step;
+
+ /* 将一个节点的未遍历相邻节点加入队列 */
+ for (int j = 0; j < 4; j++) {
+ String up = plusOne(cur, j);
+ if (!visited.contains(up)) {
+ q.offer(up);
+ visited.add(up);
+ }
+ String down = minusOne(cur, j);
+ if (!visited.contains(down)) {
+ q.offer(down);
+ visited.add(down);
+ }
+ }
+ }
+ /* 在这里增加步数 */
+ step++;
+ }
+ // 如果穷举完都没找到目标密码,那就是找不到了
+ return -1;
+}
+```
+
+至此,我们就解决这道题目了。有一个比较小的优化:可以不需要 `dead` 这个哈希集合,可以直接将这些元素初始化到 `visited` 集合中,效果是一样的,可能更加优雅一些。
+
+### 四、双向 BFS 优化
+
+你以为到这里 BFS 算法就结束了?恰恰相反。BFS 算法还有一种稍微高级一点的优化思路:**双向 BFS**,可以进一步提高算法的效率。
+
+篇幅所限,这里就提一下区别:**传统的 BFS 框架就是从起点开始向四周扩散,遇到终点时停止;而双向 BFS 则是从起点和终点同时开始扩散,当两边有交集的时候停止**。
+
+为什么这样能够能够提升效率呢?其实从 Big O 表示法分析算法复杂度的话,它俩的最坏复杂度都是 `O(N)`,但是实际上双向 BFS 确实会快一些,我给你画两张图看一眼就明白了:
+
+![](https://labuladong.github.io/algo/images/BFS/1.jpeg)
+
+![](https://labuladong.github.io/algo/images/BFS/2.jpeg)
+
+图示中的树形结构,如果终点在最底部,按照传统 BFS 算法的策略,会把整棵树的节点都搜索一遍,最后找到 `target`;而双向 BFS 其实只遍历了半棵树就出现了交集,也就是找到了最短距离。从这个例子可以直观地感受到,双向 BFS 是要比传统 BFS 高效的。
+
+**不过,双向 BFS 也有局限,因为你必须知道终点在哪里**。比如我们刚才讨论的二叉树最小高度的问题,你一开始根本就不知道终点在哪里,也就无法使用双向 BFS;但是第二个密码锁的问题,是可以使用双向 BFS 算法来提高效率的,代码稍加修改即可:
+
+```java
+int openLock(String[] deadends, String target) {
+ Set deads = new HashSet<>();
+ for (String s : deadends) deads.add(s);
+ // 用集合不用队列,可以快速判断元素是否存在
+ Set q1 = new HashSet<>();
+ Set q2 = new HashSet<>();
+ Set visited = new HashSet<>();
+
+ int step = 0;
+ q1.add("0000");
+ q2.add(target);
+
+ while (!q1.isEmpty() && !q2.isEmpty()) {
+ // 哈希集合在遍历的过程中不能修改,用 temp 存储扩散结果
+ Set temp = new HashSet<>();
+
+ /* 将 q1 中的所有节点向周围扩散 */
+ for (String cur : q1) {
+ /* 判断是否到达终点 */
+ if (deads.contains(cur))
+ continue;
+ if (q2.contains(cur))
+ return step;
+
+ visited.add(cur);
+
+ /* 将一个节点的未遍历相邻节点加入集合 */
+ for (int j = 0; j < 4; j++) {
+ String up = plusOne(cur, j);
+ if (!visited.contains(up))
+ temp.add(up);
+ String down = minusOne(cur, j);
+ if (!visited.contains(down))
+ temp.add(down);
+ }
+ }
+ /* 在这里增加步数 */
+ step++;
+ // temp 相当于 q1
+ // 这里交换 q1 q2,下一轮 while 就是扩散 q2
+ q1 = q2;
+ q2 = temp;
+ }
+ return -1;
+}
+```
+
+双向 BFS 还是遵循 BFS 算法框架的,只是**不再使用队列,而是使用 HashSet 方便快速判断两个集合是否有交集**。
+
+另外的一个技巧点就是 **while 循环的最后交换 `q1` 和 `q2` 的内容**,所以只要默认扩散 `q1` 就相当于轮流扩散 `q1` 和 `q2`。
+
+其实双向 BFS 还有一个优化,就是在 while 循环开始时做一个判断:
+
+```java
+// ...
+while (!q1.isEmpty() && !q2.isEmpty()) {
+ if (q1.size() > q2.size()) {
+ // 交换 q1 和 q2
+ temp = q1;
+ q1 = q2;
+ q2 = temp;
+ }
+ // ...
+```
+
+为什么这是一个优化呢?
+
+因为按照 BFS 的逻辑,队列(集合)中的元素越多,扩散之后新的队列(集合)中的元素就越多;在双向 BFS 算法中,如果我们每次都选择一个较小的集合进行扩散,那么占用的空间增长速度就会慢一些,效率就会高一些。
+
+不过话说回来,**无论传统 BFS 还是双向 BFS,无论做不做优化,从 Big O 衡量标准来看,时间复杂度都是一样的**,只能说双向 BFS 是一种 trick,算法运行的速度会相对快一点,掌握不掌握其实都无所谓。最关键的是把 BFS 通用框架记下来,反正所有 BFS 算法都可以用它套出解法。
+
+接下来可阅读:
+
+* [BFS 算法如何解决智力题](https://labuladong.github.io/article/fname.html?fname=BFS解决滑动拼图)
+
+
+
+
+
+引用本文的文章
+
+ - [Dijkstra 算法模板及应用](https://labuladong.github.io/article/fname.html?fname=dijkstra算法)
+ - [Prim 最小生成树算法](https://labuladong.github.io/article/fname.html?fname=prim算法)
+ - [东哥带你刷二叉树(纲领篇)](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=分治算法)
+ - [如何用 BFS 算法秒杀各种智力题](https://labuladong.github.io/article/fname.html?fname=BFS解决滑动拼图)
+ - [我的刷题心得](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=心流)
+
+
+
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| [102. Binary Tree Level Order Traversal](https://leetcode.com/problems/binary-tree-level-order-traversal/?show=1) | [102. 二叉树的层序遍历](https://leetcode.cn/problems/binary-tree-level-order-traversal/?show=1) |
+| [117. Populating Next Right Pointers in Each Node II](https://leetcode.com/problems/populating-next-right-pointers-in-each-node-ii/?show=1) | [117. 填充每个节点的下一个右侧节点指针 II](https://leetcode.cn/problems/populating-next-right-pointers-in-each-node-ii/?show=1) |
+| [431. Encode N-ary Tree to Binary Tree](https://leetcode.com/problems/encode-n-ary-tree-to-binary-tree/?show=1)🔒 | [431. 将 N 叉树编码为二叉树](https://leetcode.cn/problems/encode-n-ary-tree-to-binary-tree/?show=1)🔒 |
+| [773. Sliding Puzzle](https://leetcode.com/problems/sliding-puzzle/?show=1) | [773. 滑动谜题](https://leetcode.cn/problems/sliding-puzzle/?show=1) |
+| [863. All Nodes Distance K in Binary Tree](https://leetcode.com/problems/all-nodes-distance-k-in-binary-tree/?show=1) | [863. 二叉树中所有距离为 K 的结点](https://leetcode.cn/problems/all-nodes-distance-k-in-binary-tree/?show=1) |
+| - | [剑指 Offer 32 - II. 从上到下打印二叉树 II](https://leetcode.cn/problems/cong-shang-dao-xia-da-yin-er-cha-shu-ii-lcof/?show=1) |
+| - | [剑指 Offer II 109. 开密码锁](https://leetcode.cn/problems/zlDJc7/?show=1) |
+
+
+
+
+
+**_____________**
+
+**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
+
+![](https://labuladong.github.io/algo/images/souyisou2.png)
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/BFS\350\247\243\345\206\263\346\273\221\345\212\250\346\213\274\345\233\276.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/BFS\350\247\243\345\206\263\346\273\221\345\212\250\346\213\274\345\233\276.md"
new file mode 100644
index 0000000000000000000000000000000000000000..40e7d6ba3257a78e902f8ff0d960ca1b262e833c
--- /dev/null
+++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/BFS\350\247\243\345\206\263\346\273\221\345\212\250\346\213\274\345\233\276.md"
@@ -0,0 +1,188 @@
+# BFS 算法秒杀各种益智游戏
+
+
+
+
+
+
+
+
+![](https://labuladong.github.io/algo/images/souyisou1.png)
+
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
+
+
+
+读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
+
+| LeetCode | 力扣 | 难度 |
+| :----: | :----: | :----: |
+| [773. Sliding Puzzle](https://leetcode.com/problems/sliding-puzzle/) | [773. 滑动谜题](https://leetcode.cn/problems/sliding-puzzle/) | 🔴
+
+**-----------**
+
+滑动拼图游戏大家应该都玩过,下图是一个 4x4 的滑动拼图:
+
+![](https://labuladong.github.io/algo/images/sliding_puzzle/1.jpeg)
+
+拼图中有一个格子是空的,可以利用这个空着的格子移动其他数字。你需要通过移动这些数字,得到某个特定排列顺序,这样就算赢了。
+
+我小时候还玩过一款叫做「华容道」的益智游戏,也和滑动拼图比较类似:
+
+![](https://labuladong.github.io/algo/images/sliding_puzzle/2.jpeg)
+
+实际上,滑动拼图游戏也叫数字华容道,你看它俩挺相似的。
+
+那么这种游戏怎么玩呢?我记得是有一些套路的,类似于魔方还原公式。但是我们今天不来研究让人头秃的技巧,**这些益智游戏通通可以用暴力搜索算法解决,所以今天我们就学以致用,用 BFS 算法框架来秒杀这些游戏**。
+
+### 一、题目解析
+
+力扣第 773 题「滑动谜题」就是这个问题,题目的要求如下:
+
+给你一个 2x3 的滑动拼图,用一个 2x3 的数组 `board` 表示。拼图中有数字 0~5 六个数,其中**数字 0 就表示那个空着的格子**,你可以移动其中的数字,当 `board` 变为 `[[1,2,3],[4,5,0]]` 时,赢得游戏。
+
+请你写一个算法,计算赢得游戏需要的最少移动次数,如果不能赢得游戏,返回 -1。
+
+比如说输入的二维数组 `board = [[4,1,2],[5,0,3]]`,算法应该返回 5:
+
+![](https://labuladong.github.io/algo/images/sliding_puzzle/5.jpeg)
+
+如果输入的是 `board = [[1,2,3],[5,4,0]]`,则算法返回 -1,因为这种局面下无论如何都不能赢得游戏。
+
+### 二、思路分析
+
+对于这种计算最小步数的问题,我们就要敏感地想到 BFS 算法。
+
+这个题目转化成 BFS 问题是有一些技巧的,我们面临如下问题:
+
+1、一般的 BFS 算法,是从一个起点 `start` 开始,向终点 `target` 进行寻路,但是拼图问题不是在寻路,而是在不断交换数字,这应该怎么转化成 BFS 算法问题呢?
+
+2、即便这个问题能够转化成 BFS 问题,如何处理起点 `start` 和终点 `target`?它们都是数组哎,把数组放进队列,套 BFS 框架,想想就比较麻烦且低效。
+
+首先回答第一个问题,**BFS 算法并不只是一个寻路算法,而是一种暴力搜索算法**,只要涉及暴力穷举的问题,BFS 就可以用,而且可以最快地找到答案。
+
+你想想计算机怎么解决问题的?哪有那么多奇技淫巧,本质上就是把所有可行解暴力穷举出来,然后从中找到一个最优解罢了。
+
+明白了这个道理,我们的问题就转化成了:**如何穷举出 `board` 当前局面下可能衍生出的所有局面**?这就简单了,看数字 0 的位置呗,和上下左右的数字进行交换就行了:
+
+![](https://labuladong.github.io/algo/images/sliding_puzzle/3.jpeg)
+
+这样其实就是一个 BFS 问题,每次先找到数字 0,然后和周围的数字进行交换,形成新的局面加入队列…… 当第一次到达 `target` 时,就得到了赢得游戏的最少步数。
+
+对于第二个问题,我们这里的 `board` 仅仅是 2x3 的二维数组,所以可以压缩成一个一维字符串。**其中比较有技巧性的点在于,二维数组有「上下左右」的概念,压缩成一维后,如何得到某一个索引上下左右的索引**?
+
+对于这道题,题目说输入的数组大小都是 2 x 3,所以我们可以直接手动写出来这个映射:
+
+```java
+// 记录一维字符串的相邻索引
+int[][] neighbor = new int[][]{
+ {1, 3},
+ {0, 4, 2},
+ {1, 5},
+ {0, 4},
+ {3, 1, 5},
+ {4, 2}
+};
+```
+
+**这个含义就是,在一维字符串中,索引 `i` 在二维数组中的的相邻索引为 `neighbor[i]`**:
+
+![](https://labuladong.github.io/algo/images/sliding_puzzle/4.jpeg)
+
+那么对于一个 `m x n` 的二维数组,手写它的一维索引映射肯定不现实了,如何用代码生成它的一维索引映射呢?
+
+观察上图就能发现,如果二维数组中的某个元素 `e` 在一维数组中的索引为 `i`,那么 `e` 的左右相邻元素在一维数组中的索引就是 `i - 1` 和 `i + 1`,而 `e` 的上下相邻元素在一维数组中的索引就是 `i - n` 和 `i + n`,其中 `n` 为二维数组的列数。
+
+这样,对于 `m x n` 的二维数组,我们可以写一个函数来生成它的 `neighbor` 索引映射,篇幅所限,我这里就不写了。
+
+至此,我们就把这个问题完全转化成标准的 BFS 问题了,借助前文 [BFS 算法框架](https://labuladong.github.io/article/fname.html?fname=BFS框架) 的代码框架,直接就可以套出解法代码了:
+
+```java
+public int slidingPuzzle(int[][] board) {
+ int m = 2, n = 3;
+ StringBuilder sb = new StringBuilder();
+ String target = "123450";
+ // 将 2x3 的数组转化成字符串作为 BFS 的起点
+ for (int i = 0; i < m; i++) {
+ for (int j = 0; j < n; j++) {
+ sb.append(board[i][j]);
+ }
+ }
+ String start = sb.toString();
+
+ // 记录一维字符串的相邻索引
+ int[][] neighbor = new int[][]{
+ {1, 3},
+ {0, 4, 2},
+ {1, 5},
+ {0, 4},
+ {3, 1, 5},
+ {4, 2}
+ };
+
+ /******* BFS 算法框架开始 *******/
+ Queue q = new LinkedList<>();
+ HashSet visited = new HashSet<>();
+ // 从起点开始 BFS 搜索
+ q.offer(start);
+ visited.add(start);
+
+ int step = 0;
+ while (!q.isEmpty()) {
+ int sz = q.size();
+ for (int i = 0; i < sz; i++) {
+ String cur = q.poll();
+ // 判断是否达到目标局面
+ if (target.equals(cur)) {
+ return step;
+ }
+ // 找到数字 0 的索引
+ int idx = 0;
+ for (; cur.charAt(idx) != '0'; idx++) ;
+ // 将数字 0 和相邻的数字交换位置
+ for (int adj : neighbor[idx]) {
+ String new_board = swap(cur.toCharArray(), adj, idx);
+ // 防止走回头路
+ if (!visited.contains(new_board)) {
+ q.offer(new_board);
+ visited.add(new_board);
+ }
+ }
+ }
+ step++;
+ }
+ /******* BFS 算法框架结束 *******/
+ return -1;
+}
+
+private String swap(char[] chars, int i, int j) {
+ char temp = chars[i];
+ chars[i] = chars[j];
+ chars[j] = temp;
+ return new String(chars);
+}
+```
+
+至此,这道题目就解决了,其实框架完全没有变,套路都是一样的,我们只是花了比较多的时间将滑动拼图游戏转化成 BFS 算法。
+
+很多益智游戏都是这样,虽然看起来特别巧妙,但都架不住暴力穷举,常用的算法就是回溯算法或者 BFS 算法。
+
+
+
+
+
+引用本文的文章
+
+ - [BFS 算法解题套路框架](https://labuladong.github.io/article/fname.html?fname=BFS框架)
+
+
+
+
+
+
+
+**_____________**
+
+**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
+
+![](https://labuladong.github.io/algo/images/souyisou2.png)
\ No newline at end of file
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/FloodFill\347\256\227\346\263\225\350\257\246\350\247\243\345\217\212\345\272\224\347\224\250.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/FloodFill\347\256\227\346\263\225\350\257\246\350\247\243\345\217\212\345\272\224\347\224\250.md"
deleted file mode 100644
index 5a0c797fd735d403dd5cee09c7f006b0f810e47a..0000000000000000000000000000000000000000
--- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/FloodFill\347\256\227\346\263\225\350\257\246\350\247\243\345\217\212\345\272\224\347\224\250.md"
+++ /dev/null
@@ -1,326 +0,0 @@
-# FloodFill算法详解及应用
-
-
-
-
-
-
-
-
-
-
-![](https://labuladong.github.io/algo/images/souyisou1.png)
-
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
-
-
-
-读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
-
-| LeetCode | 力扣 | 难度 |
-| :----: | :----: | :----: |
-| [733. Flood Fill](https://leetcode.com/problems/flood-fill/) | [733. 图像渲染](https://leetcode.cn/problems/flood-fill/) | 🟢
-
-**-----------**
-
-啥是 FloodFill 算法呢,最直接的一个应用就是「颜色填充」,就是 Windows 绘画本中那个小油漆桶的标志,可以把一块被圈起来的区域全部染色。
-
-![](https://labuladong.github.io/algo/images/floodfill/floodfill.gif)
-
-这种算法思想还在许多其他地方有应用。比如说扫雷游戏,有时候你点一个方格,会一下子展开一片区域,这个展开过程,就是 FloodFill 算法实现的。
-
-![](https://labuladong.github.io/algo/images/floodfill/扫雷.png)
-
-类似的,像消消乐这类游戏,相同方块积累到一定数量,就全部消除,也是 FloodFill 算法的功劳。
-
-![](https://labuladong.github.io/algo/images/floodfill/xiaoxiaole.jpg)
-
-通过以上的几个例子,你应该对 FloodFill 算法有个概念了,现在我们要抽象问题,提取共同点。
-
-### 一、构建框架
-
-以上几个例子,都可以抽象成一个二维矩阵(图片其实就是像素点矩阵),然后从某个点开始向四周扩展,直到无法再扩展为止。
-
-矩阵,可以抽象为一幅「图」,这就是一个图的遍历问题,也就类似一个 N 叉树遍历的问题。几行代码就能解决,直接上框架吧:
-
-```java
-// (x, y) 为坐标位置
-void fill(int x, int y) {
- fill(x - 1, y); // 上
- fill(x + 1, y); // 下
- fill(x, y - 1); // 左
- fill(x, y + 1); // 右
-}
-```
-
-这个框架可以解决所有在二维矩阵中遍历的问题,说得高端一点,这就叫深度优先搜索(Depth First Search,简称 DFS),说得简单一点,这就叫四叉树遍历框架。坐标 (x, y) 就是 root,四个方向就是 root 的四个子节点。
-
-下面看一道 LeetCode 题目,其实就是让我们来实现一个「颜色填充」功能。
-
-![](https://labuladong.github.io/algo/images/floodfill/leetcode.png)
-
-根据上篇文章,我们讲了「树」算法设计的一个总路线,今天就可以用到:
-
-```java
-int[][] floodFill(int[][] image,
- int sr, int sc, int newColor) {
-
- int origColor = image[sr][sc];
- fill(image, sr, sc, origColor, newColor);
- return image;
-}
-
-void fill(int[][] image, int x, int y,
- int origColor, int newColor) {
- // 出界:超出边界索引
- if (!inArea(image, x, y)) return;
- // 碰壁:遇到其他颜色,超出 origColor 区域
- if (image[x][y] != origColor) return;
- image[x][y] = newColor;
-
- fill(image, x, y + 1, origColor, newColor);
- fill(image, x, y - 1, origColor, newColor);
- fill(image, x - 1, y, origColor, newColor);
- fill(image, x + 1, y, origColor, newColor);
-}
-
-boolean inArea(int[][] image, int x, int y) {
- return x >= 0 && x < image.length
- && y >= 0 && y < image[0].length;
-}
-```
-
-只要你能够理解这段代码,一定要给你鼓掌,给你 99 分,因为你对「框架思维」的掌控已经炉火纯青,此算法已经 cover 了 99% 的情况,仅有一个细节问题没有解决,就是当 origColor 和 newColor 相同时,会陷入无限递归。
-
-### 二、研究细节
-
-为什么会陷入无限递归呢,很好理解,因为每个坐标都要搜索上下左右,那么对于一个坐标,一定会被上下左右的坐标搜索。**被重复搜索时,必须保证递归函数能够能正确地退出,否则就会陷入死循环**。
-
-为什么 newColor 和 origColor 不同时可以正常退出呢?把算法流程画个图理解一下:
-
-![](https://labuladong.github.io/algo/images/floodfill/ppt1.PNG)
-
-可以看到,fill(1, 1) 被重复搜索了,我们用 fill(1, 1)* 表示这次重复搜索。fill(1, 1)* 执行时,(1, 1) 已经被换成了 newColor,所以 fill(1, 1)* 会在这个 if 语句被怼回去,正确退出了。
-
-```java
-// 碰壁:遇到其他颜色,超出 origColor 区域
-if (image[x][y] != origColor) return;
-```
-![](https://labuladong.github.io/algo/images/floodfill/ppt2.PNG)
-
-但是,如果说 origColor 和 newColor 一样,这个 if 语句就无法让 fill(1, 1)* 正确退出,而是开启了下面的重复递归,形成了死循环。
-
-![](https://labuladong.github.io/algo/images/floodfill/ppt3.PNG)
-
-### 三、处理细节
-
-如何避免上述问题的发生,最容易想到的就是用一个和 image 一样大小的二维 bool 数组记录走过的地方,一旦发现重复立即 return。
-
-```java
- // 出界:超出边界索引
-if (!inArea(image, x, y)) return;
-// 碰壁:遇到其他颜色,超出 origColor 区域
-if (image[x][y] != origColor) return;
-// 不走回头路
-if (visited[x][y]) return;
-visited[x][y] = true;
-image[x][y] = newColor;
-```
-
-完全 OK,这也是处理「图」的一种常用手段。不过对于此题,不用开数组,我们有一种更好的方法,那就是回溯算法。
-
-前文 [回溯算法框架套路](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版)讲过,这里不再赘述,直接套回溯算法框架:
-
-```java
-void fill(int[][] image, int x, int y,
- int origColor, int newColor) {
- // 出界:超出数组边界
- if (!inArea(image, x, y)) return;
- // 碰壁:遇到其他颜色,超出 origColor 区域
- if (image[x][y] != origColor) return;
- // 已探索过的 origColor 区域
- if (image[x][y] == -1) return;
-
- // choose:打标记,以免重复
- image[x][y] = -1;
- fill(image, x, y + 1, origColor, newColor);
- fill(image, x, y - 1, origColor, newColor);
- fill(image, x - 1, y, origColor, newColor);
- fill(image, x + 1, y, origColor, newColor);
- // unchoose:将标记替换为 newColor
- image[x][y] = newColor;
-}
-```
-
-这种解决方法是最常用的,相当于使用一个特殊值 -1 代替 visited 数组的作用,达到不走回头路的效果。为什么是 -1,因为题目中说了颜色取值在 0 - 65535 之间,所以 -1 足够特殊,能和颜色区分开。
-
-### 四、拓展延伸:自动魔棒工具和扫雷
-
-大部分图片编辑软件一定有「自动魔棒工具」这个功能:点击一个地方,帮你自动选中相近颜色的部分。如下图,我想选中老鹰,可以先用自动魔棒选中蓝天背景,然后反向选择,就选中了老鹰。我们来分析一下自动魔棒工具的原理。
-
-![](https://labuladong.github.io/algo/images/floodfill/抠图.jpg)
-
-显然,这个算法肯定是基于 FloodFill 算法的,但有两点不同:首先,背景色是蓝色,但不能保证都是相同的蓝色,毕竟是像素点,可能存在肉眼无法分辨的深浅差异,而我们希望能够忽略这种细微差异。第二,FloodFill 算法是「区域填充」,这里更像「边界填充」。
-
-对于第一个问题,很好解决,可以设置一个阈值 threshold,在阈值范围内波动的颜色都视为 origColor:
-
-```java
-if (Math.abs(image[x][y] - origColor) > threshold)
- return;
-```
-
-对于第二个问题,我们首先明确问题:不要把区域内所有 origColor 的都染色,而是只给区域最外圈染色。然后,我们分析,如何才能仅给外围染色,即如何才能找到最外围坐标,最外围坐标有什么特点?
-
-![](https://labuladong.github.io/algo/images/floodfill/ppt4.PNG)
-
-可以发现,区域边界上的坐标,至少有一个方向不是 origColor,而区域内部的坐标,四面都是 origColor,这就是解决问题的关键。保持框架不变,使用 visited 数组记录已搜索坐标,主要代码如下:
-
-```java
-int fill(int[][] image, int x, int y,
- int origColor, int newColor) {
- // 出界:超出数组边界
- if (!inArea(image, x, y)) return 0;
- // 已探索过的 origColor 区域
- if (visited[x][y]) return 1;
- // 碰壁:遇到其他颜色,超出 origColor 区域
- if (image[x][y] != origColor) return 0;
-
- visited[x][y] = true;
-
- int surround =
- fill(image, x - 1, y, origColor, newColor)
- + fill(image, x + 1, y, origColor, newColor)
- + fill(image, x, y - 1, origColor, newColor)
- + fill(image, x, y + 1, origColor, newColor);
-
- if (surround < 4)
- image[x][y] = newColor;
-
- return 1;
-}
-```
-
-这样,区域内部的坐标探索四周后得到的 surround 是 4,而边界的坐标会遇到其他颜色,或超出边界索引,surround 会小于 4。如果你对这句话不理解,我们把逻辑框架抽象出来看:
-
-```java
-int fill(int[][] image, int x, int y,
- int origColor, int newColor) {
- // 出界:超出数组边界
- if (!inArea(image, x, y)) return 0;
- // 已探索过的 origColor 区域
- if (visited[x][y]) return 1;
- // 碰壁:遇到其他颜色,超出 origColor 区域
- if (image[x][y] != origColor) return 0;
- // 未探索且属于 origColor 区域
- if (image[x][y] == origColor) {
- // ...
- return 1;
- }
-}
-```
-
-这 4 个 if 判断涵盖了 (x, y) 的所有可能情况,surround 的值由四个递归函数相加得到,而每个递归函数的返回值就这四种情况的一种。借助这个逻辑框架,你一定能理解上面那句话了。
-
-这样就实现了仅对 origColor 区域边界坐标染色的目的,等同于完成了魔棒工具选定区域边界的功能。
-
-这个算法有两个细节问题,一是必须借助 visited 来记录已探索的坐标,而无法使用回溯算法;二是开头几个 if 顺序不可打乱。读者可以思考一下原因。
-
-同理,思考扫雷游戏,应用 FloodFill 算法展开空白区域的同时,也需要计算并显示边界上雷的个数,如何实现的?其实也是相同的思路,遇到雷就返回 true,这样 surround 变量存储的就是雷的个数。当然,扫雷的 FloodFill 算法不能只检查上下左右,还得加上四个斜向。
-
-![](https://labuladong.github.io/algo/images/floodfill/ppt5.PNG)
-
-以上详细讲解了 FloodFill 算法的框架设计,**二维矩阵中的搜索问题,都逃不出这个算法框架**。
-
-**_____________**
-
-**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
-
-![](https://labuladong.github.io/algo/images/souyisou2.png)
-
-
-======其他语言代码======
-
-[733.图像渲染](https://leetcode-cn.com/problems/flood-fill)
-
-
-
-### javascript
-
-**BFS**
-从起始像素向上下左右扩散,只要相邻的点存在并和起始点颜色相同,就染成新的颜色,并继续扩散。
-
-借助一个队列去遍历节点,考察出列的节点,带出满足条件的节点入列。已经染成新色的节点不会入列,避免重复访问节点。
-
-时间复杂度:O(n)。空间复杂度:O(n)
-
-```js
-const floodFill = (image, sr, sc, newColor) => {
- const m = image.length;
- const n = image[0].length;
- const oldColor = image[sr][sc];
- if (oldColor == newColor) return image;
-
- const fill = (i, j) => {
- if (i < 0 || i >= m || j < 0 || j >= n || image[i][j] != oldColor) {
- return;
- }
- image[i][j] = newColor;
- fill(i - 1, j);
- fill(i + 1, j);
- fill(i, j - 1);
- fill(i, j + 1);
- };
-
- fill(sr, sc);
- return image;
-};
-```
-
-
-
-**DFS**
-
-思路与上文相同。
-
-```js
-/**
- * @param {number[][]} image
- * @param {number} sr
- * @param {number} sc
- * @param {number} newColor
- * @return {number[][]}
- */
-let floodFill = function (image, sr, sc, newColor) {
- let origColor = image[sr][sc];
- fill(image, sr, sc, origColor, newColor);
- return image;
-}
-
-let fill = function (image, x, y, origColor, newColor) {
- // 出界:超出边界索引
- if (!inArea(image, x, y)) return;
-
- // 碰壁:遇到其他颜色,超出 origColor 区域
- if (image[x][y] !== origColor) return;
-
- // 已探索过的 origColor 区域
- if (image[x][y] === -1) return;
-
- // 打标记 避免重复
- image[x][y] = -1;
-
- fill(image, x, y + 1, origColor, newColor);
- fill(image, x, y - 1, origColor, newColor);
- fill(image, x - 1, y, origColor, newColor);
- fill(image, x + 1, y, origColor, newColor);
-
- // un choose:将标记替换为 newColor
- image[x][y] = newColor;
-}
-
-let inArea = function (image, x, y) {
- return x >= 0 && x < image.length
- && y >= 0 && y < image[0].length;
-}
-```
-
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\345\272\224\347\224\250.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\345\272\224\347\224\250.md"
deleted file mode 100644
index ee53f6812cda0ff8869bfd39042969138a3581a7..0000000000000000000000000000000000000000
--- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\345\272\224\347\224\250.md"
+++ /dev/null
@@ -1,570 +0,0 @@
-# Union-Find算法应用
-
-
-
-
-
-
-
-
-
-![](../pictures/souyisou.png)
-
-**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
-
-读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
-
-[130.被围绕的区域](https://leetcode-cn.com/problems/surrounded-regions)
-
-[990.等式方程的可满足性](https://leetcode-cn.com/problems/satisfiability-of-equality-equations)
-
-[261.以图判树](https://leetcode-cn.com/problems/graph-valid-tree/)
-
-**-----------**
-
-上篇文章很多读者对于 Union-Find 算法的应用表示很感兴趣,这篇文章就拿几道 LeetCode 题目来讲讲这个算法的巧妙用法。
-
-首先,复习一下,Union-Find 算法解决的是图的动态连通性问题,这个算法本身不难,能不能应用出来主要是看你抽象问题的能力,是否能够把原始问题抽象成一个有关图论的问题。
-
-先复习一下上篇文章写的算法代码,回答读者提出的几个问题:
-
-```java
-class UF {
- // 记录连通分量个数
- private int count;
- // 存储若干棵树
- private int[] parent;
- // 记录树的“重量”
- private int[] size;
-
- public UF(int n) {
- this.count = n;
- parent = new int[n];
- size = new int[n];
- for (int i = 0; i < n; i++) {
- parent[i] = i;
- size[i] = 1;
- }
- }
-
- /* 将 p 和 q 连通 */
- public void union(int p, int q) {
- int rootP = find(p);
- int rootQ = find(q);
- if (rootP == rootQ)
- return;
-
- // 小树接到大树下面,较平衡
- if (size[rootP] > size[rootQ]) {
- parent[rootQ] = rootP;
- size[rootP] += size[rootQ];
- } else {
- parent[rootP] = rootQ;
- size[rootQ] += size[rootP];
- }
- count--;
- }
-
- /* 判断 p 和 q 是否互相连通 */
- public boolean connected(int p, int q) {
- int rootP = find(p);
- int rootQ = find(q);
- // 处于同一棵树上的节点,相互连通
- return rootP == rootQ;
- }
-
- /* 返回节点 x 的根节点 */
- private int find(int x) {
- while (parent[x] != x) {
- // 进行路径压缩
- parent[x] = parent[parent[x]];
- x = parent[x];
- }
- return x;
- }
-
- public int count() {
- return count;
- }
-}
-```
-
-算法的关键点有 3 个:
-
-1、用 `parent` 数组记录每个节点的父节点,相当于指向父节点的指针,所以 `parent` 数组内实际存储着一个森林(若干棵多叉树)。
-
-2、用 `size` 数组记录着每棵树的重量,目的是让 `union` 后树依然拥有平衡性,而不会退化成链表,影响操作效率。
-
-3、在 `find` 函数中进行路径压缩,保证任意树的高度保持在常数,使得 `union` 和 `connected` API 时间复杂度为 O(1)。
-
-有的读者问,**既然有了路径压缩,`size` 数组的重量平衡还需要吗**?这个问题很有意思,因为路径压缩保证了树高为常数(不超过 3),那么树就算不平衡,高度也是常数,基本没什么影响。
-
-我认为,论时间复杂度的话,确实,不需要重量平衡也是 O(1)。但是如果加上 `size` 数组辅助,效率还是略微高一些,比如下面这种情况:
-
-![](../pictures/unionfind应用/1.jpg)
-
-如果带有重量平衡优化,一定会得到情况一,而不带重量优化,可能出现情况二。高度为 3 时才会触发路径压缩那个 `while` 循环,所以情况一根本不会触发路径压缩,而情况二会多执行很多次路径压缩,将第三层节点压缩到第二层。
-
-也就是说,去掉重量平衡,虽然对于单个的 `find` 函数调用,时间复杂度依然是 O(1),但是对于 API 调用的整个过程,效率会有一定的下降。当然,好处就是减少了一些空间,不过对于 Big O 表示法来说,时空复杂度都没变。
-
-下面言归正传,来看看这个算法有什么实际应用。
-
-### 一、DFS 的替代方案
-
-很多使用 DFS 深度优先算法解决的问题,也可以用 Union-Find 算法解决。
-
-比如第 130 题,被围绕的区域:给你一个 M×N 的二维矩阵,其中包含字符 `X` 和 `O`,让你找到矩阵中**四面**被 `X` 围住的 `O`,并且把它们替换成 `X`。
-
-```java
-void solve(char[][] board);
-```
-
-注意哦,必须是四面被围的 `O` 才能被换成 `X`,也就是说边角上的 `O` 一定不会被围,进一步,与边角上的 `O` 相连的 `O` 也不会被 `X` 围四面,也不会被替换。
-
-![](../pictures/unionfind应用/2.jpg)
-
-PS:这让我想起小时候玩的棋类游戏「黑白棋」,只要你用两个棋子把对方的棋子夹在中间,对方的子就被替换成你的子。可见,占据四角的棋子是无敌的,与其相连的边棋子也是无敌的(无法被夹掉)。
-
-解决这个问题的传统方法也不困难,先用 for 循环遍历棋盘的**四边**,用 DFS 算法把那些与边界相连的 `O` 换成一个特殊字符,比如 `#`;然后再遍历整个棋盘,把剩下的 `O` 换成 `X`,把 `#` 恢复成 `O`。这样就能完成题目的要求,时间复杂度 O(MN)。
-
-这个问题也可以用 Union-Find 算法解决,虽然实现复杂一些,甚至效率也略低,但这是使用 Union-Find 算法的通用思想,值得一学。
-
-**你可以把那些不需要被替换的 `O` 看成一个拥有独门绝技的门派,它们有一个共同祖师爷叫 `dummy`,这些 `O` 和 `dummy` 互相连通,而那些需要被替换的 `O` 与 `dummy` 不连通**。
-
-![](../pictures/unionfind应用/3.jpg)
-
-这就是 Union-Find 的核心思路,明白这个图,就很容易看懂代码了。
-
-首先要解决的是,根据我们的实现,Union-Find 底层用的是一维数组,构造函数需要传入这个数组的大小,而题目给的是一个二维棋盘。
-
-这个很简单,二维坐标 `(x,y)` 可以转换成 `x * n + y` 这个数(`m` 是棋盘的行数,`n` 是棋盘的列数)。敲黑板,**这是将二维坐标映射到一维的常用技巧**。
-
-其次,我们之前描述的「祖师爷」是虚构的,需要给他老人家留个位置。索引 `[0.. m*n-1]` 都是棋盘内坐标的一维映射,那就让这个虚拟的 `dummy` 节点占据索引 `m * n` 好了。
-
-```java
-void solve(char[][] board) {
- if (board.length == 0) return;
-
- int m = board.length;
- int n = board[0].length;
- // 给 dummy 留一个额外位置
- UF uf = new UF(m * n + 1);
- int dummy = m * n;
- // 将首列和末列的 O 与 dummy 连通
- for (int i = 0; i < m; i++) {
- if (board[i][0] == 'O')
- uf.union(i * n, dummy);
- if (board[i][n - 1] == 'O')
- uf.union(i * n + n - 1, dummy);
- }
- // 将首行和末行的 O 与 dummy 连通
- for (int j = 0; j < n; j++) {
- if (board[0][j] == 'O')
- uf.union(j, dummy);
- if (board[m - 1][j] == 'O')
- uf.union(n * (m - 1) + j, dummy);
- }
- // 方向数组 d 是上下左右搜索的常用手法
- int[][] d = new int[][]{{1,0}, {0,1}, {0,-1}, {-1,0}};
- for (int i = 1; i < m - 1; i++)
- for (int j = 1; j < n - 1; j++)
- if (board[i][j] == 'O')
- // 将此 O 与上下左右的 O 连通
- for (int k = 0; k < 4; k++) {
- int x = i + d[k][0];
- int y = j + d[k][1];
- if (board[x][y] == 'O')
- uf.union(x * n + y, i * n + j);
- }
- // 所有不和 dummy 连通的 O,都要被替换
- for (int i = 1; i < m - 1; i++)
- for (int j = 1; j < n - 1; j++)
- if (!uf.connected(dummy, i * n + j))
- board[i][j] = 'X';
-}
-```
-
-这段代码很长,其实就是刚才的思路实现,只有和边界 `O` 相连的 `O` 才具有和 `dummy` 的连通性,他们不会被替换。
-
-说实话,Union-Find 算法解决这个简单的问题有点杀鸡用牛刀,它可以解决更复杂,更具有技巧性的问题,**主要思路是适时增加虚拟节点,想办法让元素「分门别类」,建立动态连通关系**。
-
-### 二、判定合法等式
-
-这个问题用 Union-Find 算法就显得十分优美了。题目是这样:
-
-给你一个数组 `equations`,装着若干字符串表示的算式。每个算式 `equations[i]` 长度都是 4,而且只有这两种情况:`a==b` 或者 `a!=b`,其中 `a,b` 可以是任意小写字母。你写一个算法,如果 `equations` 中所有算式都不会互相冲突,返回 true,否则返回 false。
-
-比如说,输入 `["a==b","b!=c","c==a"]`,算法返回 false,因为这三个算式不可能同时正确。
-
-再比如,输入 `["c==c","b==d","x!=z"]`,算法返回 true,因为这三个算式并不会造成逻辑冲突。
-
-我们前文说过,动态连通性其实就是一种等价关系,具有「自反性」「传递性」和「对称性」,其实 `==` 关系也是一种等价关系,具有这些性质。所以这个问题用 Union-Find 算法就很自然。
-
-核心思想是,**将 `equations` 中的算式根据 `==` 和 `!=` 分成两部分,先处理 `==` 算式,使得他们通过相等关系各自勾结成门派;然后处理 `!=` 算式,检查不等关系是否破坏了相等关系的连通性**。
-
-```java
-boolean equationsPossible(String[] equations) {
- // 26 个英文字母
- UF uf = new UF(26);
- // 先让相等的字母形成连通分量
- for (String eq : equations) {
- if (eq.charAt(1) == '=') {
- char x = eq.charAt(0);
- char y = eq.charAt(3);
- uf.union(x - 'a', y - 'a');
- }
- }
- // 检查不等关系是否打破相等关系的连通性
- for (String eq : equations) {
- if (eq.charAt(1) == '!') {
- char x = eq.charAt(0);
- char y = eq.charAt(3);
- // 如果相等关系成立,就是逻辑冲突
- if (uf.connected(x - 'a', y - 'a'))
- return false;
- }
- }
- return true;
-}
-```
-
-至此,这道判断算式合法性的问题就解决了,借助 Union-Find 算法,是不是很简单呢?
-
-### 三、简单总结
-
-使用 Union-Find 算法,主要是如何把原问题转化成图的动态连通性问题。对于算式合法性问题,可以直接利用等价关系,对于棋盘包围问题,则是利用一个虚拟节点,营造出动态连通特性。
-
-另外,将二维数组映射到一维数组,利用方向数组 `d` 来简化代码量,都是在写算法时常用的一些小技巧,如果没见过可以注意一下。
-
-很多更复杂的 DFS 算法问题,都可以利用 Union-Find 算法更漂亮的解决。LeetCode 上 Union-Find 相关的问题也就二十多道,有兴趣的读者可以去做一做。
-
-
-
-**_____________**
-
-**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**。
-
-**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。
-
-
-
-
-======其他语言代码======
-
-[130.被围绕的区域](https://leetcode-cn.com/problems/surrounded-regions)
-
-[990.等式方程的可满足性](https://leetcode-cn.com/problems/satisfiability-of-equality-equations)
-
-[261.以图判树](https://leetcode-cn.com/problems/graph-valid-tree/)
-
-
-
-### java
-
-第261题的Java代码(提供:[LEODPEN](https://github.com/LEODPEN))
-
-```java
-class Solution {
-
- class DisjointSet {
-
- int count; // 连通分量的总个数
- int[] parent; // 每个节点的头节点(不一定是连通分量的最终头节点)
- int[] size; // 每个连通分量的大小
-
- public DisjointSet(int n) {
- parent = new int[n];
- size = new int[n];
- // 初为n个连通分量,期望最后为1
- count = n;
- for (int i = 0; i < n; i++) {
- // 初始的连通分量只有该节点本身
- parent[i] = i;
- size[i] = 1;
- }
- }
-
- /**
- * @param first 节点1
- * @param second 节点2
- * @return 未连通 && 连通成功
- */
- public boolean union(int first, int second) {
- // 分别找到包含first 和 second 的最终根节点
- int firstParent = findRootParent(first), secondParent = findRootParent(second);
- // 相等说明已经处于一个连通分量,即说明有环
- if (firstParent == secondParent) return false;
- // 将较小的连通分量融入较大的连通分量
- if (size[firstParent] >= size[secondParent]) {
- parent[secondParent] = firstParent;
- size[firstParent] += size[secondParent];
- } else {
- parent[firstParent] = secondParent;
- size[secondParent] += size[firstParent];
- }
- // 连通分量已合并,count减少
- count--;
- return true;
- }
-
- /**
- * @param node 某节点
- * @return 包含该节点的连通分量的最终根节点
- */
- private int findRootParent(int node) {
- while (node != parent[node]) {
- // 压缩路径
- parent[node] = parent[parent[node]];
- node = parent[node];
- }
- return node;
- }
- }
-
- public boolean validTree(int n, int[][] edges) {
- // 树的特性:节点数 = 边数 + 1
- if (edges.length != n - 1) return false;
- DisjointSet djs = new DisjointSet(n);
- for (int[] edg : edges) {
- // 判断连通情况(如果合并的两个点在一个连通分量里,说明有环)
- if (!djs.union(edg[0], edg[1])) return false;
- }
- // 是否全部节点均已相连
- return djs.count == 1;
- }
-}
-```
-
-
-
-### javascript
-
-[130.被围绕的区域](https://leetcode-cn.com/problems/surrounded-regions)
-
-```js
-class UF {
- // 记录连通分量
- count;
-
- // 节点 x 的根节点是 parent[x]
- parent;
-
- // 记录树的“重量”
- size;
-
- constructor(n) {
-
- // 一开始互不连通
- this.count = n;
-
- // 父节点指针初始指向自己
- this.parent = new Array(n);
-
- this.size = new Array(n);
-
- for (let i = 0; i < n; i++) {
- this.parent[i] = i;
- this.size[i] = 1;
- }
- }
-
- /* 返回某个节点 x 的根节点 */
- find(x) {
- // 根节点的 parent[x] == x
- while (this.parent[x] !== x) {
- // 进行路径压缩
- this.parent[x] = this.parent[this.parent[x]];
- x = this.parent[x];
- }
- return x;
- }
-
- /* 将 p 和 q 连接 */
- union(p, q) {
- // 如果某两个节点被连通,则让其中的(任意)
- // 一个节点的根节点接到另一个节点的根节点上
- let rootP = this.find(p);
- let rootQ = this.find(q);
- if (rootP === rootQ) return;
-
- // 小树接到大树下面,较平衡
- if (this.size[rootP] > this.size[rootQ]) {
- this.parent[rootQ] = rootP;
- this.size[rootP] += this.size[rootQ];
- } else {
- this.parent[rootP] = rootQ;
- this.size[rootQ] += this.size[rootP];
- }
-
- this.count--; // 两个分量合二为一
- }
-
- /* 判断 p 和 q 是否连通 */
- connected(p, q) {
- let rootP = this.find(p);
- let rootQ = this.find(q);
- return rootP === rootQ;
- };
-
- /* 返回图中有多少个连通分量 */
- getCount() {
- return this.count;
- };
-}
-
-/**
- * @param {[][]} board
- * @return {void} Do not return anything, modify board in-place instead.
- */
-let solve = function (board) {
- if (board.length === 0) return;
-
- let m = board.length;
- let n = board[0].length;
-
- // 给 dummy 留一个额外位置
- let uf = new UF(m * n + 1);
- let dummy = m * n;
- // 将首列和末列的 O 与 dummy 连通
- for (let i = 0; i < m; i++) {
- if (board[i][0] === 'O')
- uf.union(i * n, dummy);
- if (board[i][n - 1] === 'O')
- uf.union(i * n + n - 1, dummy);
- }
- // 将首行和末行的 O 与 dummy 连通
- for (let j = 0; j < n; j++) {
- if (board[0][j] === 'O')
- uf.union(j, dummy);
- if (board[m - 1][j] === 'O')
- uf.union(n * (m - 1) + j, dummy);
- }
- // 方向数组 d 是上下左右搜索的常用手法
- let d = [[1, 0], [0, 1], [0, -1], [-1, 0]];
- for (let i = 1; i < m - 1; i++)
- for (let j = 1; j < n - 1; j++)
- if (board[i][j] === 'O')
- // 将此 O 与上下左右的 O 连通
- for (let k = 0; k < 4; k++) {
- let x = i + d[k][0];
- let y = j + d[k][1];
- if (board[x][y] === 'O')
- uf.union(x * n + y, i * n + j);
- }
- // 所有不和 dummy 连通的 O,都要被替换
- for (let i = 1; i < m - 1; i++)
- for (let j = 1; j < n - 1; j++)
- if (!uf.connected(dummy, i * n + j))
- board[i][j] = 'X';
-}
-```
-
-
-
-[990.等式方程的可满足性](https://leetcode-cn.com/problems/surrounded-regions)
-
-需要注意的点主要为js字符与ASCII码互转。
-
-在java、c这些语言中,字符串直接相减,得到的是ASCII码的差值,结果为整数;而js中`"a" - "b"`的结果为NaN,所以需要使用`charCodeAt(index)`方法来获取字符的ASCII码,index不填时,默认结果为第一个字符的ASCII码。
-
-```js
-class UF {
- // 记录连通分量
- count;
-
- // 节点 x 的根节点是 parent[x]
- parent;
-
- // 记录树的“重量”
- size;
-
- constructor(n) {
-
- // 一开始互不连通
- this.count = n;
-
- // 父节点指针初始指向自己
- this.parent = new Array(n);
-
- this.size = new Array(n);
-
- for (let i = 0; i < n; i++) {
- this.parent[i] = i;
- this.size[i] = 1;
- }
- }
-
- /* 返回某个节点 x 的根节点 */
- find(x) {
- // 根节点的 parent[x] == x
- while (this.parent[x] !== x) {
- // 进行路径压缩
- this.parent[x] = this.parent[this.parent[x]];
- x = this.parent[x];
- }
- return x;
- }
-
- /* 将 p 和 q 连接 */
- union(p, q) {
- // 如果某两个节点被连通,则让其中的(任意)
- // 一个节点的根节点接到另一个节点的根节点上
- let rootP = this.find(p);
- let rootQ = this.find(q);
- if (rootP === rootQ) return;
-
- // 小树接到大树下面,较平衡
- if (this.size[rootP] > this.size[rootQ]) {
- this.parent[rootQ] = rootP;
- this.size[rootP] += this.size[rootQ];
- } else {
- this.parent[rootP] = rootQ;
- this.size[rootQ] += this.size[rootP];
- }
-
- this.count--; // 两个分量合二为一
- }
-
- /* 判断 p 和 q 是否连通 */
- connected(p, q) {
- let rootP = this.find(p);
- let rootQ = this.find(q);
- return rootP === rootQ;
- };
-
- /* 返回图中有多少个连通分量 */
- getCount() {
- return this.count;
- };
-}
-/**
- * @param {string[]} equations
- * @return {boolean}
- */
-let equationsPossible = function (equations) {
- // 26 个英文字母
- let uf = new UF(26);
- // 先让相等的字母形成连通分量
- for (let eq of equations) {
- if (eq[1] === '=') {
- let x = eq[0];
- let y = eq[3];
-
- // 'a'.charCodeAt() 为 97
- uf.union(x.charCodeAt(0) - 97, y.charCodeAt(0) - 97);
- }
- }
- // 检查不等关系是否打破相等关系的连通性
- for (let eq of equations) {
- if (eq[1] === '!') {
- let x = eq[0];
- let y = eq[3];
- // 如果相等关系成立,就是逻辑冲突
- if (uf.connected(x.charCodeAt(0) - 97, y.charCodeAt(0) - 97))
- return false;
- }
- }
- return true;
-};
-```
-
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\350\257\246\350\247\243.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\350\257\246\350\247\243.md"
index a86594d9298f4109201dd4d3764829c9212afa81..6147c6eac79c174b7c8e05a090bc7bbb67bca99a 100644
--- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\350\257\246\350\247\243.md"
+++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/UnionFind\347\256\227\346\263\225\350\257\246\350\247\243.md"
@@ -1,9 +1,5 @@
# Union-Find 算法详解
-
-
-
-
@@ -13,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -527,6 +523,41 @@ class UF {
最后,Union-Find 算法也会在一些其他经典图论算法中用到,比如判断「图」和「树」,以及最小生成树的计算,详情见 [Kruskal 最小生成树算法](https://labuladong.github.io/article/fname.html?fname=kruskal)。
+
+
+
+
+引用本文的文章
+
+ - [Dijkstra 算法模板及应用](https://labuladong.github.io/article/fname.html?fname=dijkstra算法)
+ - [Kruskal 最小生成树算法](https://labuladong.github.io/article/fname.html?fname=kruskal)
+ - [Prim 最小生成树算法](https://labuladong.github.io/article/fname.html?fname=prim算法)
+ - [一文秒杀所有岛屿题目](https://labuladong.github.io/article/fname.html?fname=岛屿题目)
+ - [二分图判定算法](https://labuladong.github.io/article/fname.html?fname=二分图)
+ - [我的刷题心得](https://labuladong.github.io/article/fname.html?fname=算法心得)
+
+
+
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| [1361. Validate Binary Tree Nodes](https://leetcode.com/problems/validate-binary-tree-nodes/?show=1) | [1361. 验证二叉树](https://leetcode.cn/problems/validate-binary-tree-nodes/?show=1) |
+| [200. Number of Islands](https://leetcode.com/problems/number-of-islands/?show=1) | [200. 岛屿数量](https://leetcode.cn/problems/number-of-islands/?show=1) |
+| [261. Graph Valid Tree](https://leetcode.com/problems/graph-valid-tree/?show=1)🔒 | [261. 以图判树](https://leetcode.cn/problems/graph-valid-tree/?show=1)🔒 |
+| [765. Couples Holding Hands](https://leetcode.com/problems/couples-holding-hands/?show=1) | [765. 情侣牵手](https://leetcode.cn/problems/couples-holding-hands/?show=1) |
+
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/twoSum\351\227\256\351\242\230\347\232\204\346\240\270\345\277\203\346\200\235\346\203\263.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/twoSum\351\227\256\351\242\230\347\232\204\346\240\270\345\277\203\346\200\235\346\203\263.md"
deleted file mode 100644
index 4586a15ea5e174f11d7ae332bdc2ad8e288e4e6c..0000000000000000000000000000000000000000
--- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/twoSum\351\227\256\351\242\230\347\232\204\346\240\270\345\277\203\346\200\235\346\203\263.md"
+++ /dev/null
@@ -1,300 +0,0 @@
-# twoSum问题的核心思想
-
-
-
-
-
-
-
-
-
-![](../pictures/souyisou.png)
-
-**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
-
-读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
-
-[1.两数之和](https://leetcode-cn.com/problems/two-sum)
-
-[170.两数之和 III - 数据结构设计](https://leetcode-cn.com/problems/two-sum-iii-data-structure-design)
-
-**-----------**
-
-Two Sum 系列问题在 LeetCode 上有好几道,这篇文章就挑出有代表性的几道,介绍一下这种问题怎么解决。
-
-### TwoSum I
-
-这个问题的**最基本形式**是这样:给你一个数组和一个整数 `target`,可以保证数组中**存在**两个数的和为 `target`,请你返回这两个数的索引。
-
-比如输入 `nums = [3,1,3,6], target = 6`,算法应该返回数组 `[0,2]`,因为 3 + 3 = 6。
-
-这个问题如何解决呢?首先最简单粗暴的办法当然是穷举了:
-
-```java
-int[] twoSum(int[] nums, int target) {
-
- for (int i = 0; i < nums.length; i++)
- for (int j = i + 1; j < nums.length; j++)
- if (nums[j] == target - nums[i])
- return new int[] { i, j };
-
- // 不存在这么两个数
- return new int[] {-1, -1};
-}
-```
-
-这个解法非常直接,时间复杂度 O(N^2),空间复杂度 O(1)。
-
-可以通过一个哈希表减少时间复杂度:
-
-```java
-int[] twoSum(int[] nums, int target) {
- int n = nums.length;
- index index = new HashMap<>();
- // 构造一个哈希表:元素映射到相应的索引
- for (int i = 0; i < n; i++)
- index.put(nums[i], i);
-
- for (int i = 0; i < n; i++) {
- int other = target - nums[i];
- // 如果 other 存在且不是 nums[i] 本身
- if (index.containsKey(other) && index.get(other) != i)
- return new int[] {i, index.get(other)};
- }
-
- return new int[] {-1, -1};
-}
-```
-
-这样,由于哈希表的查询时间为 O(1),算法的时间复杂度降低到 O(N),但是需要 O(N) 的空间复杂度来存储哈希表。不过综合来看,是要比暴力解法高效的。
-
-**我觉得 Two Sum 系列问题就是想教我们如何使用哈希表处理问题**。我们接着往后看。
-
-### TwoSum II
-
-这里我们稍微修改一下上面的问题。我们设计一个类,拥有两个 API:
-
-```java
-class TwoSum {
- // 向数据结构中添加一个数 number
- public void add(int number);
- // 寻找当前数据结构中是否存在两个数的和为 value
- public boolean find(int value);
-}
-```
-
-如何实现这两个 API 呢,我们可以仿照上一道题目,使用一个哈希表辅助 `find` 方法:
-
-```java
-class TwoSum {
- Map freq = new HashMap<>();
-
- public void add(int number) {
- // 记录 number 出现的次数
- freq.put(number, freq.getOrDefault(number, 0) + 1);
- }
-
- public boolean find(int value) {
- for (Integer key : freq.keySet()) {
- int other = value - key;
- // 情况一
- if (other == key && freq.get(key) > 1)
- return true;
- // 情况二
- if (other != key && freq.containsKey(other))
- return true;
- }
- return false;
- }
-}
-```
-
-进行 `find` 的时候有两种情况,举个例子:
-
-情况一:`add` 了 `[3,3,2,5]` 之后,执行 `find(6)`,由于 3 出现了两次,3 + 3 = 6,所以返回 true。
-
-情况二:`add` 了 `[3,3,2,5]` 之后,执行 `find(7)`,那么 `key` 为 2,`other` 为 5 时算法可以返回 true。
-
-除了上述两种情况外,`find` 只能返回 false 了。
-
-对于这个解法的时间复杂度呢,`add` 方法是 O(1),`find` 方法是 O(N),空间复杂度为 O(N),和上一道题目比较类似。
-
-**但是对于 API 的设计,是需要考虑现实情况的**。比如说,我们设计的这个类,使用 `find` 方法非常频繁,那么每次都要 O(N) 的时间,岂不是很浪费费时间吗?对于这种情况,我们是否可以做些优化呢?
-
-是的,对于频繁使用 `find` 方法的场景,我们可以进行优化。我们可以参考上一道题目的暴力解法,借助**哈希集合**来针对性优化 `find` 方法:
-
-```java
-class TwoSum {
- Set sum = new HashSet<>();
- List nums = new ArrayList<>();
-
- public void add(int number) {
- // 记录所有可能组成的和
- for (int n : nums)
- sum.add(n + number);
- nums.add(number);
- }
-
- public boolean find(int value) {
- return sum.contains(value);
- }
-}
-```
-
-这样 `sum` 中就储存了所有加入数字可能组成的和,每次 `find` 只要花费 O(1) 的时间在集合中判断一下是否存在就行了,显然非常适合频繁使用 `find` 的场景。
-
-### 三、总结
-
-对于 TwoSum 问题,一个难点就是给的数组**无序**。对于一个无序的数组,我们似乎什么技巧也没有,只能暴力穷举所有可能。
-
-**一般情况下,我们会首先把数组排序再考虑双指针技巧**。TwoSum 启发我们,HashMap 或者 HashSet 也可以帮助我们处理无序数组相关的简单问题。
-
-另外,设计的核心在于权衡,利用不同的数据结构,可以得到一些针对性的加强。
-
-最后,如果 TwoSum I 中给的数组是有序的,应该如何编写算法呢?答案很简单,前文「双指针技巧汇总」写过:
-
-```java
-int[] twoSum(int[] nums, int target) {
- int left = 0, right = nums.length - 1;
- while (left < right) {
- int sum = nums[left] + nums[right];
- if (sum == target) {
- return new int[]{left, right};
- } else if (sum < target) {
- left++; // 让 sum 大一点
- } else if (sum > target) {
- right--; // 让 sum 小一点
- }
- }
- // 不存在这样两个数
- return new int[]{-1, -1};
-}
-```
-
-
-
-**_____________**
-
-**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**。
-
-**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。
-
-
-
-
-======其他语言代码======
-
-[1.两数之和](https://leetcode-cn.com/problems/two-sum)
-
-[170.两数之和 III - 数据结构设计](https://leetcode-cn.com/problems/two-sum-iii-data-structure-design)
-
-
-
-### python
-
-由[JodyZ0203](https://github.com/JodyZ0203)提供 1. Two Sums Python3 解法代码:
-
-只用一个哈希表
-
-```python
-class Solution:
- def twoSum(self, nums, target):
- """
- :type nums: List[int]
- :type target: int
- :rtype: List[int]
- """
- # 提前构造一个哈希表
- hashTable = {}
- # 寻找两个目标数值
- for i, n in enumerate(nums):
- other_num = target - n
- # 如果存在这个余数 other_num
- if other_num in hashTable.keys():
- # 查看是否存在哈希表里,如果存在的话就返回数组
- return [i, hashTable[other_num]]
- # 如果不存在的话继续处理剩余的数
- hashTable[n] = i
-```
-
-
-
-### javascript
-
-[1.两数之和](https://leetcode-cn.com/problems/two-sum)
-
-穷举
-
-```js
-/**
- * @param {number[]} nums
- * @param {number} target
- * @return {number[]}
- */
-var twoSum = function (nums, target) {
- for (let i = 0; i < nums.length; i++)
- for (let j = i + 1; j < nums.length; j++)
- if (nums[j] === target - nums[i])
- return [i, j];
-
- // 不存在这么两个数
- return [-1, -1];
-};
-```
-
-备忘录
-
-```js
-/**
- * @param {number[]} nums
- * @param {number} target
- * @return {number[]}
- */
-var twoSum = function (nums, target) {
- let n = nums.length;
- let index = new Map();
- // 构造一个哈希表:元素映射到相应的索引
- for (let i = 0; i < n; i++)
- index.set(nums[i], i);
-
- for (let i = 0; i < n; i++) {
- let other = target - nums[i];
- // 如果 other 存在且不是 nums[i] 本身
- if (index.has(other) && index.get(other) !== i)
- return [i, index.get(other)];
- }
-
- // 不存在这么两个数
- return [-1, -1];
-};
-```
-
-
-
-[170.两数之和 III - 数据结构设计](https://leetcode-cn.com/problems/two-sum-iii-data-structure-design)
-
-哈希集合优化。
-
-```js
-class TwoSum {
- constructor() {
- this.sum = new Set();
- this.nums = [];
- }
-
- // 向数据结构中添加一个数 number
- add(number) {
- // 记录所有可能组成的和
- for (let n of this.nums) {
- this.sum.push(n + number)
- }
- this.nums.add(number);
- }
-
- // 寻找当前数据结构中是否存在两个数的和为 value
- find(value) {
- return this.sum.has(value);
- }
-}
-```
-
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\270\272\344\273\200\344\271\210\346\216\250\350\215\220\347\256\227\346\263\2254.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\270\272\344\273\200\344\271\210\346\216\250\350\215\220\347\256\227\346\263\2254.md"
deleted file mode 100644
index 2938724895512329c154753bf5ea88a5ed5b1c42..0000000000000000000000000000000000000000
--- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\270\272\344\273\200\344\271\210\346\216\250\350\215\220\347\256\227\346\263\2254.md"
+++ /dev/null
@@ -1,102 +0,0 @@
-# 为什么我推荐《算法4》
-
-
-
-
-
-
-
-
-
-![](../pictures/souyisou.png)
-
-**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
-
-**-----------**
-
-通知:如果本站对你学习算法有帮助,**请收藏网址,并推荐给你的朋友**。由于 **labuladong** 的算法套路太火,很多人直接拿我的 GitHub 文章去开付费专栏,价格还不便宜。我这免费写给你看,**多宣传原创作者是你唯一能做的**,谁也不希望劣币驱逐良币对吧?
-
-咱们的公众号有很多硬核的算法文章,今天就聊点轻松的,就具体聊聊我非常“鼓吹”的《算法4》。这本书我在之前的文章多次推荐过,但是没有具体的介绍,今天就来正式介绍一下。。
-
-我的推荐不会直接甩一大堆书目,而是会联系实际生活,讲一些书中有趣有用的知识,无论你最后会不会去看这本书,本文都会给你带来一些收获。
-
-**首先这本书是适合初学者的**。总是有很多读者问,我只会 C 语言,能不能看《算法4》?学算法最好用什么语言?诸如此类的问题。
-
-经常看咱们公众号的读者应该体会到了,算法其实是一种思维模式,和你用什么语言没啥关系。我们的文章也不会固定用某一种语言,而是什么语言写出来容易理解就用什么语言。再退一步说,到底适不适合你,网上找个 PDF 亲自看一下不就知道了?
-
-《算法4》看起来挺厚的,但是前面几十页是教你 Java 的;每章后面还有习题,占了不少页数;每章还有一些数学证明,这些都可以忽略。这样算下来,剩下的就是基础知识和疑难解答之类的内容,含金量很高,把这些基础知识动手实践一遍,真的就可以达到不错的水平了。
-
-我觉得这本书之所以能有这么高的评分,一个是因为讲解详细,还有大量配图,另一个原因就是书中把一些算法和现实生活中的使用场景联系起来,你不仅知道某个算法怎么实现,也知道它大概能运用到什么场景,下面我就来介绍两个图算法的简单应用。
-
-### 一、二分图的应用
-
-我想举的第一个例子是**二分图**。简单来说,二分图就是一幅拥有特殊性质的图:能够用两种颜色为所有顶点着色,使得任何一条边的两个顶点颜色不同。
-
-![](../pictures/algo4/1.jpg)
-
-明白了二分图是什么,能解决什么实际问题呢?**算法方面,常见的操作是如何判定一幅图是不是二分图**。比如说下面这道 LeetCode 题目:
-
-![](../pictures/algo4/title.png)
-
-你想想,如果我们把每个人视为一个顶点,边代表讨厌;相互讨厌的两个人之间连接一条边,就可以形成一幅图。那么根据刚才二分图的定义,如果这幅图是一幅二分图,就说明这些人可以被分为两组,否则的话就不行。
-
-这是判定二分图算法的一个应用,**其实二分图在数据结构方面也有一些不错的特性**。
-
-比如说我们需要一种数据结构来储存电影和演员之间的关系:某一部电影肯定是由多位演员出演的,且某一位演员可能会出演多部电影。你使用什么数据结构来存储这种关系呢?
-
-既然是存储映射关系,最简单的不就是使用哈希表嘛,我们可以使用一个 `HashMap>` 来存储电影到演员列表的映射,如果给一部电影的名字,就能快速得到出演该电影的演员。
-
-但是如果给出一个演员的名字,我们想快速得到该演员演出的所有电影,怎么办呢?这就需要「反向索引」,对之前的哈希表进行一些操作,新建另一个哈希表,把演员作为键,把电影列表作为值。
-
-对于上面这个例子,可以使用二分图来取代哈希表。电影和演员是具有二分图性质的:如果把电影和演员视为图中的顶点,出演关系作为边,那么与电影顶点相连的一定是演员,与演员相邻的一定是电影,不存在演员和演员相连,电影和电影相连的情况。
-
-回顾二分图的定义,如果对演员和电影顶点着色,肯定就是一幅二分图:
-
-![](../pictures/algo4/2.jpg)
-
-如果这幅图构建完成,就不需要反向索引,对于演员顶点,其直接连接的顶点就是他出演的电影,对于电影顶点,其直接连接的顶点就是出演演员。
-
-当然,对于这个问题,书中还提到了一些其他有趣的玩法,比如说社交网络中「间隔度数」的计算(六度空间理论应该听说过)等等,其实就是一个 BFS 广度优先搜索寻找最短路径的问题,具体代码实现这里就不展开了。
-
-### 二、套汇的算法
-
-如果我们说货币 A 到货币 B 的汇率是 10,意思就是 1 单位的货币 A 可以换 10 单位货币 B。如果我们把每种货币视为一幅图的顶点,货币之间的汇率视为加权有向边,那么整个汇率市场就是一幅「完全加权有向图」。
-
-一旦把现实生活中的情景抽象成图,就有可能运用算法解决一些问题。比如说图中可能存在下面的情况:
-
-![](../pictures/algo4/3.jpg)
-
-图中的加权有向边代表汇率,我们可以发现如果把 100 单位的货币 A 换成 B,再换成 C,最后换回 A,就可以得到 100×0.9×0.8×1.4 = 100.8 单位的 A!如果交易的金额大一些的话,赚的钱是很可观的,这种空手套白狼的操作就是套汇。
-
-现实中交易会有种种限制,而且市场瞬息万变,但是套汇的利润还是很高的,关键就在于如何**快速**找到这种套汇机会呢?
-
-借助图的抽象,我们发现套汇机会其实就是一个环,且这个环上的权重之积大于 1,只要在顺着这个环交易一圈就能空手套白狼。
-
-图论中有一个经典算法叫做 **Bellman-Ford 算法,可以用于寻找负权重环**。对于我们说的套汇问题,可以先把所有边的权重 w 替换成 -ln(w),这样「寻找权重乘积大于 1 的环」就转化成了「寻找权重和小于 0 的环」,就可以使用 Bellman-Ford 算法在 O(EV) 的时间内寻找负权重环,也就是寻找套汇机会。
-
-《算法4》就介绍到这里,关于上面两个例子的具体内容,可以自己去看书,**公众号后台回复关键词「算法4」就有 PDF**。
-
-
-### 三、最后说几句
-
-首先,前文说对于数学证明、章后习题可以忽略,可能有人要抬杠了:难道习题和数学证明不重要吗?
-
-那我想说,就是不重要,起码对大多数人来说不重要。我觉得吧,学习就要带着目的性去学,大部分人学算法不就是巩固计算机知识,对付面试题目吗?**如果是这个目的**,那就学些基本的数据结构和经典算法,明白它们的时间复杂度,然后去刷题就好了,何必和习题、证明过不去?
-
-这也是我从来不推荐《算法导论》这本书的原因。如果有人给你推荐这本书,只可能有两个原因,要么他是真大佬,要么他在装大佬。《算法导论》中充斥大量数学证明,而且很多数据结构是很少用到的,顶多当个字典用。你说你学了那些有啥用呢,饶过自己呗。
-
-另外,读书在精不在多。你花时间《算法4》过个大半(最后小半部分有点困难),同时刷点题,看看咱们的公众号文章,算法这块真就够了,别对细节问题太较真。
-
-
-
-**_____________**
-
-**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**。
-
-**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。
-
-
-
-
-
-======其他语言代码======
\ No newline at end of file
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\350\257\246\350\247\243.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\350\257\246\350\247\243.md"
index f2d19a7250a5ee5bf9221b3e0eb13a6db465c49e..7e1377ed861e4b480a21541d1166ff8c2d7c792a 100644
--- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\350\257\246\350\247\243.md"
+++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\350\257\246\350\247\243.md"
@@ -1,11 +1,5 @@
# 二分查找算法详解
-
-
-
-
-
-
@@ -15,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -505,6 +499,58 @@ int right_bound(int[] nums, int target) {
理解本文能保证你写出正确的二分查找的代码,但实际题目中不会直接让你写二分代码,我会在 [二分查找的变体](https://appktavsiei5995.pc.xiaoe-tech.com/detail/i_62a07736e4b01a485209b0b4/1) 和 [二分查找的运用](https://labuladong.github.io/article/fname.html?fname=二分运用) 中进一步讲解如何把二分思维运用到更多算法题中。
+
+
+
+
+引用本文的文章
+
+ - [base case 和备忘录的初始值怎么定?](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://appktavsiei5995.pc.xiaoe-tech.com/detail/i_62a07736e4b01a485209b0b4/1)
+ - [二分查找高效判定子序列](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=滑动窗口技巧进阶)
+ - [我的刷题心得](https://labuladong.github.io/article/fname.html?fname=算法心得)
+ - [讲两道常考的阶乘算法题](https://labuladong.github.io/article/fname.html?fname=阶乘题目)
+
+
+
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| [1201. Ugly Number III](https://leetcode.com/problems/ugly-number-iii/?show=1) | [1201. 丑数 III](https://leetcode.cn/problems/ugly-number-iii/?show=1) |
+| [162. Find Peak Element](https://leetcode.com/problems/find-peak-element/?show=1) | [162. 寻找峰值](https://leetcode.cn/problems/find-peak-element/?show=1) |
+| [240. Search a 2D Matrix II](https://leetcode.com/problems/search-a-2d-matrix-ii/?show=1) | [240. 搜索二维矩阵 II](https://leetcode.cn/problems/search-a-2d-matrix-ii/?show=1) |
+| [33. Search in Rotated Sorted Array](https://leetcode.com/problems/search-in-rotated-sorted-array/?show=1) | [33. 搜索旋转排序数组](https://leetcode.cn/problems/search-in-rotated-sorted-array/?show=1) |
+| [35. Search Insert Position](https://leetcode.com/problems/search-insert-position/?show=1) | [35. 搜索插入位置](https://leetcode.cn/problems/search-insert-position/?show=1) |
+| [74. Search a 2D Matrix](https://leetcode.com/problems/search-a-2d-matrix/?show=1) | [74. 搜索二维矩阵](https://leetcode.cn/problems/search-a-2d-matrix/?show=1) |
+| [793. Preimage Size of Factorial Zeroes Function](https://leetcode.com/problems/preimage-size-of-factorial-zeroes-function/?show=1) | [793. 阶乘函数后 K 个零](https://leetcode.cn/problems/preimage-size-of-factorial-zeroes-function/?show=1) |
+| [81. Search in Rotated Sorted Array II](https://leetcode.com/problems/search-in-rotated-sorted-array-ii/?show=1) | [81. 搜索旋转排序数组 II](https://leetcode.cn/problems/search-in-rotated-sorted-array-ii/?show=1) |
+| [852. Peak Index in a Mountain Array](https://leetcode.com/problems/peak-index-in-a-mountain-array/?show=1) | [852. 山脉数组的峰顶索引](https://leetcode.cn/problems/peak-index-in-a-mountain-array/?show=1) |
+| - | [剑指 Offer 04. 二维数组中的查找](https://leetcode.cn/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof/?show=1) |
+| - | [剑指 Offer 53 - I. 在排序数组中查找数字 I](https://leetcode.cn/problems/zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof/?show=1) |
+| - | [剑指 Offer 53 - II. 0~n-1中缺失的数字](https://leetcode.cn/problems/que-shi-de-shu-zi-lcof/?show=1) |
+| - | [剑指 Offer II 068. 查找插入位置](https://leetcode.cn/problems/N6YdxV/?show=1) |
+| - | [剑指 Offer II 069. 山峰数组的顶部](https://leetcode.cn/problems/B1IidL/?show=1) |
+
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\277\241\345\260\201\345\265\214\345\245\227\351\227\256\351\242\230.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\277\241\345\260\201\345\265\214\345\245\227\351\227\256\351\242\230.md"
deleted file mode 100644
index 64ee928e2c00500e4a7eef368da42147b92b4340..0000000000000000000000000000000000000000
--- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\344\277\241\345\260\201\345\265\214\345\245\227\351\227\256\351\242\230.md"
+++ /dev/null
@@ -1,255 +0,0 @@
-# 信封嵌套问题
-
-
-
-
-
-
-
-
-
-![](../pictures/souyisou.png)
-
-**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
-
-读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
-
-[354.俄罗斯套娃信封问题](https://leetcode-cn.com/problems/russian-doll-envelopes)
-
-**-----------**
-
-很多算法问题都需要排序技巧,其难点不在于排序本身,而是需要巧妙地排序进行预处理,将算法问题进行转换,为之后的操作打下基础。
-
-信封嵌套问题就需要先按特定的规则排序,之后就转换为一个 [最长递增子序列问题](https://labuladong.gitee.io/algo/),可以用前文 [二分查找详解](https://labuladong.gitee.io/algo/) 的技巧来解决了。
-
-### 一、题目概述
-
-信封嵌套问题是个很有意思且经常出现在生活中的问题,先看下题目:
-
-![title](../pictures/%E4%BF%A1%E5%B0%81%E5%B5%8C%E5%A5%97/title.png)
-
-这道题目其实是最长递增子序列(Longes Increasing Subsequence,简写为 LIS)的一个变种,因为很显然,每次合法的嵌套是大的套小的,相当于找一个最长递增的子序列,其长度就是最多能嵌套的信封个数。
-
-但是难点在于,标准的 LIS 算法只能在数组中寻找最长子序列,而我们的信封是由 `(w, h)` 这样的二维数对形式表示的,如何把 LIS 算法运用过来呢?
-
-![0](../pictures/%E4%BF%A1%E5%B0%81%E5%B5%8C%E5%A5%97/0.jpg)
-
-读者也许会想,通过 `w × h` 计算面积,然后对面积进行标准的 LIS 算法。但是稍加思考就会发现这样不行,比如 `1 × 10` 大于 `3 × 3`,但是显然这样的两个信封是无法互相嵌套的。
-
-### 二、解法
-
-这道题的解法是比较巧妙的:
-
-**先对宽度 `w` 进行升序排序,如果遇到 `w` 相同的情况,则按照高度 `h` 降序排序。之后把所有的 `h` 作为一个数组,在这个数组上计算 LIS 的长度就是答案。**
-
-画个图理解一下,先对这些数对进行排序:
-
-![1](../pictures/%E4%BF%A1%E5%B0%81%E5%B5%8C%E5%A5%97/1.jpg)
-
-然后在 `h` 上寻找最长递增子序列:
-
-![2](../pictures/%E4%BF%A1%E5%B0%81%E5%B5%8C%E5%A5%97/2.jpg)
-
-这个子序列就是最优的嵌套方案。
-
-这个解法的关键在于,对于宽度 `w` 相同的数对,要对其高度 `h` 进行降序排序。因为两个宽度相同的信封不能相互包含的,逆序排序保证在 `w` 相同的数对中最多只选取一个。
-
-下面看代码:
-
-```java
-// envelopes = [[w, h], [w, h]...]
-public int maxEnvelopes(int[][] envelopes) {
- int n = envelopes.length;
- // 按宽度升序排列,如果宽度一样,则按高度降序排列
- Arrays.sort(envelopes, new Comparator()
- {
- public int compare(int[] a, int[] b) {
- return a[0] == b[0] ?
- b[1] - a[1] : a[0] - b[0];
- }
- });
- // 对高度数组寻找 LIS
- int[] height = new int[n];
- for (int i = 0; i < n; i++)
- height[i] = envelopes[i][1];
-
- return lengthOfLIS(height);
-}
-```
-
-关于最长递增子序列的寻找方法,在前文中详细介绍了动态规划解法,并用扑克牌游戏解释了二分查找解法,本文就不展开了,直接套用算法模板:
-
-```java
-/* 返回 nums 中 LIS 的长度 */
-public int lengthOfLIS(int[] nums) {
- int piles = 0, n = nums.length;
- int[] top = new int[n];
- for (int i = 0; i < n; i++) {
- // 要处理的扑克牌
- int poker = nums[i];
- int left = 0, right = piles;
- // 二分查找插入位置
- while (left < right) {
- int mid = (left + right) / 2;
- if (top[mid] >= poker)
- right = mid;
- else
- left = mid + 1;
- }
- if (left == piles) piles++;
- // 把这张牌放到牌堆顶
- top[left] = poker;
- }
- // 牌堆数就是 LIS 长度
- return piles;
-}
-```
-
-为了清晰,我将代码分为了两个函数, 你也可以合并,这样可以节省下 `height` 数组的空间。
-
-此算法的时间复杂度为 `O(NlogN)`,因为排序和计算 LIS 各需要 `O(NlogN)` 的时间。
-
-空间复杂度为 `O(N)`,因为计算 LIS 的函数中需要一个 `top` 数组。
-
-### 三、总结
-
-这个问题是个 Hard 级别的题目,难就难在排序,正确地排序后此问题就被转化成了一个标准的 LIS 问题,容易解决一些。
-
-其实这种问题还可以拓展到三维,比如说现在不是让你嵌套信封,而是嵌套箱子,每个箱子有长宽高三个维度,请你算算最多能嵌套几个箱子?
-
-我们可能会这样想,先把前两个维度(长和宽)按信封嵌套的思路求一个嵌套序列,最后在这个序列的第三个维度(高度)找一下 LIS,应该能算出答案。
-
-实际上,这个思路是错误的。这类问题叫做「偏序问题」,上升到三维会使难度巨幅提升,需要借助一种高级数据结构「树状数组」,有兴趣的读者可以自行搜索。
-
-有很多算法问题都需要排序后进行处理,阿东正在进行整理总结。希望本文对你有帮助。
-
-**_____________**
-
-**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**。
-
-**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。
-
-
-
-
-
-======其他语言代码======
-
-[354.俄罗斯套娃信封问题](https://leetcode-cn.com/problems/russian-doll-envelopes)
-
-
-
-### javascript
-
-[300. 最长递增子序列](https://leetcode-cn.com/problems/longest-increasing-subsequence/)
-
-给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
-
-子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
-
-```js
-/**
- * @param {number[]} nums
- * @return {number}
- */
-let lengthOfLIS = function(nums) {
- let top = new Array(nums.length);
-
- // 牌堆数初始化为 0
- let piles = 0;
-
- for (let i = 0; i < nums.length; i++) {
- // 要处理的扑克牌
- let poker = nums[i];
-
- /***** 搜索左侧边界的二分查找 *****/
- let left = 0, right = piles;
- while (left < right) {
- let mid = (left + right) / 2;
- if (top[mid] > poker) {
- right = mid;
- } else if (top[mid] < poker) {
- left = mid + 1;
- } else {
- right = mid;
- }
- }
- /*********************************/
-
- // 没找到合适的牌堆,新建一堆
- if (left === piles) piles++;
-
- // 把这张牌放到牌堆顶
- top[left] = poker;
- }
-
- // 牌堆数就是 LIS 长度
- return piles;
-};
-```
-
-
-
-[354.俄罗斯套娃信封问题](https://leetcode-cn.com/problems/russian-doll-envelopes)
-
-```js
-/**
- * @param {number[]} nums
- * @return {number}
- */
-let lengthOfLIS = function(nums) {
- let top = new Array(nums.length);
-
- // 牌堆数初始化为 0
- let piles = 0;
-
- for (let i = 0; i < nums.length; i++) {
- // 要处理的扑克牌
- let poker = nums[i];
-
- /***** 搜索左侧边界的二分查找 *****/
- let left = 0, right = piles;
- while (left < right) {
- let mid = Math.floor((left + right) / 2);
- if (top[mid] > poker) {
- right = mid;
- } else if (top[mid] < poker) {
- left = mid + 1;
- } else {
- right = mid;
- }
- }
- /*********************************/
-
- // 没找到合适的牌堆,新建一堆
- if (left === piles) piles++;
-
- // 把这张牌放到牌堆顶
- top[left] = poker;
- }
-
- // 牌堆数就是 LIS 长度
- return piles;
-};
-
-/**
- * @param {number[][]} envelopes
- * @return {number}
- */
-var maxEnvelopes = function (envelopes) {
- let n = envelopes.length;
- // 按宽度升序排列,如果宽度一样,则按高度降序排列
- envelopes.sort((a, b) => {
- return a[0] === b[0] ? b[1] - a[1] : a[0] - b[0];
- })
-
- // 对高度数组寻找 LIS
- let height = new Array(n);
- for (let i = 0; i < n; i++)
- height[i] = envelopes[i][1];
-
- return lengthOfLIS(height);
-};
-
-```
-
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\207\240\344\270\252\345\217\215\347\233\264\350\247\211\347\232\204\346\246\202\347\216\207\351\227\256\351\242\230.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\207\240\344\270\252\345\217\215\347\233\264\350\247\211\347\232\204\346\246\202\347\216\207\351\227\256\351\242\230.md"
index 65d69711ff4708182b307ccd6e13fe7c5f653044..f8e389e4ad192a0adb02ff6a8de975e82ef3a112 100644
--- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\207\240\344\270\252\345\217\215\347\233\264\350\247\211\347\232\204\346\246\202\347\216\207\351\227\256\351\242\230.md"
+++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\207\240\344\270\252\345\217\215\347\233\264\350\247\211\347\232\204\346\246\202\347\216\207\351\227\256\351\242\230.md"
@@ -1,20 +1,21 @@
# 几个反直觉的概率问题
-
+
-
-![](../pictures/souyisou.png)
+![](https://labuladong.github.io/algo/images/souyisou1.png)
+
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
+
-**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
**-----------**
-上篇文章 [洗牌算法详解](https://labuladong.gitee.io/algo/) 讲到了验证概率算法的蒙特卡罗方法,今天聊点轻松的内容:几个和概率相关的有趣问题。
+上篇文章 [洗牌算法详解](https://labuladong.github.io/article/fname.html?fname=洗牌算法) 讲到了验证概率算法的蒙特卡罗方法,今天聊点轻松的内容:几个和概率相关的有趣问题。
计算概率有下面两个最简单的原则:
@@ -28,7 +29,6 @@
下面介绍几个简单却具有迷惑性的问题,分别是男孩女孩问题、生日悖论、三门问题。当然,三门问题可能是大家最耳熟的,所以就多说一些有趣的思考。
-
### 一、男孩女孩问题
假设有一个家庭,有两个孩子,现在告诉你其中有一个男孩,请问另一个也是男孩的概率是多少?
@@ -51,14 +51,13 @@
我竟然觉得有那么一丝道理!但其实,我们只是通过年龄差异来表示两个孩子的独立性,也就是说即便两个孩子同性,也有两种可能。所以不要用双胞胎抬杠了。
-
### 二、生日悖论
生日悖论是由这样一个问题引出的:一个屋子里需要有多少人,才能使得存在至少两个人生日是同一天的概率达到 50%?
答案是 23 个人,也就是说房子里如果有 23 个人,那么就有 50% 的概率会存在两个人生日相同。这个结论看起来不可思议,所以被称为悖论。按照直觉,要得到 50% 的概率,起码得有 183 个人吧,因为一年有 365 天呀?其实不是的,觉得这个结论不可思议主要有两个思维误区:
-**第一个误区是误解「存在」这个词的含义。**
+**第一个误区是误解「存在」这个词的含义**。
读者可能认为,如果 23 个人中出现相同生日的概率就能达到 50%,是不是意味着:
@@ -72,7 +71,7 @@
这样计算得到的结果是不是看起来合理多了?生日悖论计算对象的不是某一个人,而是一个整体,其中包含了所有人的排列组合,它们的概率之和当然会大得多。
-**第二个误区是认为概率是线性变化的。**
+**第二个误区是认为概率是线性变化的**。
读者可能认为,如果 23 个人中出现相同生日的概率就能达到 50%,是不是意味着 46 个人的概率就能达到 100%?
@@ -84,8 +83,7 @@
那为什么只要 23 个人出现相同生日的概率就能大于 50% 了呢?我们先计算 23 个人生日都唯一(不重复)的概率。只有 1 个人的时候,生日唯一的概率是 `365/365`,2 个人时,生日唯一的概率是 `365/365 × 364/365`,以此类推可知 23 人的生日都唯一的概率:
-
-![](../pictures/概率问题/p.png)
+![](https://labuladong.github.io/algo/images/概率问题/p.png)
算出来大约是 0.493,所以存在相同生日的概率就是 0.507,差不多就是 50% 了。实际上,按照这个算法,当人数达到 70 时,存在两个人生日相同的概率就上升到了 99.9%,基本可以认为是 100% 了。所以从概率上说,一个几十人的小团体中存在生日相同的人真没啥稀奇的。
@@ -97,13 +95,13 @@
你是游戏参与者,现在有门 1,2,3,假设你随机选择了门 1,然后主持人打开了门 3 告诉你那后面是山羊。现在,你是坚持你最初的选择门 1,还是选择换成门 2 呢?
-![](../pictures/概率问题/sanmen.png)
+![](https://labuladong.github.io/algo/images/概率问题/sanmen.png)
答案是应该换门,换门之后抽到跑车的概率是 2/3,不换的话是 1/3。又一次反直觉,感觉换不换的中奖概率应该都一样啊,因为最后肯定就剩两个门,一个是羊,一个是跑车,这是事实,所以不管选哪个的概率不都是 1/2 吗?
类似前面说的男孩女孩问题,最简单稳妥的方法就是把所有可能结果穷举出来:
-![穷举树](../pictures/概率问题/tree.png)
+![](https://labuladong.github.io/algo/images/概率问题/tree.png)
很容易看到选择换门中奖的概率是 2/3,不换的话是 1/3。
@@ -129,14 +127,15 @@
当然,运用此策略蒙题的前提是你真的抓瞎,真的随机乱选答案,这样概率才能作为最后的杀手锏。
+
+
+
+
**_____________**
-**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**。
+**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
-**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。
+![](https://labuladong.github.io/algo/images/souyisou2.png)
-
-
-
======其他语言代码======
\ No newline at end of file
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\211\215\347\274\200\345\222\214\346\212\200\345\267\247.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\211\215\347\274\200\345\222\214\346\212\200\345\267\247.md"
index 73260dd9707b4611140d55bb437948c1cc9a65bf..d41a3f0d5fe72e2ce4ac29f0d257049684ca6001 100644
--- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\211\215\347\274\200\345\222\214\346\212\200\345\267\247.md"
+++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\211\215\347\274\200\345\222\214\346\212\200\345\267\247.md"
@@ -1,11 +1,5 @@
# 经典数组技巧:前缀和数组
-
-
-
-
-
-
@@ -15,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -183,6 +177,56 @@ class NumMatrix {
除了本文举例的基本用法,前缀和数组经常和其他数据结构或算法技巧相结合,我会在 [前缀和技巧高频习题](https://appktavsiei5995.pc.xiaoe-tech.com/detail/i_627cd61de4b0cedf38b0f3a0/1) 中举例讲解。
+
+
+
+
+引用本文的文章
+
+ - [二维数组的花式遍历技巧](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=算法心得)
+ - [经典数组技巧:差分数组](https://appktavsiei5995.pc.xiaoe-tech.com/detail/i_629e0d3ae4b0812e17a32f01/1)
+
+
+
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| [1314. Matrix Block Sum](https://leetcode.com/problems/matrix-block-sum/?show=1) | [1314. 矩阵区域和](https://leetcode.cn/problems/matrix-block-sum/?show=1) |
+| [1352. Product of the Last K Numbers](https://leetcode.com/problems/product-of-the-last-k-numbers/?show=1) | [1352. 最后 K 个数的乘积](https://leetcode.cn/problems/product-of-the-last-k-numbers/?show=1) |
+| [238. Product of Array Except Self](https://leetcode.com/problems/product-of-array-except-self/?show=1) | [238. 除自身以外数组的乘积](https://leetcode.cn/problems/product-of-array-except-self/?show=1) |
+| [327. Count of Range Sum](https://leetcode.com/problems/count-of-range-sum/?show=1) | [327. 区间和的个数](https://leetcode.cn/problems/count-of-range-sum/?show=1) |
+| [437. Path Sum III](https://leetcode.com/problems/path-sum-iii/?show=1) | [437. 路径总和 III](https://leetcode.cn/problems/path-sum-iii/?show=1) |
+| [523. Continuous Subarray Sum](https://leetcode.com/problems/continuous-subarray-sum/?show=1) | [523. 连续的子数组和](https://leetcode.cn/problems/continuous-subarray-sum/?show=1) |
+| [525. Contiguous Array](https://leetcode.com/problems/contiguous-array/?show=1) | [525. 连续数组](https://leetcode.cn/problems/contiguous-array/?show=1) |
+| [560. Subarray Sum Equals K](https://leetcode.com/problems/subarray-sum-equals-k/?show=1) | [560. 和为 K 的子数组](https://leetcode.cn/problems/subarray-sum-equals-k/?show=1) |
+| [724. Find Pivot Index](https://leetcode.com/problems/find-pivot-index/?show=1) | [724. 寻找数组的中心下标](https://leetcode.cn/problems/find-pivot-index/?show=1) |
+| [862. Shortest Subarray with Sum at Least K](https://leetcode.com/problems/shortest-subarray-with-sum-at-least-k/?show=1) | [862. 和至少为 K 的最短子数组](https://leetcode.cn/problems/shortest-subarray-with-sum-at-least-k/?show=1) |
+| [918. Maximum Sum Circular Subarray](https://leetcode.com/problems/maximum-sum-circular-subarray/?show=1) | [918. 环形子数组的最大和](https://leetcode.cn/problems/maximum-sum-circular-subarray/?show=1) |
+| [974. Subarray Sums Divisible by K](https://leetcode.com/problems/subarray-sums-divisible-by-k/?show=1) | [974. 和可被 K 整除的子数组](https://leetcode.cn/problems/subarray-sums-divisible-by-k/?show=1) |
+| - | [剑指 Offer 57 - II. 和为s的连续正数序列](https://leetcode.cn/problems/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof/?show=1) |
+| - | [剑指 Offer II 010. 和为 k 的子数组](https://leetcode.cn/problems/QTMn0o/?show=1) |
+| - | [剑指 Offer II 011. 0 和 1 个数相同的子数组](https://leetcode.cn/problems/A1NYOS/?show=1) |
+| - | [剑指 Offer II 012. 左右两边子数组的和相等](https://leetcode.cn/problems/tvdfij/?show=1) |
+| - | [剑指 Offer II 013. 二维子矩阵的和](https://leetcode.cn/problems/O4NDxx/?show=1) |
+| - | [剑指 Offer II 050. 向下的路径节点之和](https://leetcode.cn/problems/6eUYwP/?show=1) |
+
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\214\272\351\227\264\344\272\244\351\233\206\351\227\256\351\242\230.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\214\272\351\227\264\344\272\244\351\233\206\351\227\256\351\242\230.md"
deleted file mode 100644
index aea8b65d895429cd9e1c4c1e23a5d147444e70db..0000000000000000000000000000000000000000
--- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\214\272\351\227\264\344\272\244\351\233\206\351\227\256\351\242\230.md"
+++ /dev/null
@@ -1,206 +0,0 @@
-# 区间交集问题
-
-
-
-
-
-
-
-
-
-![](../pictures/souyisou.png)
-
-**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
-
-读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
-
-[986.区间列表的交集](https://leetcode-cn.com/problems/interval-list-intersections)
-
-**-----------**
-
-本文是区间系列问题的第三篇,前两篇分别讲了区间的最大不相交子集和重叠区间的合并,今天再写一个算法,可以快速找出两组区间的交集。
-
-先看下题目,LeetCode 第 986 题就是这个问题:
-
-![title](../pictures/intersection/title.png)
-
-题目很好理解,就是让你找交集,注意区间都是闭区间。
-
-### 思路
-
-解决区间问题的思路一般是先排序,以便操作,不过题目说已经排好序了,那么可以用两个索引指针在 `A` 和 `B` 中游走,把交集找出来,代码大概是这样的:
-
-```python
-# A, B 形如 [[0,2],[5,10]...]
-def intervalIntersection(A, B):
- i, j = 0, 0
- res = []
- while i < len(A) and j < len(B):
- # ...
- j += 1
- i += 1
- return res
-```
-
-不难,我们先老老实实分析一下各种情况。
-
-首先,**对于两个区间**,我们用 `[a1,a2]` 和 `[b1,b2]` 表示在 `A` 和 `B` 中的两个区间,那么什么情况下这两个区间**没有交集**呢:
-
-![](../pictures/intersection/1.jpg)
-
-只有这两种情况,写成代码的条件判断就是这样:
-
-```python
-if b2 < a1 or a2 < b1:
- [a1,a2] 和 [b1,b2] 无交集
-```
-
-那么,什么情况下,两个区间存在交集呢?根据命题的否定,上面逻辑的否命题就是存在交集的条件:
-
-```python
-# 不等号取反,or 也要变成 and
-if b2 >= a1 and a2 >= b1:
- [a1,a2] 和 [b1,b2] 存在交集
-```
-
-接下来,两个区间存在交集的情况有哪些呢?穷举出来:
-
-![](../pictures/intersection/2.jpg)
-
-这很简单吧,就这四种情况而已。那么接下来思考,这几种情况下,交集是否有什么共同点呢?
-
-![](../pictures/intersection/3.jpg)
-
-我们惊奇地发现,交集区间是有规律的!如果交集区间是 `[c1,c2]`,那么 `c1=max(a1,b1)`,`c2=min(a2,b2)`!这一点就是寻找交集的核心,我们把代码更进一步:
-
-```python
-while i < len(A) and j < len(B):
- a1, a2 = A[i][0], A[i][1]
- b1, b2 = B[j][0], B[j][1]
- if b2 >= a1 and a2 >= b1:
- res.append([max(a1, b1), min(a2, b2)])
- # ...
-```
-
-最后一步,我们的指针 `i` 和 `j` 肯定要前进(递增)的,什么时候应该前进呢?
-
-![](../pictures/intersection/4.gif)
-
-结合动画示例就很好理解了,是否前进,只取决于 `a2` 和 `b2` 的大小关系:
-
-```python
-while i < len(A) and j < len(B):
- # ...
- if b2 < a2:
- j += 1
- else:
- i += 1
-```
-
-### 代码
-
-```python
-# A, B 形如 [[0,2],[5,10]...]
-def intervalIntersection(A, B):
- i, j = 0, 0 # 双指针
- res = []
- while i < len(A) and j < len(B):
- a1, a2 = A[i][0], A[i][1]
- b1, b2 = B[j][0], B[j][1]
- # 两个区间存在交集
- if b2 >= a1 and a2 >= b1:
- # 计算出交集,加入 res
- res.append([max(a1, b1), min(a2, b2)])
- # 指针前进
- if b2 < a2: j += 1
- else: i += 1
- return res
-```
-
-总结一下,区间类问题看起来都比较复杂,情况很多难以处理,但实际上通过观察各种不同情况之间的共性可以发现规律,用简洁的代码就能处理。
-
-另外,区间问题没啥特别厉害的奇技淫巧,其操作也朴实无华,但其应用却十分广泛。
-
-**_____________**
-
-**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**。
-
-**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。
-
-
-
-
-======其他语言代码======
-
-[986.区间列表的交集](https://leetcode-cn.com/problems/interval-list-intersections)
-
-
-
-### java
-
-[KiraZh](https://github.com/KiraZh)提供第986题Java代码
-
-```java
-class Solution {
- public int[][] intervalIntersection(int[][] A, int[][] B) {
- List res = new ArrayList<>();
- int a = 0, b = 0;
- while(a < A.length && b < B.length) {
- // 确定左边界,两个区间左边界的最大值
- int left = Math.max(A[a][0], B[b][0]);
- // 确定右边界,两个区间右边界的最小值
- int right = Math.min(A[a][1], B[b][1]);
- // 左边界小于右边界则加入结果集
- if (left <= right)
- res.add(new int[] {left, right});
- // 右边界更大的保持不动,另一个指针移动,继续比较
- if(A[a][1] < B[b][1]) a++;
- else b++;
- }
- // 将结果转为数组
- return res.toArray(new int[0][]);
- }
-}
-```
-
-
-
-### javascript
-
-[986.区间列表的交集](https://leetcode-cn.com/problems/interval-list-intersections)
-
-```js
-/**
- * @param {number[][]} firstList
- * @param {number[][]} secondList
- * @return {number[][]}
- */
-var intervalIntersection = function (firstList, secondList) {
- let i, j;
- i = j = 0;
-
- let res = [];
-
- while (i < firstList.length && j < secondList.length) {
- let a1 = firstList[i][0];
- let a2 = firstList[i][1];
- let b1 = secondList[j][0];
- let b2 = secondList[j][1];
-
- // 两个区间存在交集
- if (b2 >= a1 && a2 >= b1) {
- // 计算出交集,加入 res
- res.push([Math.max(a1, b1), Math.min(a2, b2)])
- }
-
- // 指针前进
- if (b2 < a2) {
- j += 1;
- } else {
- i += 1
- }
- }
- return res;
-};
-```
-
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\214\272\351\227\264\350\260\203\345\272\246\351\227\256\351\242\230\344\271\213\345\214\272\351\227\264\345\220\210\345\271\266.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\214\272\351\227\264\350\260\203\345\272\246\351\227\256\351\242\230\344\271\213\345\214\272\351\227\264\345\220\210\345\271\266.md"
deleted file mode 100644
index 2413e1d8ef2d2aafac5ef267efe88bdbd867f35e..0000000000000000000000000000000000000000
--- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\214\272\351\227\264\350\260\203\345\272\246\351\227\256\351\242\230\344\271\213\345\214\272\351\227\264\345\220\210\345\271\266.md"
+++ /dev/null
@@ -1,237 +0,0 @@
-# 区间调度问题之区间合并
-
-
-
-
-
-
-
-
-
-![](../pictures/souyisou.png)
-
-**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
-
-读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
-
-[56.合并区间](https://leetcode-cn.com/problems/merge-intervals)
-
-**-----------**
-
-上篇文章用贪心算法解决了区间调度问题:给你很多区间,让你求其中的最大不重叠子集。
-
-其实对于区间相关的问题,还有很多其他类型,本文就来讲讲区间合并问题(Merge Interval)。
-
-LeetCode 第 56 题就是一道相关问题,题目很好理解:
-
-![title](../pictures/mergeInterval/title.png)
-
-我们解决区间问题的一般思路是先排序,然后观察规律。
-
-### 一、思路
-
-一个区间可以表示为 `[start, end]`,前文聊的区间调度问题,需要按 `end` 排序,以便满足贪心选择性质。而对于区间合并问题,其实按 `end` 和 `start` 排序都可以,不过为了清晰起见,我们选择按 `start` 排序。
-
-![1](../pictures/mergeInterval/1.jpg)
-
-**显然,对于几个相交区间合并后的结果区间 `x`,`x.start` 一定是这些相交区间中 `start` 最小的,`x.end` 一定是这些相交区间中 `end` 最大的。**
-
-![2](../pictures/mergeInterval/2.jpg)
-
-由于已经排了序,`x.start` 很好确定,求 `x.end` 也很容易,可以类比在数组中找最大值的过程:
-
-```java
-int max_ele = arr[0];
-for (int i = 1; i < arr.length; i++)
- max_ele = max(max_ele, arr[i]);
-return max_ele;
-```
-
-### 二、代码
-
-```python
-# intervals 形如 [[1,3],[2,6]...]
-def merge(intervals):
- if not intervals: return []
- # 按区间的 start 升序排列
- intervals.sort(key=lambda intv: intv[0])
- res = []
- res.append(intervals[0])
-
- for i in range(1, len(intervals)):
- curr = intervals[i]
- # res 中最后一个元素的引用
- last = res[-1]
- if curr[0] <= last[1]:
- # 找到最大的 end
- last[1] = max(last[1], curr[1])
- else:
- # 处理下一个待合并区间
- res.append(curr)
- return res
-```
-
-看下动画就一目了然了:
-
-![3](../pictures/mergeInterval/3.gif)
-
-至此,区间合并问题就解决了。本文篇幅短小,因为区间合并只是区间问题的一个类型,后续还有一些区间问题。本想把所有问题类型都总结在一篇文章,但有读者反应,长文只会收藏不会看... 所以还是分成小短文吧,读者有什么看法可以在留言板留言交流。
-
-本文终,希望对你有帮助。
-
-**_____________**
-
-**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**。
-
-**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。
-
-
-
-
-======其他语言代码======
-
-[56.合并区间](https://leetcode-cn.com/problems/merge-intervals)
-
-
-
-### java
-
-```java
-class Solution {
- /**
- * 1. 先对区间集合进行排序(根据开始位置)
- * 2. 合并的情况一共有三种
- * a. b. c.
- * |---------| |--------| |--------|
- * |---------| |--| |--------|
- * a和b两种情况,合并取右边界大的值,c情况不合并
- *
- */
-
- private int[][] tmp;
-
- public int[][] merge(int[][] intervals) {
- if(intervals == null ||intervals.length == 0)return new int[0][0];
- int length = intervals.length;
- //将列表中的区间按照左端点升序排序
- // Arrays.sort(intervals,(v1,v2) -> v1[0]-v2[0]);
-
- this.tmp = new int[length][2];
- sort(intervals,0,length-1);
-
- int[][] ans = new int[length][2];
- int index = -1;
- for(int[] interval:intervals){
- // 当结果数组是空是,或者当前区间的起始位置 > 结果数组中最后区间的终止位置(即上图情况c);
- // 则不合并,直接将当前区间加入结果数组。
- if(index == -1 || interval[0] > ans[index][1]){
- ans[++index] = interval;
- }else{
- // 反之将当前区间合并至结果数组的最后区间(即上图情况a,b)
- ans[index][1] = Math.max(ans[index][1],interval[1]);
- }
- }
- return Arrays.copyOf(ans, index + 1);
- }
-
- //归并排序
- public void sort(int[][] intervals,int l,int r){
- if(l >= r)return;
-
- int mid = l + (r-l)/2;
- sort(intervals,l,mid);
- sort(intervals,mid+1,r);
-
- //合并
- int i=l,j=mid+1;
- for(int k=l;k<=r;k++){
- if(i>mid)tmp[k]=intervals[j++];
- else if(j>r)tmp[k]=intervals[i++];
- else if(intervals[i][0]>intervals[j][0])tmp[k] = intervals[j++];
- else tmp[k] = intervals[i++];
- }
-
- System.arraycopy(tmp,l,intervals,l,r-l+1);
- }
-
-}
-```
-
-### c++
-
-[Kian](https://github.com/KianKw/) 提供第 56 题 C++ 代码
-
-```c++
-class Solution {
-public:
- vector> merge(vector>& intervals) {
- // len 为 intervals 的长度
- int len = intervals.size();
- if (len < 1)
- return {};
-
- // 按区间的 start 升序排列
- sort(intervals.begin(), intervals.end());
-
- // 初始化 res 数组
- vector> res;
- res.push_back(intervals[0]);
-
- for (int i = 1; i < len; i++) {
- vector curr = intervals[i];
- // res.back() 为 res 中最后一个元素的索引
- if (curr[0] <= res.back()[1]) {
- // 找到最大的 end
- res.back()[1] = max(res.back()[1], curr[1]);
- } else {
- // 处理下一个待合并区间
- res.push_back(curr);
- }
- }
- return res;
- }
-};
-```
-
-
-
-### javascript
-
-[56. 合并区间](https://leetcode-cn.com/problems/merge-intervals/)
-
-```js
-/**
- * @param {number[][]} intervals
- * @return {number[][]}
- */
-var merge = function (intervals) {
- if (intervals.length < 1) {
- return []
- }
-
- // 按区间的 start 升序排列
- intervals.sort((a, b) => {
- return a[0] - b[0]
- })
-
- const res = []
- res.push(intervals[0].concat())
-
- for (let i = 1; i < intervals.length; i++) {
-
- let curr = intervals[i]
- // res 中最后一个元素的引用
- let last = res[res.length - 1]
-
- if (curr[0] <= last[1]) {
- // 找到最大的 end
- last[1] = Math.max(last[1], curr[1])
- } else {
- // 处理下一个待合并区间
- res.push(curr.concat())
- }
- }
- return res
-}
-```
-
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\217\214\346\214\207\351\222\210\346\212\200\345\267\247.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\217\214\346\214\207\351\222\210\346\212\200\345\267\247.md"
index 513cdf61a6976227b5e6f9e5632aff3d7dd8745c..3c1446aeb75348bb58872ba721660272ef529b62 100644
--- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\217\214\346\214\207\351\222\210\346\212\200\345\267\247.md"
+++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\217\214\346\214\207\351\222\210\346\212\200\345\267\247.md"
@@ -1,11 +1,5 @@
# 数组双指针技巧汇总
-
-
-
-
-
-
@@ -15,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -385,6 +379,49 @@ String longestPalindrome(String s) {
到这里,数组相关的双指针技巧就全部讲完了,这些技巧的更多扩展延伸见 [更多双指针经典高频题](https://appktavsiei5995.pc.xiaoe-tech.com/detail/i_62a1dd68e4b09dda1273a5f9/1)。
+
+
+
+
+引用本文的文章
+
+ - [一个方法团灭 nSum 问题](https://labuladong.github.io/article/fname.html?fname=nSum)
+ - [分治算法详解:运算优先级](https://labuladong.github.io/article/fname.html?fname=分治算法)
+ - [动态规划之子序列问题解题模板](https://labuladong.github.io/article/fname.html?fname=子序列问题模板)
+ - [双指针更多经典题目](https://appktavsiei5995.pc.xiaoe-tech.com/detail/i_62a1dd68e4b09dda1273a5f9/1)
+ - [如何判断回文链表](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=时间复杂度)
+ - [算法笔试「骗分」套路](https://labuladong.github.io/article/fname.html?fname=刷题技巧)
+
+
+
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| [1. Two Sum](https://leetcode.com/problems/two-sum/?show=1) | [1. 两数之和](https://leetcode.cn/problems/two-sum/?show=1) |
+| [281. Zigzag Iterator](https://leetcode.com/problems/zigzag-iterator/?show=1)🔒 | [281. 锯齿迭代器](https://leetcode.cn/problems/zigzag-iterator/?show=1)🔒 |
+| [42. Trapping Rain Water](https://leetcode.com/problems/trapping-rain-water/?show=1) | [42. 接雨水](https://leetcode.cn/problems/trapping-rain-water/?show=1) |
+| [80. Remove Duplicates from Sorted Array II](https://leetcode.com/problems/remove-duplicates-from-sorted-array-ii/?show=1) | [80. 删除有序数组中的重复项 II](https://leetcode.cn/problems/remove-duplicates-from-sorted-array-ii/?show=1) |
+| [82. Remove Duplicates from Sorted List II](https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/?show=1) | [82. 删除排序链表中的重复元素 II](https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/?show=1) |
+| - | [剑指 Offer 21. 调整数组顺序使奇数位于偶数前面](https://leetcode.cn/problems/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof/?show=1) |
+| - | [剑指 Offer 57. 和为s的两个数字](https://leetcode.cn/problems/he-wei-sde-liang-ge-shu-zi-lcof/?show=1) |
+
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\233\236\346\272\257\347\256\227\346\263\225\350\257\246\350\247\243\344\277\256\350\256\242\347\211\210.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\233\236\346\272\257\347\256\227\346\263\225\350\257\246\350\247\243\344\277\256\350\256\242\347\211\210.md"
index 001d31d84c4fc764c4abb1cf5c11867065e85ec4..67e7e26068e07adebe291f419e7c3776b782a9f8 100644
--- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\233\236\346\272\257\347\256\227\346\263\225\350\257\246\350\247\243\344\277\256\350\256\242\347\211\210.md"
+++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\233\236\346\272\257\347\256\227\346\263\225\350\257\246\350\247\243\344\277\256\350\256\242\347\211\210.md"
@@ -1,9 +1,5 @@
# 回溯算法详解
-
-
-
-
@@ -13,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -333,6 +329,67 @@ def backtrack(...):
动态规划和回溯算法底层都把问题抽象成了树的结构,但这两种算法在思路上是完全不同的。在 [东哥带你刷二叉树(纲领篇)](https://labuladong.github.io/article/fname.html?fname=二叉树总结) 你将看到动态规划和回溯算法更深层次的区别和联系。
+
+
+
+
+引用本文的文章
+
+ - [BFS 算法解题套路框架](https://labuladong.github.io/article/fname.html?fname=BFS框架)
+ - [Dijkstra 算法模板及应用](https://labuladong.github.io/article/fname.html?fname=dijkstra算法)
+ - [FloodFill算法详解及应用](https://labuladong.github.io/article/fname.html?fname=FloodFill算法详解及应用)
+ - [base case 和备忘录的初始值怎么定?](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=trie)
+ - [动态规划和回溯算法到底谁是谁爹?](https://labuladong.github.io/article/fname.html?fname=targetSum)
+ - [回溯算法最佳实践:括号生成](https://labuladong.github.io/article/fname.html?fname=合法括号生成)
+ - [回溯算法最佳实践:解数独](https://labuladong.github.io/article/fname.html?fname=sudoku)
+ - [回溯算法秒杀所有排列/组合/子集问题](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=心流)
+ - [算法笔试「骗分」套路](https://labuladong.github.io/article/fname.html?fname=刷题技巧)
+ - [经典动态规划:戳气球](https://labuladong.github.io/article/fname.html?fname=扎气球)
+ - [经典回溯算法:集合划分问题](https://labuladong.github.io/article/fname.html?fname=集合划分)
+
+
+
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| [112. Path Sum](https://leetcode.com/problems/path-sum/?show=1) | [112. 路径总和](https://leetcode.cn/problems/path-sum/?show=1) |
+| [113. Path Sum II](https://leetcode.com/problems/path-sum-ii/?show=1) | [113. 路径总和 II](https://leetcode.cn/problems/path-sum-ii/?show=1) |
+| [140. Word Break II](https://leetcode.com/problems/word-break-ii/?show=1) | [140. 单词拆分 II](https://leetcode.cn/problems/word-break-ii/?show=1) |
+| [17. Letter Combinations of a Phone Number](https://leetcode.com/problems/letter-combinations-of-a-phone-number/?show=1) | [17. 电话号码的字母组合](https://leetcode.cn/problems/letter-combinations-of-a-phone-number/?show=1) |
+| [22. Generate Parentheses](https://leetcode.com/problems/generate-parentheses/?show=1) | [22. 括号生成](https://leetcode.cn/problems/generate-parentheses/?show=1) |
+| [39. Combination Sum](https://leetcode.com/problems/combination-sum/?show=1) | [39. 组合总和](https://leetcode.cn/problems/combination-sum/?show=1) |
+| [698. Partition to K Equal Sum Subsets](https://leetcode.com/problems/partition-to-k-equal-sum-subsets/?show=1) | [698. 划分为k个相等的子集](https://leetcode.cn/problems/partition-to-k-equal-sum-subsets/?show=1) |
+| [77. Combinations](https://leetcode.com/problems/combinations/?show=1) | [77. 组合](https://leetcode.cn/problems/combinations/?show=1) |
+| [78. Subsets](https://leetcode.com/problems/subsets/?show=1) | [78. 子集](https://leetcode.cn/problems/subsets/?show=1) |
+| - | [剑指 Offer 34. 二叉树中和为某一值的路径](https://leetcode.cn/problems/er-cha-shu-zhong-he-wei-mou-yi-zhi-de-lu-jing-lcof/?show=1) |
+| - | [剑指 Offer II 079. 所有子集](https://leetcode.cn/problems/TVdhkn/?show=1) |
+| - | [剑指 Offer II 080. 含有 k 个元素的组合](https://leetcode.cn/problems/uUsW3B/?show=1) |
+| - | [剑指 Offer II 081. 允许重复选择元素的组合](https://leetcode.cn/problems/Ygoe9J/?show=1) |
+| - | [剑指 Offer II 083. 没有重复元素集合的全排列](https://leetcode.cn/problems/VvJkup/?show=1) |
+| - | [剑指 Offer II 085. 生成匹配的括号](https://leetcode.cn/problems/IDBivT/?show=1) |
+
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225.md"
index a1b49ead5132ce74178217d5778e7c1a6c7ff780..7fda86f850b7d4c1873d48d70010b8ffb132be34 100644
--- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225.md"
+++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225.md"
@@ -1,9 +1,5 @@
# 字符串乘法
-
-
-
-
@@ -13,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -98,6 +94,10 @@ string multiply(string num1, string num2) {
也许算法就是一种**寻找思维定式的思维**吧,希望本文对你有帮助。
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\246\344\271\240\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225\347\232\204\351\253\230\346\225\210\346\226\271\346\263\225.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\246\344\271\240\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225\347\232\204\351\253\230\346\225\210\346\226\271\346\263\225.md"
index 034a123ede109a62aa5f33618426777e8b03e5d5..33e458980bf02740728347e433921b632b6c9de1 100644
--- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\246\344\271\240\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225\347\232\204\351\253\230\346\225\210\346\226\271\346\263\225.md"
+++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\246\344\271\240\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225\347\232\204\351\253\230\346\225\210\346\226\271\346\263\225.md"
@@ -1,22 +1,23 @@
# 学习算法和刷题的思路指南
-
+
-
-![](../pictures/souyisou.png)
+![](https://labuladong.github.io/algo/images/souyisou1.png)
+
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
+
-**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
**-----------**
-通知:如果本站对你学习算法有帮助,**请收藏网址,并推荐给你的朋友**。由于 **labuladong** 的算法套路太火,很多人直接拿我的 GitHub 文章去开付费专栏,价格还不便宜。我这免费写给你看,**多宣传原创作者是你唯一能做的**,谁也不希望劣币驱逐良币对吧?
+> 本文有视频版:[学习数据结构和算法的框架思维](https://www.bilibili.com/video/BV1EN4y1M79p/)
-这是好久之前的一篇文章「学习数据结构和算法的框架思维」的修订版。之前那篇文章收到广泛好评,没看过也没关系,这篇文章会涵盖之前的所有内容,并且会举很多代码的实例,教你如何使用框架思维。
+这是好久之前的一篇文章 [学习数据结构和算法的框架思维](https://mp.weixin.qq.com/s/gE-5KMi4bBvJovdsQXIKgw) 的修订版。之前那篇文章收到广泛好评,没看过也没关系,这篇文章会涵盖之前的所有内容,并且会举很多代码的实例,教你如何使用框架思维。
首先,这里讲的都是普通的数据结构,咱不是搞算法竞赛的,野路子出生,我只会解决常规的问题。另外,以下是我个人的经验的总结,没有哪本算法书会写这些东西,所以请读者试着理解我的角度,别纠结于细节问题,因为这篇文章就是希望对数据结构和算法建立一个框架性的认识。
@@ -46,7 +47,6 @@
**链表**因为元素不连续,而是靠指针指向下一个元素的位置,所以不存在数组的扩容问题;如果知道某一元素的前驱和后驱,操作指针即可删除该元素或者插入新元素,时间复杂度 O(1)。但是正因为存储空间不连续,你无法根据一个索引算出对应元素的地址,所以不能随机访问;而且由于每个元素必须存储指向前后元素位置的指针,会消耗相对更多的储存空间。
-
### 二、数据结构的基本操作
对于任何数据结构,其基本操作无非遍历 + 访问,再具体一点就是:增删查改。
@@ -84,7 +84,7 @@ void traverse(ListNode head) {
void traverse(ListNode head) {
// 递归访问 head.val
- traverse(head.next)
+ traverse(head.next);
}
```
@@ -98,8 +98,8 @@ class TreeNode {
}
void traverse(TreeNode root) {
- traverse(root.left)
- traverse(root.right)
+ traverse(root.left);
+ traverse(root.right);
}
```
@@ -120,134 +120,164 @@ void traverse(TreeNode root) {
}
```
-N 叉树的遍历又可以扩展为图的遍历,因为图就是好几 N 叉棵树的结合体。你说图是可能出现环的?这个很好办,用个布尔数组 visited 做标记就行了,这里就不写代码了。
+`N` 叉树的遍历又可以扩展为图的遍历,因为图就是好几 `N` 叉棵树的结合体。你说图是可能出现环的?这个很好办,用个布尔数组 `visited` 做标记就行了,这里就不写代码了。
-**所谓框架,就是套路。不管增删查改,这些代码都是永远无法脱离的结构,你可以把这个结构作为大纲,根据具体问题在框架上添加代码就行了,下面会具体举例**。
+**所谓框架,就是套路。不管增删查改,这些代码都是永远无法脱离的结构**,你可以把这个结构作为大纲,根据具体问题在框架上添加代码就行了,下面会具体举例。
### 三、算法刷题指南
-首先要明确的是,**数据结构是工具,算法是通过合适的工具解决特定问题的方法**。也就是说,学习算法之前,最起码得了解那些常用的数据结构,了解它们的特性和缺陷。
+首先要明确的是,数据结构是工具,算法是通过合适的工具解决特定问题的方法。也就是说,学习算法之前,最起码得了解那些常用的数据结构,了解它们的特性和缺陷。
+
+所以我建议的刷题顺序是:
+
+**1、先学习像数组、链表这种基本数据结构的常用算法**,比如单链表翻转,前缀和数组,二分搜索等。
-那么该如何在 LeetCode 刷题呢?之前的文章[算法学习之路](https://labuladong.gitee.io/algo/)写过一些,什么按标签刷,坚持下去云云。现在距那篇文章已经过去将近一年了,我不说那些不痛不痒的话,直接说具体的建议:
+因为这些算法属于会者不难难者不会的类型,难度不大,学习它们不会花费太多时间。而且这些小而美的算法经常让你大呼精妙,能够有效培养你对算法的兴趣。
-**先刷二叉树,先刷二叉树,先刷二叉树**!
+**2、学会基础算法之后,不要急着上来就刷回溯算法、动态规划这类笔试常考题,而应该先刷二叉树,先刷二叉树,先刷二叉树**,重要的事情说三遍。
-这是我这刷题一年的亲身体会,下图是去年十月份的提交截图:
+这是我这刷题多年的亲身体会,下图是我刚开始学算法的提交截图:
-![](../pictures/others/leetcode.jpeg)
+![](https://labuladong.github.io/algo/images/others/leetcode.jpeg)
公众号文章的阅读数据显示,大部分人对数据结构相关的算法文章不感兴趣,而是更关心动规回溯分治等等技巧。为什么要先刷二叉树呢,**因为二叉树是最容易培养框架思维的,而且大部分算法技巧,本质上都是树的遍历问题**。
-刷二叉树看到题目没思路?根据很多读者的问题,其实大家不是没思路,只是没有理解我们说的「框架」是什么。**不要小看这几行破代码,几乎所有二叉树的题目都是一套这个框架就出来了**。
+刷二叉树看到题目没思路?根据很多读者的问题,其实大家不是没思路,只是没有理解我们说的「框架」是什么。
+
+**不要小看这几行破代码,几乎所有二叉树的题目都是一套这个框架就出来了**:
```java
void traverse(TreeNode root) {
- // 前序遍历
- traverse(root.left)
- // 中序遍历
- traverse(root.right)
- // 后序遍历
+ // 前序位置
+ traverse(root.left);
+ // 中序位置
+ traverse(root.right);
+ // 后序位置
}
```
比如说我随便拿几道题的解法出来,不用管具体的代码逻辑,只要看看框架在其中是如何发挥作用的就行。
-LeetCode 124 题,难度 Hard,让你求二叉树中最大路径和,主要代码如下:
+力扣第 124 题,难度困难,让你求二叉树中最大路径和,主要代码如下:
-```cpp
-int ans = INT_MIN;
-int oneSideMax(TreeNode* root) {
- if (root == nullptr) return 0;
- int left = max(0, oneSideMax(root->left));
- int right = max(0, oneSideMax(root->right));
- ans = max(ans, left + right + root->val);
- return max(left, right) + root->val;
+```java
+int res = Integer.MIN_VALUE;
+int oneSideMax(TreeNode root) {
+ if (root == null) return 0;
+ int left = max(0, oneSideMax(root.left));
+ int right = max(0, oneSideMax(root.right));
+ // 后序位置
+ res = Math.max(res, left + right + root.val);
+ return Math.max(left, right) + root.val;
}
```
-你看,这就是个后序遍历嘛。
+注意递归函数的位置,这就是个后序遍历嘛,无非就是把 `traverse` 函数名字改成 `oneSideMax` 了。
-LeetCode 105 题,难度 Medium,让你根据前序遍历和中序遍历的结果还原一棵二叉树,很经典的问题吧,主要代码如下:
+力扣第 105 题,难度中等,让你根据前序遍历和中序遍历的结果还原一棵二叉树,很经典的问题吧,主要代码如下:
```java
-TreeNode buildTree(int[] preorder, int preStart, int preEnd,
- int[] inorder, int inStart, int inEnd, Map inMap) {
-
- if(preStart > preEnd || inStart > inEnd) return null;
-
- TreeNode root = new TreeNode(preorder[preStart]);
- int inRoot = inMap.get(root.val);
- int numsLeft = inRoot - inStart;
-
- root.left = buildTree(preorder, preStart + 1, preStart + numsLeft,
- inorder, inStart, inRoot - 1, inMap);
- root.right = buildTree(preorder, preStart + numsLeft + 1, preEnd,
- inorder, inRoot + 1, inEnd, inMap);
+TreeNode build(int[] preorder, int preStart, int preEnd,
+ int[] inorder, int inStart, int inEnd) {
+ // 前序位置,寻找左右子树的索引
+ if (preStart > preEnd) {
+ return null;
+ }
+ int rootVal = preorder[preStart];
+ int index = 0;
+ for (int i = inStart; i <= inEnd; i++) {
+ if (inorder[i] == rootVal) {
+ index = i;
+ break;
+ }
+ }
+ int leftSize = index - inStart;
+ TreeNode root = new TreeNode(rootVal);
+
+ // 递归构造左右子树
+ root.left = build(preorder, preStart + 1, preStart + leftSize,
+ inorder, inStart, index - 1);
+ root.right = build(preorder, preStart + leftSize + 1, preEnd,
+ inorder, index + 1, inEnd);
return root;
}
```
-不要看这个函数的参数很多,只是为了控制数组索引而已,本质上该算法也就是一个前序遍历。
+不要看这个函数的参数很多,只是为了控制数组索引而已。注意找递归函数 `build` 的位置,本质上该算法也就是一个前序遍历,因为它在前序遍历的位置加了一坨代码逻辑。
-LeetCode 99 题,难度 Hard,恢复一棵 BST,主要代码如下:
+力扣第 230 题,难度中等,寻找二叉搜索树中的第 `k` 小的元素,主要代码如下:
-```cpp
-void traverse(TreeNode* node) {
- if (!node) return;
- traverse(node->left);
- if (node->val < prev->val) {
- s = (s == NULL) ? prev : s;
- t = node;
+```java
+int res = 0;
+int rank = 0;
+void traverse(TreeNode root, int k) {
+ if (root == null) {
+ return;
}
- prev = node;
- traverse(node->right);
+ traverse(root.left, k);
+ /* 中序遍历代码位置 */
+ rank++;
+ if (k == rank) {
+ res = root.val;
+ return;
+ }
+ /*****************/
+ traverse(root.right, k);
}
```
这不就是个中序遍历嘛,对于一棵 BST 中序遍历意味着什么,应该不需要解释了吧。
-你看,Hard 难度的题目不过如此,而且还这么有规律可循,只要把框架写出来,然后往相应的位置加东西就行了,这不就是思路吗。
+你看,二叉树的题目不过如此,只要把框架写出来,然后往相应的位置加代码就行了,这不就是思路吗。
对于一个理解二叉树的人来说,刷一道二叉树的题目花不了多长时间。那么如果你对刷题无从下手或者有畏惧心理,不妨从二叉树下手,前 10 道也许有点难受;结合框架再做 20 道,也许你就有点自己的理解了;刷完整个专题,再去做什么回溯动规分治专题,**你就会发现只要涉及递归的问题,都是树的问题**。
+PS:[刷题插件](https://mp.weixin.qq.com/s/OE1zPVPj0V2o82N4HtLQbw) 集成了手把手刷二叉树功能,按照公式和套路讲解了 150 道二叉树题目,可手把手带你刷完二叉树分类的题目,迅速掌握递归思维。
+
再举例吧,说几道我们之前文章写过的问题。
-[动态规划详解](https://labuladong.gitee.io/algo/)说过凑零钱问题,暴力解法就是遍历一棵 N 叉树:
+[动态规划详解](https://labuladong.github.io/article/fname.html?fname=动态规划详解进阶)说过凑零钱问题,暴力解法就是遍历一棵 N 叉树:
-![](../pictures/动态规划详解进阶/5.jpg)
+![](https://labuladong.github.io/algo/images/动态规划详解进阶/5.jpg)
-```python
-def coinChange(coins: List[int], amount: int):
-
- def dp(n):
- if n == 0: return 0
- if n < 0: return -1
-
- res = float('INF')
- for coin in coins:
- subproblem = dp(n - coin)
- # 子问题无解,跳过
- if subproblem == -1: continue
- res = min(res, 1 + subproblem)
- return res if res != float('INF') else -1
-
- return dp(amount)
+```java
+int dp(int[] coins, int amount) {
+ // base case
+ if (amount == 0) return 0;
+ if (amount < 0) return -1;
+
+ int res = Integer.MAX_VALUE;
+ for (int coin : coins) {
+ int subProblem = dp(coins, amount - coin);
+ // 子问题无解则跳过
+ if (subProblem == -1) continue;
+ // 在子问题中选择最优解,然后加一
+ res = Math.min(res, subProblem + 1);
+ }
+ return res == Integer.MAX_VALUE ? -1 : res;
+}
```
这么多代码看不懂咋办?直接提取出框架,就能看出核心思路了:
```python
# 不过是一个 N 叉树的遍历问题而已
-def dp(n):
- for coin in coins:
- dp(n - coin)
+int dp(int amount) {
+ for (int coin : coins) {
+ dp(amount - coin);
+ }
+}
```
其实很多动态规划问题就是在遍历一棵树,你如果对树的遍历操作烂熟于心,起码知道怎么把思路转化成代码,也知道如何提取别人解法的核心思路。
-再看看回溯算法,前文[回溯算法详解](https://labuladong.gitee.io/algo/)干脆直接说了,回溯算法就是个 N 叉树的前后序遍历问题,没有例外。
+再看看回溯算法,前文 [回溯算法详解](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版) 干脆直接说了,回溯算法就是个 N 叉树的前后序遍历问题,没有例外。
+
+比如全排列问题吧,本质上全排列就是在遍历下面这棵树,到叶子节点的路径就是一个全排列:
+
+![](https://labuladong.github.io/algo/images/backtracking/1.jpg)
-比如 N 皇后问题吧,主要代码如下:
+全排列算法的主要代码如下:
```java
void backtrack(int[] nums, LinkedList track) {
@@ -264,7 +294,12 @@ void backtrack(int[] nums, LinkedList track) {
backtrack(nums, track);
track.removeLast();
}
+}
+```
+
+看不懂?没关系,把其中的递归部分抽取出来:
+```java
/* 提取出 N 叉树遍历框架 */
void backtrack(int[] nums, LinkedList track) {
for (int i = 0; i < nums.length; i++) {
@@ -272,11 +307,11 @@ void backtrack(int[] nums, LinkedList track) {
}
```
-N 叉树的遍历框架,找出来了把~你说,树这种结构重不重要?
+N 叉树的遍历框架,找出来了吧?你说,树这种结构重不重要?
-**综上,对于畏惧算法的朋友来说,可以先刷树的相关题目,试着从框架上看问题,而不要纠结于细节问题**。
+**综上,对于畏惧算法的同学来说,可以先刷树的相关题目,试着从框架上看问题,而不要纠结于细节问题**。
-纠结细节问题,就比如纠结 i 到底应该加到 n 还是加到 n - 1,这个数组的大小到底应该开 n 还是 n + 1 ?
+纠结细节问题,就比如纠结 `i` 到底应该加到 `n` 还是加到 `n - 1`,这个数组的大小到底应该开 `n` 还是 `n + 1`?
从框架上看问题,就是像我们这样基于框架进行抽取和扩展,既可以在看别人解法时快速理解核心逻辑,也有助于找到我们自己写解法时的思路方向。
@@ -284,27 +319,71 @@ N 叉树的遍历框架,找出来了把~你说,树这种结构重不重要
但是,你要是心中没有框架,那么你根本无法解题,给了你答案,你也不会发现这就是个树的遍历问题。
-这种思维是很重要的,[动态规划详解](https://labuladong.gitee.io/algo/)中总结的找状态转移方程的几步流程,有时候按照流程写出解法,说实话我自己都不知道为啥是对的,反正它就是对了。。。
-
-**这就是框架的力量,能够保证你在快睡着的时候,依然能写出正确的程序;就算你啥都不会,都能比别人高一个级别。**
+这种思维是很重要的,[动态规划详解](https://labuladong.github.io/article/fname.html?fname=动态规划详解进阶) 中总结的找状态转移方程的几步流程,有时候按照流程写出解法,可能自己都不知道为啥是对的,反正它就是对了。。。
+**这就是框架的力量,能够保证你在快睡着的时候,依然能写出正确的程序;就算你啥都不会,都能比别人高一个级别**。
-### 四、总结几句
+本文最后,总结一下吧:
数据结构的基本存储方式就是链式和顺序两种,基本操作就是增删查改,遍历方式无非迭代和递归。
-刷算法题建议从「树」分类开始刷,结合框架思维,把这几十道题刷完,对于树结构的理解应该就到位了。这时候去看回溯、动规、分治等算法专题,对思路的理解可能会更加深刻一些。
+学完基本算法之后,建议从「二叉树」系列问题开始刷,结合框架思维,把树结构理解到位,然后再去看回溯、动规、分治等算法专题,对思路的理解就会更加深刻。
+
+> 最后打个广告,我亲自制作了一门 [数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO),以视频课为主,手把手带你实现常用的数据结构及相关算法,旨在帮助算法基础较为薄弱的读者深入理解常用数据结构的底层原理,在算法学习中少走弯路。
+
+
+
+
+
+引用本文的文章
+
+ - [Dijkstra 算法模板及应用](https://labuladong.github.io/article/fname.html?fname=dijkstra算法)
+ - [一文秒杀所有岛屿题目](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=trie)
+ - [动态规划和回溯算法到底谁是谁爹?](https://labuladong.github.io/article/fname.html?fname=targetSum)
+ - [回溯算法秒杀所有排列/组合/子集问题](https://labuladong.github.io/article/fname.html?fname=子集排列组合)
+ - [回溯算法解题套路框架](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版)
+ - [图论基础及遍历算法](https://labuladong.github.io/article/fname.html?fname=图)
+ - [如何 K 个一组反转链表](https://labuladong.github.io/article/fname.html?fname=k个一组反转链表)
+ - [如何判断回文链表](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=时间复杂度)
+ - [题目不让我干什么,我偏要干什么](https://labuladong.github.io/article/fname.html?fname=nestInteger)
+
+
+
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| [100. Same Tree](https://leetcode.com/problems/same-tree/?show=1) | [100. 相同的树](https://leetcode.cn/problems/same-tree/?show=1) |
+| [341. Flatten Nested List Iterator](https://leetcode.com/problems/flatten-nested-list-iterator/?show=1) | [341. 扁平化嵌套列表迭代器](https://leetcode.cn/problems/flatten-nested-list-iterator/?show=1) |
+| [589. N-ary Tree Preorder Traversal](https://leetcode.com/problems/n-ary-tree-preorder-traversal/?show=1) | [589. N 叉树的前序遍历](https://leetcode.cn/problems/n-ary-tree-preorder-traversal/?show=1) |
+| [590. N-ary Tree Postorder Traversal](https://leetcode.com/problems/n-ary-tree-postorder-traversal/?show=1) | [590. N 叉树的后序遍历](https://leetcode.cn/problems/n-ary-tree-postorder-traversal/?show=1) |
+
+
**_____________**
-**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**。
+**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
-**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。
+![](https://labuladong.github.io/algo/images/souyisou2.png)
-
-
-
======其他语言代码======
\ No newline at end of file
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\267\256\345\210\206\346\212\200\345\267\247.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\267\256\345\210\206\346\212\200\345\267\247.md"
new file mode 100644
index 0000000000000000000000000000000000000000..b5b3eefc82862743c0cd65095ea223c85a5df8e3
--- /dev/null
+++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\267\256\345\210\206\346\212\200\345\267\247.md"
@@ -0,0 +1,300 @@
+# 那些小而美的算法技巧:前缀和/差分数组
+
+
+
+
+
+
+
+
+![](https://labuladong.github.io/algo/images/souyisou1.png)
+
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
+
+
+
+读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
+
+| LeetCode | 力扣 | 难度 |
+| :----: | :----: | :----: |
+| [1094. Car Pooling](https://leetcode.com/problems/car-pooling/) | [1094. 拼车](https://leetcode.cn/problems/car-pooling/) | 🟠
+| [1109. Corporate Flight Bookings](https://leetcode.com/problems/corporate-flight-bookings/) | [1109. 航班预订统计](https://leetcode.cn/problems/corporate-flight-bookings/) | 🟠
+| [370. Range Addition](https://leetcode.com/problems/range-addition/)🔒 | [370. 区间加法](https://leetcode.cn/problems/range-addition/)🔒 | 🟠
+
+**-----------**
+
+前文 [前缀和技巧详解](https://labuladong.github.io/article/fname.html?fname=前缀和技巧) 写过的前缀和技巧是非常常用的算法技巧,**前缀和主要适用的场景是原始数组不会被修改的情况下,频繁查询某个区间的累加和**。
+
+没看过前文没关系,这里简单介绍一下前缀和,核心代码就是下面这段:
+
+```java
+class PrefixSum {
+ // 前缀和数组
+ private int[] prefix;
+
+ /* 输入一个数组,构造前缀和 */
+ public PrefixSum(int[] nums) {
+ prefix = new int[nums.length + 1];
+ // 计算 nums 的累加和
+ for (int i = 1; i < prefix.length; i++) {
+ prefix[i] = prefix[i - 1] + nums[i - 1];
+ }
+ }
+
+ /* 查询闭区间 [i, j] 的累加和 */
+ public int query(int i, int j) {
+ return prefix[j + 1] - prefix[i];
+ }
+}
+```
+
+![](https://labuladong.github.io/algo/images/差分数组/1.jpeg)
+
+`prefix[i]` 就代表着 `nums[0..i-1]` 所有元素的累加和,如果我们想求区间 `nums[i..j]` 的累加和,只要计算 `prefix[j+1] - prefix[i]` 即可,而不需要遍历整个区间求和。
+
+本文讲一个和前缀和思想非常类似的算法技巧「差分数组」,**差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减**。
+
+比如说,我给你输入一个数组 `nums`,然后又要求给区间 `nums[2..6]` 全部加 1,再给 `nums[3..9]` 全部减 3,再给 `nums[0..4]` 全部加 2,再给...
+
+一通操作猛如虎,然后问你,最后 `nums` 数组的值是什么?
+
+常规的思路很容易,你让我给区间 `nums[i..j]` 加上 `val`,那我就一个 for 循环给它们都加上呗,还能咋样?这种思路的时间复杂度是 O(N),由于这个场景下对 `nums` 的修改非常频繁,所以效率会很低下。
+
+这里就需要差分数组的技巧,类似前缀和技巧构造的 `prefix` 数组,我们先对 `nums` 数组构造一个 `diff` 差分数组,**`diff[i]` 就是 `nums[i]` 和 `nums[i-1]` 之差**:
+
+```java
+int[] diff = new int[nums.length];
+// 构造差分数组
+diff[0] = nums[0];
+for (int i = 1; i < nums.length; i++) {
+ diff[i] = nums[i] - nums[i - 1];
+}
+```
+
+![](https://labuladong.github.io/algo/images/差分数组/2.jpeg)
+
+通过这个 `diff` 差分数组是可以反推出原始数组 `nums` 的,代码逻辑如下:
+
+```java
+int[] res = new int[diff.length];
+// 根据差分数组构造结果数组
+res[0] = diff[0];
+for (int i = 1; i < diff.length; i++) {
+ res[i] = res[i - 1] + diff[i];
+}
+```
+
+**这样构造差分数组 `diff`,就可以快速进行区间增减的操作**,如果你想对区间 `nums[i..j]` 的元素全部加 3,那么只需要让 `diff[i] += 3`,然后再让 `diff[j+1] -= 3` 即可:
+
+![](https://labuladong.github.io/algo/images/差分数组/3.jpeg)
+
+**原理很简单,回想 `diff` 数组反推 `nums` 数组的过程,`diff[i] += 3` 意味着给 `nums[i..]` 所有的元素都加了 3,然后 `diff[j+1] -= 3` 又意味着对于 `nums[j+1..]` 所有元素再减 3,那综合起来,是不是就是对 `nums[i..j]` 中的所有元素都加 3 了**?
+
+只要花费 O(1) 的时间修改 `diff` 数组,就相当于给 `nums` 的整个区间做了修改。多次修改 `diff`,然后通过 `diff` 数组反推,即可得到 `nums` 修改后的结果。
+
+现在我们把差分数组抽象成一个类,包含 `increment` 方法和 `result` 方法:
+
+```java
+// 差分数组工具类
+class Difference {
+ // 差分数组
+ private int[] diff;
+
+ /* 输入一个初始数组,区间操作将在这个数组上进行 */
+ public Difference(int[] nums) {
+ assert nums.length > 0;
+ diff = new int[nums.length];
+ // 根据初始数组构造差分数组
+ diff[0] = nums[0];
+ for (int i = 1; i < nums.length; i++) {
+ diff[i] = nums[i] - nums[i - 1];
+ }
+ }
+
+ /* 给闭区间 [i, j] 增加 val(可以是负数)*/
+ public void increment(int i, int j, int val) {
+ diff[i] += val;
+ if (j + 1 < diff.length) {
+ diff[j + 1] -= val;
+ }
+ }
+
+ /* 返回结果数组 */
+ public int[] result() {
+ int[] res = new int[diff.length];
+ // 根据差分数组构造结果数组
+ res[0] = diff[0];
+ for (int i = 1; i < diff.length; i++) {
+ res[i] = res[i - 1] + diff[i];
+ }
+ return res;
+ }
+}
+
+```
+
+这里注意一下 `increment` 方法中的 if 语句:
+
+```java
+public void increment(int i, int j, int val) {
+ diff[i] += val;
+ if (j + 1 < diff.length) {
+ diff[j + 1] -= val;
+ }
+}
+```
+
+当 `j+1 >= diff.length` 时,说明是对 `nums[i]` 及以后的整个数组都进行修改,那么就不需要再给 `diff` 数组减 `val` 了。
+
+### 算法实践
+
+首先,力扣第 370 题「区间加法」 就直接考察了差分数组技巧:
+
+![](https://labuladong.github.io/algo/images/差分数组/title1.png)
+
+那么我们直接复用刚才实现的 `Difference` 类就能把这道题解决掉:
+
+```java
+int[] getModifiedArray(int length, int[][] updates) {
+ // nums 初始化为全 0
+ int[] nums = new int[length];
+ // 构造差分解法
+ Difference df = new Difference(nums);
+
+ for (int[] update : updates) {
+ int i = update[0];
+ int j = update[1];
+ int val = update[2];
+ df.increment(i, j, val);
+ }
+
+ return df.result();
+}
+```
+
+当然,实际的算法题可能需要我们对题目进行联想和抽象,不会这么直接地让你看出来要用差分数组技巧,这里看一下力扣第 1109 题「航班预订统计」:
+
+![](https://labuladong.github.io/algo/images/差分数组/title.png)
+
+函数签名如下:
+
+```java
+int[] corpFlightBookings(int[][] bookings, int n)
+```
+
+这个题目就在那绕弯弯,其实它就是个差分数组的题,我给你翻译一下:
+
+给你输入一个长度为 `n` 的数组 `nums`,其中所有元素都是 0。再给你输入一个 `bookings`,里面是若干三元组 `(i, j,k)`,每个三元组的含义就是要求你给 `nums` 数组的闭区间 `[i-1,j-1]` 中所有元素都加上 `k`。请你返回最后的 `nums` 数组是多少?
+
+> PS:因为题目说的 `n` 是从 1 开始计数的,而数组索引从 0 开始,所以对于输入的三元组 `(i, j,k)`,数组区间应该对应 `[i-1,j-1]`。
+
+这么一看,不就是一道标准的差分数组题嘛?我们可以直接复用刚才写的类:
+
+```java
+int[] corpFlightBookings(int[][] bookings, int n) {
+ // nums 初始化为全 0
+ int[] nums = new int[n];
+ // 构造差分解法
+ Difference df = new Difference(nums);
+
+ for (int[] booking : bookings) {
+ // 注意转成数组索引要减一哦
+ int i = booking[0] - 1;
+ int j = booking[1] - 1;
+ int val = booking[2];
+ // 对区间 nums[i..j] 增加 val
+ df.increment(i, j, val);
+ }
+ // 返回最终的结果数组
+ return df.result();
+}
+```
+
+这道题就解决了。
+
+还有一道很类似的题目是力扣第 1094 题「拼车」,我简单描述下题目:
+
+你是一个开公交车的司机,公交车的最大载客量为 `capacity`,沿途要经过若干车站,给你一份乘客行程表 `int[][] trips`,其中 `trips[i] = [num, start, end]` 代表着有 `num` 个旅客要从站点 `start` 上车,到站点 `end` 下车,请你计算是否能够一次把所有旅客运送完毕(不能超过最大载客量 `capacity`)。
+
+函数签名如下:
+
+```java
+boolean carPooling(int[][] trips, int capacity);
+```
+
+比如输入:
+
+```shell
+trips = [[2,1,5],[3,3,7]], capacity = 4
+```
+
+这就不能一次运完,因为 `trips[1]` 最多只能上 2 人,否则车就会超载。
+
+相信你已经能够联想到差分数组技巧了:**`trips[i]` 代表着一组区间操作,旅客的上车和下车就相当于数组的区间加减;只要结果数组中的元素都小于 `capacity`,就说明可以不超载运输所有旅客**。
+
+但问题是,差分数组的长度(车站的个数)应该是多少呢?题目没有直接给,但给出了数据取值范围:
+
+```java
+0 <= trips[i][1] < trips[i][2] <= 1000
+```
+
+车站编号从 0 开始,最多到 1000,也就是最多有 1001 个车站,那么我们的差分数组长度可以直接设置为 1001,这样索引刚好能够涵盖所有车站的编号:
+
+```java
+boolean carPooling(int[][] trips, int capacity) {
+ // 最多有 1001 个车站
+ int[] nums = new int[1001];
+ // 构造差分解法
+ Difference df = new Difference(nums);
+
+ for (int[] trip : trips) {
+ // 乘客数量
+ int val = trip[0];
+ // 第 trip[1] 站乘客上车
+ int i = trip[1];
+ // 第 trip[2] 站乘客已经下车,
+ // 即乘客在车上的区间是 [trip[1], trip[2] - 1]
+ int j = trip[2] - 1;
+ // 进行区间操作
+ df.increment(i, j, val);
+ }
+
+ int[] res = df.result();
+
+ // 客车自始至终都不应该超载
+ for (int i = 0; i < res.length; i++) {
+ if (capacity < res[i]) {
+ return false;
+ }
+ }
+ return true;
+}
+```
+
+至此,这道题也解决了。
+
+最后,差分数组和前缀和数组都是比较常见且巧妙的算法技巧,分别适用不同的场景,而且是会者不难,难者不会。所以,关于差分数组的使用,你学会了吗?
+
+> 最后打个广告,我亲自制作了一门 [数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO),以视频课为主,手把手带你实现常用的数据结构及相关算法,旨在帮助算法基础较为薄弱的读者深入理解常用数据结构的底层原理,在算法学习中少走弯路。
+
+
+
+
+
+引用本文的文章
+
+ - [二维数组的花式遍历技巧](https://labuladong.github.io/article/fname.html?fname=花式遍历)
+ - [我的刷题心得](https://labuladong.github.io/article/fname.html?fname=算法心得)
+ - [扫描线技巧:安排会议室](https://labuladong.github.io/article/fname.html?fname=安排会议室)
+
+
+
+
+
+
+
+**_____________**
+
+**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
+
+![](https://labuladong.github.io/algo/images/souyisou2.png)
\ No newline at end of file
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\270\270\347\224\250\347\232\204\344\275\215\346\223\215\344\275\234.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\270\270\347\224\250\347\232\204\344\275\215\346\223\215\344\275\234.md"
index e10737aa27882d9c7c0e390aede80ed553d96b4e..2eb91e44eb0061e670fcb4e2ab0a5b7e74813f59 100644
--- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\270\270\347\224\250\347\232\204\344\275\215\346\223\215\344\275\234.md"
+++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\270\270\347\224\250\347\232\204\344\275\215\346\223\215\344\275\234.md"
@@ -1,9 +1,5 @@
# 常用的位运算技巧
-
-
-
-
@@ -13,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -247,6 +243,35 @@ int missingNumber(int[] nums) {
http://graphics.stanford.edu/~seander/bithacks.html#ReverseParallel
+
+
+
+
+引用本文的文章
+
+ - [丑数系列算法详解](https://labuladong.github.io/article/fname.html?fname=丑数)
+ - [如何同时寻找缺失和重复的元素](https://labuladong.github.io/article/fname.html?fname=缺失和重复的元素)
+
+
+
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| [389. Find the Difference](https://leetcode.com/problems/find-the-difference/?show=1) | [389. 找不同](https://leetcode.cn/problems/find-the-difference/?show=1) |
+| - | [剑指 Offer 15. 二进制中1的个数](https://leetcode.cn/problems/er-jin-zhi-zhong-1de-ge-shu-lcof/?show=1) |
+
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\264\227\347\211\214\347\256\227\346\263\225.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\264\227\347\211\214\347\256\227\346\263\225.md"
index 12492ca71288164ecf6482179cabcb4a71824229..b070eea8a2dfd3defdc0ebb8e61d829f880cb89b 100644
--- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\264\227\347\211\214\347\256\227\346\263\225.md"
+++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\264\227\347\211\214\347\256\227\346\263\225.md"
@@ -1,7 +1,5 @@
# 洗牌算法
-
-
@@ -11,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -204,6 +202,21 @@ for (int feq : count)
第二部分写了洗牌算法正确性的衡量标准,即每种随机结果出现的概率必须相等。如果我们不用严格的数学证明,可以通过蒙特卡罗方法大力出奇迹,粗略验证算法的正确性。蒙特卡罗方法也有不同的思路,不过要求不必太严格,因为我们只是寻求一个简单的验证。
+
+
+
+
+引用本文的文章
+
+ - [几个反直觉的概率问题](https://labuladong.github.io/article/fname.html?fname=几个反直觉的概率问题)
+ - [快速排序详解及应用](https://labuladong.github.io/article/fname.html?fname=快速排序)
+
+
+
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\273\221\345\212\250\347\252\227\345\217\243\346\212\200\345\267\247.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\273\221\345\212\250\347\252\227\345\217\243\346\212\200\345\267\247.md"
deleted file mode 100644
index 9f28548260ce3f395189d3662470cb395a1cb4bf..0000000000000000000000000000000000000000
--- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\273\221\345\212\250\347\252\227\345\217\243\346\212\200\345\267\247.md"
+++ /dev/null
@@ -1,580 +0,0 @@
-# 滑动窗口算法框架
-
-
-
-
-
-
-
-
-![](../pictures/souyisou.png)
-
-**最新消息:关注公众号参与活动,有机会成为 [70k star 算法仓库](https://github.com/labuladong/fucking-algorithm) 的贡献者,机不可失时不再来**!
-
-相关推荐:
-* [东哥吃葡萄时竟然吃出一道算法题!](https://labuladong.gitee.io/algo/)
-* [如何寻找缺失的元素](https://labuladong.gitee.io/algo/)
-
-读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
-
-[76.最小覆盖子串](https://leetcode-cn.com/problems/minimum-window-substring)
-
-[567.字符串的排列](https://leetcode-cn.com/problems/permutation-in-string)
-
-[438.找到字符串中所有字母异位词](https://leetcode-cn.com/problems/find-all-anagrams-in-a-string)
-
-[3.无重复字符的最长子串](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters)
-
-**-----------**
-
-本文是之前的一篇文章 [滑动窗口算法详解](https://mp.weixin.qq.com/s/nJHIxQ2BbqhDv5jZ9NgXrQ) 的修订版,添加了滑动窗口算法更详细的解释。
-
-本文详解「滑动窗口」这种高级双指针技巧的算法框架,带你秒杀几道高难度的子字符串匹配问题。
-
-LeetCode 上至少有 9 道题目可以用此方法高效解决。但是有几道是 VIP 题目,有几道题目虽不难但太复杂,所以本文只选择点赞最高,较为经典的,最能够讲明白的三道题来讲解。第一题为了让读者掌握算法模板,篇幅相对长,后两题就基本秒杀了。
-
-本文代码为 C++ 实现,不会用到什么编程方面的奇技淫巧,但是还是简单介绍一下一些用到的数据结构,以免有的读者因为语言的细节问题阻碍对算法思想的理解:
-
-`unordered_map` 就是哈希表(字典),它的一个方法 count(key) 相当于 containsKey(key) 可以判断键 key 是否存在。
-
-可以使用方括号访问键对应的值 map[key]。需要注意的是,如果该 key 不存在,C++ 会自动创建这个 key,并把 map[key] 赋值为 0。
-
-所以代码中多次出现的 `map[key]++` 相当于 Java 的 `map.put(key, map.getOrDefault(key, 0) + 1)`。
-
-本文大部分代码都是图片形式,可以点开放大,更重要的是可以左右滑动方便对比代码。下面进入正题。
-
-### 一、最小覆盖子串
-
-![题目链接](../pictures/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/title1.png)
-
-题目不难理解,就是说要在 S(source) 中找到包含 T(target) 中全部字母的一个子串,顺序无所谓,但这个子串一定是所有可能子串中最短的。
-
-如果我们使用暴力解法,代码大概是这样的:
-
-```java
-for (int i = 0; i < s.size(); i++)
- for (int j = i + 1; j < s.size(); j++)
- if s[i:j] 包含 t 的所有字母:
- 更新答案
-```
-
-思路很直接吧,但是显然,这个算法的复杂度肯定大于 O(N^2) 了,不好。
-
-滑动窗口算法的思路是这样:
-
-1、我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引闭区间 [left, right] 称为一个「窗口」。
-
-2、我们先不断地增加 right 指针扩大窗口 [left, right],直到窗口中的字符串符合要求(包含了 T 中的所有字符)。
-
-3、此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right],直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。
-
-4、重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。
-
-这个思路其实也不难,**第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解。**左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动。
-
-下面画图理解一下,needs 和 window 相当于计数器,分别记录 T 中字符出现次数和窗口中的相应字符的出现次数。
-
-初始状态:
-
-![0](../pictures/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/0.png)
-
-增加 right,直到窗口 [left, right] 包含了 T 中所有字符:
-
-![0](../pictures/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/1.png)
-
-
-现在开始增加 left,缩小窗口 [left, right]。
-
-![0](../pictures/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/2.png)
-
-直到窗口中的字符串不再符合要求,left 不再继续移动。
-
-![0](../pictures/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/3.png)
-
-
-之后重复上述过程,先移动 right,再移动 left…… 直到 right 指针到达字符串 S 的末端,算法结束。
-
-如果你能够理解上述过程,恭喜,你已经完全掌握了滑动窗口算法思想。至于如何具体到问题,如何得出此题的答案,都是编程问题,等会提供一套模板,理解一下就会了。
-
-上述过程可以简单地写出如下伪码框架:
-
-```cpp
-string s, t;
-// 在 s 中寻找 t 的「最小覆盖子串」
-int left = 0, right = 0;
-string res = s;
-
-while(right < s.size()) {
- window.add(s[right]);
- right++;
- // 如果符合要求,移动 left 缩小窗口
- while (window 符合要求) {
- // 如果这个窗口的子串更短,则更新 res
- res = minLen(res, window);
- window.remove(s[left]);
- left++;
- }
-}
-return res;
-```
-
-如果上述代码你也能够理解,那么你离解题更近了一步。现在就剩下一个比较棘手的问题:如何判断 window 即子串 s[left...right] 是否符合要求,是否包含 t 的所有字符呢?
-
-可以用两个哈希表当作计数器解决。用一个哈希表 needs 记录字符串 t 中包含的字符及出现次数,用另一个哈希表 window 记录当前「窗口」中包含的字符及出现的次数,如果 window 包含所有 needs 中的键,且这些键对应的值都大于等于 needs 中的值,那么就可以知道当前「窗口」符合要求了,可以开始移动 left 指针了。
-
-现在将上面的框架继续细化:
-
-```cpp
-string s, t;
-// 在 s 中寻找 t 的「最小覆盖子串」
-int left = 0, right = 0;
-string res = s;
-
-// 相当于两个计数器
-unordered_map window;
-unordered_map needs;
-for (char c : t) needs[c]++;
-
-// 记录 window 中已经有多少字符符合要求了
-int match = 0;
-
-while (right < s.size()) {
- char c1 = s[right];
- if (needs.count(c1)) {
- window[c1]++; // 加入 window
- if (window[c1] == needs[c1])
- // 字符 c1 的出现次数符合要求了
- match++;
- }
- right++;
-
- // window 中的字符串已符合 needs 的要求了
- while (match == needs.size()) {
- // 更新结果 res
- res = minLen(res, window);
- char c2 = s[left];
- if (needs.count(c2)) {
- window[c2]--; // 移出 window
- if (window[c2] < needs[c2])
- // 字符 c2 出现次数不再符合要求
- match--;
- }
- left++;
- }
-}
-return res;
-```
-
-上述代码已经具备完整的逻辑了,只有一处伪码,即更新 res 的地方,不过这个问题太好解决了,直接看解法吧!
-
-```cpp
-string minWindow(string s, string t) {
- // 记录最短子串的开始位置和长度
- int start = 0, minLen = INT_MAX;
- int left = 0, right = 0;
-
- unordered_map window;
- unordered_map needs;
- for (char c : t) needs[c]++;
-
- int match = 0;
-
- while (right < s.size()) {
- char c1 = s[right];
- if (needs.count(c1)) {
- window[c1]++;
- if (window[c1] == needs[c1])
- match++;
- }
- right++;
-
- while (match == needs.size()) {
- if (right - left < minLen) {
- // 更新最小子串的位置和长度
- start = left;
- minLen = right - left;
- }
- char c2 = s[left];
- if (needs.count(c2)) {
- window[c2]--;
- if (window[c2] < needs[c2])
- match--;
- }
- left++;
- }
- }
- return minLen == INT_MAX ?
- "" : s.substr(start, minLen);
-}
-```
-
-如果直接甩给你这么一大段代码,我想你的心态是爆炸的,但是通过之前的步步跟进,你是否能够理解这个算法的内在逻辑呢?你是否能清晰看出该算法的结构呢?
-
-这个算法的时间复杂度是 O(M + N),M 和 N 分别是字符串 S 和 T 的长度。因为我们先用 for 循环遍历了字符串 T 来初始化 needs,时间 O(N),之后的两个 while 循环最多执行 2M 次,时间 O(M)。
-
-读者也许认为嵌套的 while 循环复杂度应该是平方级,但是你这样想,while 执行的次数就是双指针 left 和 right 走的总路程,最多是 2M 嘛。
-
-
-### 二、找到字符串中所有字母异位词
-
-![题目链接](../pictures/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/title2.png)
-
-这道题的难度是 Easy,但是评论区点赞最多的一条是这样:
-
-`How can this problem be marked as easy???`
-
-实际上,这个 Easy 是属于了解双指针技巧的人的,只要把上一道题的代码改中更新 res 部分的代码稍加修改就成了这道题的解:
-
-```cpp
-vector findAnagrams(string s, string t) {
- // 用数组记录答案
- vector res;
- int left = 0, right = 0;
- unordered_map needs;
- unordered_map window;
- for (char c : t) needs[c]++;
- int match = 0;
-
- while (right < s.size()) {
- char c1 = s[right];
- if (needs.count(c1)) {
- window[c1]++;
- if (window[c1] == needs[c1])
- match++;
- }
- right++;
-
- while (match == needs.size()) {
- // 如果 window 的大小合适
- // 就把起始索引 left 加入结果
- if (right - left == t.size()) {
- res.push_back(left);
- }
- char c2 = s[left];
- if (needs.count(c2)) {
- window[c2]--;
- if (window[c2] < needs[c2])
- match--;
- }
- left++;
- }
- }
- return res;
-}
-```
-
-因为这道题和上一道的场景类似,也需要 window 中包含串 t 的所有字符,但上一道题要找长度最短的子串,这道题要找长度相同的子串,也就是「字母异位词」嘛。
-
-### 三、无重复字符的最长子串
-
-![题目链接](../pictures/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3/title3.png)
-
-遇到子串问题,首先想到的就是滑动窗口技巧。
-
-类似之前的思路,使用 window 作为计数器记录窗口中的字符出现次数,然后先向右移动 right,当 window 中出现重复字符时,开始移动 left 缩小窗口,如此往复:
-
-```cpp
-int lengthOfLongestSubstring(string s) {
- int left = 0, right = 0;
- unordered_map window;
- int res = 0; // 记录最长长度
-
- while (right < s.size()) {
- char c1 = s[right];
- window[c1]++;
- right++;
- // 如果 window 中出现重复字符
- // 开始移动 left 缩小窗口
- while (window[c1] > 1) {
- char c2 = s[left];
- window[c2]--;
- left++;
- }
- res = max(res, right - left);
- }
- return res;
-}
-```
-
-需要注意的是,因为我们要求的是最长子串,所以需要在每次移动 right 增大窗口时更新 res,而不是像之前的题目在移动 left 缩小窗口时更新 res。
-
-### 最后总结
-
-通过上面三道题,我们可以总结出滑动窗口算法的抽象思想:
-
-```java
-int left = 0, right = 0;
-
-while (right < s.size()) {
- window.add(s[right]);
- right++;
-
- while (valid) {
- window.remove(s[left]);
- left++;
- }
-}
-```
-
-其中 window 的数据类型可以视具体情况而定,比如上述题目都使用哈希表充当计数器,当然你也可以用一个数组实现同样效果,因为我们只处理英文字母。
-
-稍微麻烦的地方就是这个 valid 条件,为了实现这个条件的实时更新,我们可能会写很多代码。比如前两道题,看起来解法篇幅那么长,实际上思想还是很简单,只是大多数代码都在处理这个问题而已。
-
-**_____________**
-
-**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**。
-
-**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。
-
-
-
-
-
-======其他语言代码======
-
-[Jiajun](https://github.com/liujiajun) 提供最小覆盖子串 Python3 代码:
-```python3
-class Solution:
- def minWindow(self, s: str, t: str) -> str:
- # 最短子串开始位置和长度
- start, min_len = 0, float('Inf')
- left, right = 0, 0
- res = s
-
- # 两个计数器
- needs = Counter(t)
- window = collections.defaultdict(int)
- # defaultdict在访问的key不存在的时候返回默认值0, 可以减少一次逻辑判断
-
- match = 0
-
- while right < len(s):
- c1 = s[right]
- if needs[c1] > 0:
- window[c1] += 1
- if window[c1] == needs[c1]:
- match += 1
- right += 1
-
- while match == len(needs):
- if right - left < min_len:
- # 更新最小子串长度
- min_len = right - left
- start = left
- c2 = s[left]
- if needs[c2] > 0:
- window[c2] -= 1
- if window[c2] < needs[c2]:
- match -= 1
- left += 1
-
- return s[start:start+min_len] if min_len != float("Inf") else ""
-```
-
-**_____________**
-
-**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**。
-
-**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。
-
-
-
-
-
-======其他语言代码======
-
-### python
-
-第3题 Python3 代码(提供: [FaDrYL](https://github.com/FaDrYL) ):
-```python
-def lengthOfLongestSubstring(self, s: str) -> int:
- # 子字符串
- sub = ""
- largest = 0
-
- # 循环字符串,将当前字符加入子字符串,并检查长度
- for i in range(len(s)):
- if s[i] not in sub:
- # 当前字符不存在于子字符串中,加入当前字符
- sub += s[i]
- else:
- # 如果当前子字符串的长度超过了之前的记录
- if len(sub) > largest:
- largest = len(sub)
- # 将子字符串从当前字符处+1切片至最后,并加入当前字符
- sub = sub[sub.find(s[i])+1:] + s[i]
-
- # 如果最后的子字符串长度超过了之前的记录
- if len(sub) > largest:
- return len(sub)
- return largest
-```
-
-
-
-### javascript
-
-[76.最小覆盖子串](https://leetcode-cn.com/problems/minimum-window-substring)
-
-```js
-/**
- * @param {string} s
- * @param {string} t
- * @return {string}
- */
-const minWindow = (s, t) => {
- let minLen = s.length + 1;
- let start = s.length; // 结果子串的起始位置
- let map = {}; // 存储目标字符和对应的缺失个数
- let missingType = 0; // 当前缺失的字符种类数
- for (const c of t) { // t为baac的话,map为{a:2,b:1,c:1}
- if (!map[c]) {
- missingType++; // 需要找齐的种类数 +1
- map[c] = 1;
- } else {
- map[c]++;
- }
- }
- let l = 0, r = 0; // 左右指针
- for (; r < s.length; r++) { // 主旋律扩张窗口,超出s串就结束
- let rightChar = s[r]; // 获取right指向的新字符
- if (map[rightChar] !== undefined) map[rightChar]--; // 是目标字符,它的缺失个数-1
- if (map[rightChar] == 0) missingType--; // 它的缺失个数新变为0,缺失的种类数就-1
- while (missingType == 0) { // 当前窗口包含所有字符的前提下,尽量收缩窗口
- if (r - l + 1 < minLen) { // 窗口宽度如果比minLen小,就更新minLen
- minLen = r - l + 1;
- start = l; // 更新最小窗口的起点
- }
- let leftChar = s[l]; // 左指针要右移,左指针指向的字符要被丢弃
- if (map[leftChar] !== undefined) map[leftChar]++; // 被舍弃的是目标字符,缺失个数+1
- if (map[leftChar] > 0) missingType++; // 如果缺失个数新变为>0,缺失的种类+1
- l++; // 左指针要右移 收缩窗口
- }
- }
- if (start == s.length) return "";
- return s.substring(start, start + minLen); // 根据起点和minLen截取子串
-};
-```
-
-[567.字符串的排列](https://leetcode-cn.com/problems/permutation-in-string)
-
-```js
-var checkInclusion = function(s1, s2) {
- const n = s1.length, m = s2.length;
- if (n > m) {
- return false;
- }
- const cnt1 = new Array(26).fill(0);
- const cnt2 = new Array(26).fill(0);
- for (let i = 0; i < n; ++i) {
- ++cnt1[s1[i].charCodeAt() - 'a'.charCodeAt()];
- ++cnt2[s2[i].charCodeAt() - 'a'.charCodeAt()];
- }
- if (cnt1.toString() === cnt2.toString()) {
- return true;
- }
- for (let i = n; i < m; ++i) {
- ++cnt2[s2[i].charCodeAt() - 'a'.charCodeAt()];
- --cnt2[s2[i - n].charCodeAt() - 'a'.charCodeAt()];
- if (cnt1.toString() === cnt2.toString()) {
- return true;
- }
- }
- return false;
-};
-```
-
-[438.找到字符串中所有字母异位词](https://leetcode-cn.com/problems/find-all-anagrams-in-a-string)
-
-```js
-/**
- * @param {string} s
- * @param {string} p
- * @return {number[]}
- */
-var findAnagrams = function(s, p) {
- // 用于保存结果
- const res = []
- // 用于统计p串所需字符
- const need = new Map()
- for(let i = 0; i < p.length; i++) {
- need.set(p[i], need.has(p[i])?need.get(p[i])+1: 1)
- }
- // 定义滑动窗口
- let left = 0, right = 0, valid = 0
- // 用于统计窗口中的字符
- const window = new Map()
- // 遍历s串
- while(right < s.length) {
- // 进入窗口的字符
- const c = s[right]
- // 扩大窗口
- right++
- // 进入窗口的字符是所需字符
- if (need.has(c)) {
- // 更新滑动窗口中的字符记录
- window.set(c, window.has(c)?window.get(c)+1:1)
- // 当窗口中的字符数和滑动窗口中的字符数一致
- if (window.get(c) === need.get(c)) {
- // 有效字符自增
- valid++
- }
- }
- // 当滑动窗口的大小超出p串长度时 收缩窗口
- while (right - left >= p.length) {
- // 有效字符和所需字符数一致 找到一条符合条件的子串
- if (valid === need.size) {
- // 保存子串的起始索引位置
- res.push(left)
- }
- // 离开窗口的字符
- const d = s[left]
- // 收缩窗口
- left++
- // 如果离开窗口字符是所需字符
- if (need.has(d)) {
- // 如果离开字符数和所需字符数一致
- if (window.get(d) === need.get(d)) {
- // 有效字符减少一个
- valid--
- }
- // 更新滑动窗口中的字符数
- window.set(d, window.get(d)-1)
- }
- }
- }
- // 返回结果
- return res
-};
-```
-
-[3.无重复字符的最长子串](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters)
-
-```js
-/**
- * @param {string} s
- * @return {number}
- */
-var lengthOfLongestSubstring = function(s) {
- // 哈希集合,记录每个字符是否出现过
- const occ = new Set();
- const n = s.length;
- // 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
- let rk = -1, ans = 0;
- for (let i = 0; i < n; ++i) {
- if (i != 0) {
- // 左指针向右移动一格,移除一个字符
- occ.delete(s.charAt(i - 1));
- }
- while (rk + 1 < n && !occ.has(s.charAt(rk + 1))) {
- // 不断地移动右指针
- occ.add(s.charAt(rk + 1));
- ++rk;
- }
- // 第 i 到 rk 个字符是一个极长的无重复字符子串
- ans = Math.max(ans, rk - i + 1);
- }
- return ans;
-};
-```
-
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\273\221\345\212\250\347\252\227\345\217\243\346\212\200\345\267\247\350\277\233\351\230\266.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\273\221\345\212\250\347\252\227\345\217\243\346\212\200\345\267\247\350\277\233\351\230\266.md"
new file mode 100644
index 0000000000000000000000000000000000000000..dccf74541ae52c5d9f285e9d75d12169ec224729
--- /dev/null
+++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\273\221\345\212\250\347\252\227\345\217\243\346\212\200\345\267\247\350\277\233\351\230\266.md"
@@ -0,0 +1,481 @@
+# 滑动窗口算法框架
+
+
+
+
+
+
+
+
+![](https://labuladong.github.io/algo/images/souyisou1.png)
+
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
+
+
+
+读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
+
+| LeetCode | 力扣 | 难度 |
+| :----: | :----: | :----: |
+| [3. Longest Substring Without Repeating Characters](https://leetcode.com/problems/longest-substring-without-repeating-characters/) | [3. 无重复字符的最长子串](https://leetcode.cn/problems/longest-substring-without-repeating-characters/) | 🟠
+| [438. Find All Anagrams in a String](https://leetcode.com/problems/find-all-anagrams-in-a-string/) | [438. 找到字符串中所有字母异位词](https://leetcode.cn/problems/find-all-anagrams-in-a-string/) | 🟠
+| [567. Permutation in String](https://leetcode.com/problems/permutation-in-string/) | [567. 字符串的排列](https://leetcode.cn/problems/permutation-in-string/) | 🟠
+| [76. Minimum Window Substring](https://leetcode.com/problems/minimum-window-substring/) | [76. 最小覆盖子串](https://leetcode.cn/problems/minimum-window-substring/) | 🔴
+| - | [剑指 Offer 48. 最长不含重复字符的子字符串](https://leetcode.cn/problems/zui-chang-bu-han-zhong-fu-zi-fu-de-zi-zi-fu-chuan-lcof/) | 🟠
+| - | [剑指 Offer II 014. 字符串中的变位词](https://leetcode.cn/problems/MPnaiL/) | 🟠
+| - | [剑指 Offer II 015. 字符串中的所有变位词](https://leetcode.cn/problems/VabMRr/) | 🟠
+| - | [剑指 Offer II 016. 不含重复字符的最长子字符串](https://leetcode.cn/problems/wtcaE1/) | 🟠
+| - | [剑指 Offer II 017. 含有所有字符的最短字符串](https://leetcode.cn/problems/M1oyTv/) | 🔴
+
+**-----------**
+
+> 本文有视频版:[滑动窗口算法核心模板框架](https://www.bilibili.com/video/BV1AV4y1n7Zt/)
+
+鉴于前文 [二分搜索框架详解](https://labuladong.github.io/article/fname.html?fname=二分查找详解) 的那首《二分搜索升天词》很受好评,并在民间广为流传,成为安睡助眠的一剂良方,今天在滑动窗口算法框架中,我再次编写一首小诗来歌颂滑动窗口算法的伟大(手动狗头):
+
+![](https://labuladong.github.io/algo/images/slidingwindow/poem.jpg)
+
+哈哈,我自己快把自己夸上天了,大家乐一乐就好,不要当真:)
+
+关于双指针的快慢指针和左右指针的用法,可以参见前文 [双指针技巧汇总](https://labuladong.github.io/article/fname.html?fname=双指针技巧),本文就解决一类最难掌握的双指针技巧:滑动窗口技巧。总结出一套框架,可以保你闭着眼睛都能写出正确的解法。
+
+说起滑动窗口算法,很多读者都会头疼。这个算法技巧的思路非常简单,就是维护一个窗口,不断滑动,然后更新答案么。LeetCode 上有起码 10 道运用滑动窗口算法的题目,难度都是中等和困难。该算法的大致逻辑如下:
+
+```cpp
+int left = 0, right = 0;
+
+while (right < s.size()) {
+ // 增大窗口
+ window.add(s[right]);
+ right++;
+
+ while (window needs shrink) {
+ // 缩小窗口
+ window.remove(s[left]);
+ left++;
+ }
+}
+```
+
+这个算法技巧的时间复杂度是 O(N),比字符串暴力算法要高效得多。
+
+其实困扰大家的,不是算法的思路,而是各种细节问题。比如说如何向窗口中添加新元素,如何缩小窗口,在窗口滑动的哪个阶段更新结果。即便你明白了这些细节,也容易出 bug,找 bug 还不知道怎么找,真的挺让人心烦的。
+
+**所以今天我就写一套滑动窗口算法的代码框架,我连再哪里做输出 debug 都给你写好了,以后遇到相关的问题,你就默写出来如下框架然后改三个地方就行,还不会出 bug**:
+
+```cpp
+/* 滑动窗口算法框架 */
+void slidingWindow(string s) {
+ unordered_map window;
+
+ int left = 0, right = 0;
+ while (right < s.size()) {
+ // c 是将移入窗口的字符
+ char c = s[right];
+ // 增大窗口
+ right++;
+ // 进行窗口内数据的一系列更新
+ ...
+
+ /*** debug 输出的位置 ***/
+ printf("window: [%d, %d)\n", left, right);
+ /********************/
+
+ // 判断左侧窗口是否要收缩
+ while (window needs shrink) {
+ // d 是将移出窗口的字符
+ char d = s[left];
+ // 缩小窗口
+ left++;
+ // 进行窗口内数据的一系列更新
+ ...
+ }
+ }
+}
+```
+
+**其中两处 `...` 表示的更新窗口数据的地方,到时候你直接往里面填就行了**。
+
+而且,这两个 `...` 处的操作分别是扩大和缩小窗口的更新操作,等会你会发现它们操作是完全对称的。
+
+另外,虽然滑动窗口代码框架中有一个嵌套的 while 循环,但算法的时间复杂度依然是 `O(N)`,其中 `N` 是输入字符串/数组的长度。
+
+为什么呢?简单说,字符串/数组中的每个元素都只会进入窗口一次,然后被移出窗口一次,不会说有某些元素多次进入和离开窗口,所以算法的时间复杂度就和字符串/数组的长度成正比。前文 [算法时空复杂度分析实用指南](https://labuladong.github.io/article/fname.html?fname=时间复杂度) 有具体讲时间复杂度的估算,这里就不展开了。
+
+说句题外话,我发现很多人喜欢执着于表象,不喜欢探求问题的本质。比如说有很多人评论我这个框架,说什么散列表速度慢,不如用数组代替散列表;还有很多人喜欢把代码写得特别短小,说我这样代码太多余,影响编译速度,LeetCode 上速度不够快。
+
+我的意见是,算法主要看时间复杂度,你能确保自己的时间复杂度最优就行了。至于 LeetCode 所谓的运行速度,那个都是玄学,只要不是慢的离谱就没啥问题,根本不值得你从编译层面优化,不要舍本逐末……
+
+我的公众号重点在于算法思想,你把框架思维了然于心,然后随你魔改代码好吧,你高兴就好。
+
+言归正传,下面就直接上**四道**力扣原题来套这个框架,其中第一道题会详细说明其原理,后面四道就直接闭眼睛秒杀了。
+
+因为滑动窗口很多时候都是在处理字符串相关的问题,而 Java 处理字符串不方便,所以本文代码为 C++ 实现。不会用到什么编程语言层面的奇技淫巧,但是还是简单介绍一下一些用到的数据结构,以免有的读者因为语言的细节问题阻碍对算法思想的理解:
+
+`unordered_map` 就是哈希表(字典),相当于 Java 的 `HashMap`,它的一个方法 `count(key)` 相当于 Java 的 `containsKey(key)` 可以判断键 key 是否存在。
+
+可以使用方括号访问键对应的值 `map[key]`。需要注意的是,如果该 `key` 不存在,C++ 会自动创建这个 key,并把 `map[key]` 赋值为 0。所以代码中多次出现的 `map[key]++` 相当于 Java 的 `map.put(key, map.getOrDefault(key, 0) + 1)`。
+
+另外,Java 中的 Integer 和 String 这种包装类不能直接用 `==` 进行相等判断,而应该使用类的 `equals` 方法,这个语言特性坑了不少读者,在代码部分我会给出具体提示。
+
+### 一、最小覆盖子串
+
+先来看看力扣第 76 题「最小覆盖子串」难度 Hard:
+
+![](https://labuladong.github.io/algo/images/slidingwindow/title1.png)
+
+就是说要在 `S`(source) 中找到包含 `T`(target) 中全部字母的一个子串,且这个子串一定是所有可能子串中最短的。
+
+如果我们使用暴力解法,代码大概是这样的:
+
+```java
+for (int i = 0; i < s.size(); i++)
+ for (int j = i + 1; j < s.size(); j++)
+ if s[i:j] 包含 t 的所有字母:
+ 更新答案
+```
+
+思路很直接,但是显然,这个算法的复杂度肯定大于 O(N^2) 了,不好。
+
+**滑动窗口算法的思路是这样**:
+
+1、我们在字符串 `S` 中使用双指针中的左右指针技巧,初始化 `left = right = 0`,把索引**左闭右开**区间 `[left, right)` 称为一个「窗口」。
+
+> PS:理论上你可以设计两端都开或者两端都闭的区间,但设计为左闭右开区间是最方便处理的。因为这样初始化 `left = right = 0` 时区间 `[0, 0)` 中没有元素,但只要让 `right` 向右移动(扩大)一位,区间 `[0, 1)` 就包含一个元素 `0` 了。如果你设置为两端都开的区间,那么让 `right` 向右移动一位后开区间 `(0, 1)` 仍然没有元素;如果你设置为两端都闭的区间,那么初始区间 `[0, 0]` 就包含了一个元素。这两种情况都会给边界处理带来不必要的麻烦。
+
+2、我们先不断地增加 `right` 指针扩大窗口 `[left, right)`,直到窗口中的字符串符合要求(包含了 `T` 中的所有字符)。
+
+3、此时,我们停止增加 `right`,转而不断增加 `left` 指针缩小窗口 `[left, right)`,直到窗口中的字符串不再符合要求(不包含 `T` 中的所有字符了)。同时,每次增加 `left`,我们都要更新一轮结果。
+
+4、重复第 2 和第 3 步,直到 `right` 到达字符串 `S` 的尽头。
+
+这个思路其实也不难,**第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解**,也就是最短的覆盖子串。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动,这就是「滑动窗口」这个名字的来历。
+
+下面画图理解一下,`needs` 和 `window` 相当于计数器,分别记录 `T` 中字符出现次数和「窗口」中的相应字符的出现次数。
+
+初始状态:
+
+![](https://labuladong.github.io/algo/images/slidingwindow/1.png)
+
+增加 `right`,直到窗口 `[left, right)` 包含了 `T` 中所有字符:
+
+![](https://labuladong.github.io/algo/images/slidingwindow/2.png)
+
+现在开始增加 `left`,缩小窗口 `[left, right)`:
+
+![](https://labuladong.github.io/algo/images/slidingwindow/3.png)
+
+直到窗口中的字符串不再符合要求,`left` 不再继续移动:
+
+![](https://labuladong.github.io/algo/images/slidingwindow/4.png)
+
+之后重复上述过程,先移动 `right`,再移动 `left`…… 直到 `right` 指针到达字符串 `S` 的末端,算法结束。
+
+如果你能够理解上述过程,恭喜,你已经完全掌握了滑动窗口算法思想。**现在我们来看看这个滑动窗口代码框架怎么用**:
+
+首先,初始化 `window` 和 `need` 两个哈希表,记录窗口中的字符和需要凑齐的字符:
+
+```cpp
+unordered_map need, window;
+for (char c : t) need[c]++;
+```
+
+然后,使用 `left` 和 `right` 变量初始化窗口的两端,不要忘了,区间 `[left, right)` 是左闭右开的,所以初始情况下窗口没有包含任何元素:
+
+```cpp
+int left = 0, right = 0;
+int valid = 0;
+while (right < s.size()) {
+ // 开始滑动
+}
+```
+
+**其中 `valid` 变量表示窗口中满足 `need` 条件的字符个数**,如果 `valid` 和 `need.size` 的大小相同,则说明窗口已满足条件,已经完全覆盖了串 `T`。
+
+**现在开始套模板,只需要思考以下几个问题**:
+
+1、什么时候应该移动 `right` 扩大窗口?窗口加入字符时,应该更新哪些数据?
+
+2、什么时候窗口应该暂停扩大,开始移动 `left` 缩小窗口?从窗口移出字符时,应该更新哪些数据?
+
+3、我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?
+
+如果一个字符进入窗口,应该增加 `window` 计数器;如果一个字符将移出窗口的时候,应该减少 `window` 计数器;当 `valid` 满足 `need` 时应该收缩窗口;应该在收缩窗口的时候更新最终结果。
+
+下面是完整代码:
+
+```cpp
+string minWindow(string s, string t) {
+ unordered_map need, window;
+ for (char c : t) need[c]++;
+
+ int left = 0, right = 0;
+ int valid = 0;
+ // 记录最小覆盖子串的起始索引及长度
+ int start = 0, len = INT_MAX;
+ while (right < s.size()) {
+ // c 是将移入窗口的字符
+ char c = s[right];
+ // 扩大窗口
+ right++;
+ // 进行窗口内数据的一系列更新
+ if (need.count(c)) {
+ window[c]++;
+ if (window[c] == need[c])
+ valid++;
+ }
+
+ // 判断左侧窗口是否要收缩
+ while (valid == need.size()) {
+ // 在这里更新最小覆盖子串
+ if (right - left < len) {
+ start = left;
+ len = right - left;
+ }
+ // d 是将移出窗口的字符
+ char d = s[left];
+ // 缩小窗口
+ left++;
+ // 进行窗口内数据的一系列更新
+ if (need.count(d)) {
+ if (window[d] == need[d])
+ valid--;
+ window[d]--;
+ }
+ }
+ }
+ // 返回最小覆盖子串
+ return len == INT_MAX ?
+ "" : s.substr(start, len);
+}
+```
+
+> PS:使用 Java 的读者要尤其警惕语言特性的陷阱。Java 的 Integer,String 等类型判定相等应该用 `equals` 方法而不能直接用等号 `==`,这是 Java 包装类的一个隐晦细节。所以在缩小窗口更新数据的时候,不能直接改写为 `window.get(d) == need.get(d)`,而要用 `window.get(d).equals(need.get(d))`,之后的题目代码同理。
+
+需要注意的是,当我们发现某个字符在 `window` 的数量满足了 `need` 的需要,就要更新 `valid`,表示有一个字符已经满足要求。而且,你能发现,两次对窗口内数据的更新操作是完全对称的。
+
+当 `valid == need.size()` 时,说明 `T` 中所有字符已经被覆盖,已经得到一个可行的覆盖子串,现在应该开始收缩窗口了,以便得到「最小覆盖子串」。
+
+移动 `left` 收缩窗口时,窗口内的字符都是可行解,所以应该在收缩窗口的阶段进行最小覆盖子串的更新,以便从可行解中找到长度最短的最终结果。
+
+至此,应该可以完全理解这套框架了,滑动窗口算法又不难,就是细节问题让人烦得很。**以后遇到滑动窗口算法,你就按照这框架写代码,保准没有 bug,还省事儿**。
+
+下面就直接利用这套框架秒杀几道题吧,你基本上一眼就能看出思路了。
+
+### 二、字符串排列
+
+这是力扣第 567 题「字符串的排列」,难度中等:
+
+![](https://labuladong.github.io/algo/images/slidingwindow/title2.png)
+
+注意哦,输入的 `s1` 是可以包含重复字符的,所以这个题难度不小。
+
+这种题目,是明显的滑动窗口算法,**相当给你一个 `S` 和一个 `T`,请问你 `S` 中是否存在一个子串,包含 `T` 中所有字符且不包含其他字符**?
+
+首先,先复制粘贴之前的算法框架代码,然后明确刚才提出的几个问题,即可写出这道题的答案:
+
+```cpp
+// 判断 s 中是否存在 t 的排列
+bool checkInclusion(string t, string s) {
+ unordered_map need, window;
+ for (char c : t) need[c]++;
+
+ int left = 0, right = 0;
+ int valid = 0;
+ while (right < s.size()) {
+ char c = s[right];
+ right++;
+ // 进行窗口内数据的一系列更新
+ if (need.count(c)) {
+ window[c]++;
+ if (window[c] == need[c])
+ valid++;
+ }
+
+ // 判断左侧窗口是否要收缩
+ while (right - left >= t.size()) {
+ // 在这里判断是否找到了合法的子串
+ if (valid == need.size())
+ return true;
+ char d = s[left];
+ left++;
+ // 进行窗口内数据的一系列更新
+ if (need.count(d)) {
+ if (window[d] == need[d])
+ valid--;
+ window[d]--;
+ }
+ }
+ }
+ // 未找到符合条件的子串
+ return false;
+}
+```
+
+对于这道题的解法代码,基本上和最小覆盖子串一模一样,只需要改变几个地方:
+
+1、本题移动 `left` 缩小窗口的时机是窗口大小大于 `t.size()` 时,应为排列嘛,显然长度应该是一样的。
+
+2、当发现 `valid == need.size()` 时,就说明窗口中就是一个合法的排列,所以立即返回 `true`。
+
+至于如何处理窗口的扩大和缩小,和最小覆盖子串完全相同。
+
+> PS:由于这道题中 `[left, right)` 其实维护的是一个**定长**的窗口,窗口大小为 `t.size()`。因为定长窗口每次向前滑动时只会移出一个字符,所以可以把内层的 while 改成 if,效果是一样的。
+
+### 三、找所有字母异位词
+
+这是力扣第 438 题「找到字符串中所有字母异位词」,难度中等:
+
+![](https://labuladong.github.io/algo/images/slidingwindow/title3.png)
+
+呵呵,这个所谓的字母异位词,不就是排列吗,搞个高端的说法就能糊弄人了吗?**相当于,输入一个串 `S`,一个串 `T`,找到 `S` 中所有 `T` 的排列,返回它们的起始索引**。
+
+直接默写一下框架,明确刚才讲的 4 个问题,即可秒杀这道题:
+
+```cpp
+vector findAnagrams(string s, string t) {
+ unordered_map need, window;
+ for (char c : t) need[c]++;
+
+ int left = 0, right = 0;
+ int valid = 0;
+ vector res; // 记录结果
+ while (right < s.size()) {
+ char c = s[right];
+ right++;
+ // 进行窗口内数据的一系列更新
+ if (need.count(c)) {
+ window[c]++;
+ if (window[c] == need[c])
+ valid++;
+ }
+ // 判断左侧窗口是否要收缩
+ while (right - left >= t.size()) {
+ // 当窗口符合条件时,把起始索引加入 res
+ if (valid == need.size())
+ res.push_back(left);
+ char d = s[left];
+ left++;
+ // 进行窗口内数据的一系列更新
+ if (need.count(d)) {
+ if (window[d] == need[d])
+ valid--;
+ window[d]--;
+ }
+ }
+ }
+ return res;
+}
+```
+
+跟寻找字符串的排列一样,只是找到一个合法异位词(排列)之后将起始索引加入 `res` 即可。
+
+### 四、最长无重复子串
+
+这是力扣第 3 题「无重复字符的最长子串」,难度中等:
+
+![](https://labuladong.github.io/algo/images/slidingwindow/title4.png)
+
+这个题终于有了点新意,不是一套框架就出答案,不过反而更简单了,稍微改一改框架就行了:
+
+```cpp
+int lengthOfLongestSubstring(string s) {
+ unordered_map window;
+
+ int left = 0, right = 0;
+ int res = 0; // 记录结果
+ while (right < s.size()) {
+ char c = s[right];
+ right++;
+ // 进行窗口内数据的一系列更新
+ window[c]++;
+ // 判断左侧窗口是否要收缩
+ while (window[c] > 1) {
+ char d = s[left];
+ left++;
+ // 进行窗口内数据的一系列更新
+ window[d]--;
+ }
+ // 在这里更新答案
+ res = max(res, right - left);
+ }
+ return res;
+}
+```
+
+这就是变简单了,连 `need` 和 `valid` 都不需要,而且更新窗口内数据也只需要简单的更新计数器 `window` 即可。
+
+当 `window[c]` 值大于 1 时,说明窗口中存在重复字符,不符合条件,就该移动 `left` 缩小窗口了嘛。
+
+唯一需要注意的是,在哪里更新结果 `res` 呢?我们要的是最长无重复子串,哪一个阶段可以保证窗口中的字符串是没有重复的呢?
+
+这里和之前不一样,要在收缩窗口完成后更新 `res`,因为窗口收缩的 while 条件是存在重复元素,换句话说收缩完成后一定保证窗口中没有重复嘛。
+
+好了,滑动窗口算法模板就讲到这里,希望大家能理解其中的思想,记住算法模板并融会贯通。回顾一下,遇到子数组/子串相关的问题,你只要能回答出来以下几个问题,就能运用滑动窗口算法:
+
+1、什么时候应该扩大窗口?
+
+2、什么时候应该缩小窗口?
+
+3、什么时候得到一个合法的答案?
+
+我在 [滑动窗口经典习题](https://appktavsiei5995.pc.xiaoe-tech.com/detail/i_62b57985e4b00a4f371dd705/1) 中使用这套思维模式列举了更多经典的习题,旨在强化你对算法的理解和记忆,以后就再也不怕子串、子数组问题了。
+
+
+
+
+
+引用本文的文章
+
+ - [分治算法详解:运算优先级](https://labuladong.github.io/article/fname.html?fname=分治算法)
+ - [动态规划设计:最大子数组](https://labuladong.github.io/article/fname.html?fname=最大子数组)
+ - [单调队列的通用实现及经典习题](https://appktavsiei5995.pc.xiaoe-tech.com/detail/i_62a692efe4b01a48520b9b9b/1)
+ - [单调队列结构解决滑动窗口问题](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=算法心得)
+ - [滑动窗口算法延伸:Rabin Karp 字符匹配算法](https://labuladong.github.io/article/fname.html?fname=rabinkarp)
+ - [滑动窗口算法经典习题](https://appktavsiei5995.pc.xiaoe-tech.com/detail/i_62b57985e4b00a4f371dd705/1)
+ - [算法时空复杂度分析实用指南](https://labuladong.github.io/article/fname.html?fname=时间复杂度)
+
+
+
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| [1004. Max Consecutive Ones III](https://leetcode.com/problems/max-consecutive-ones-iii/?show=1) | [1004. 最大连续1的个数 III](https://leetcode.cn/problems/max-consecutive-ones-iii/?show=1) |
+| [1438. Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit](https://leetcode.com/problems/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit/?show=1) | [1438. 绝对差不超过限制的最长连续子数组](https://leetcode.cn/problems/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit/?show=1) |
+| [1658. Minimum Operations to Reduce X to Zero](https://leetcode.com/problems/minimum-operations-to-reduce-x-to-zero/?show=1) | [1658. 将 x 减到 0 的最小操作数](https://leetcode.cn/problems/minimum-operations-to-reduce-x-to-zero/?show=1) |
+| [209. Minimum Size Subarray Sum](https://leetcode.com/problems/minimum-size-subarray-sum/?show=1) | [209. 长度最小的子数组](https://leetcode.cn/problems/minimum-size-subarray-sum/?show=1) |
+| [219. Contains Duplicate II](https://leetcode.com/problems/contains-duplicate-ii/?show=1) | [219. 存在重复元素 II](https://leetcode.cn/problems/contains-duplicate-ii/?show=1) |
+| [220. Contains Duplicate III](https://leetcode.com/problems/contains-duplicate-iii/?show=1) | [220. 存在重复元素 III](https://leetcode.cn/problems/contains-duplicate-iii/?show=1) |
+| [395. Longest Substring with At Least K Repeating Characters](https://leetcode.com/problems/longest-substring-with-at-least-k-repeating-characters/?show=1) | [395. 至少有 K 个重复字符的最长子串](https://leetcode.cn/problems/longest-substring-with-at-least-k-repeating-characters/?show=1) |
+| [424. Longest Repeating Character Replacement](https://leetcode.com/problems/longest-repeating-character-replacement/?show=1) | [424. 替换后的最长重复字符](https://leetcode.cn/problems/longest-repeating-character-replacement/?show=1) |
+| [713. Subarray Product Less Than K](https://leetcode.com/problems/subarray-product-less-than-k/?show=1) | [713. 乘积小于K的子数组](https://leetcode.cn/problems/subarray-product-less-than-k/?show=1) |
+| [862. Shortest Subarray with Sum at Least K](https://leetcode.com/problems/shortest-subarray-with-sum-at-least-k/?show=1) | [862. 和至少为 K 的最短子数组](https://leetcode.cn/problems/shortest-subarray-with-sum-at-least-k/?show=1) |
+| - | [剑指 Offer 48. 最长不含重复字符的子字符串](https://leetcode.cn/problems/zui-chang-bu-han-zhong-fu-zi-fu-de-zi-zi-fu-chuan-lcof/?show=1) |
+| - | [剑指 Offer 57 - II. 和为s的连续正数序列](https://leetcode.cn/problems/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof/?show=1) |
+| - | [剑指 Offer II 009. 乘积小于 K 的子数组](https://leetcode.cn/problems/ZVAVXX/?show=1) |
+| - | [剑指 Offer II 014. 字符串中的变位词](https://leetcode.cn/problems/MPnaiL/?show=1) |
+| - | [剑指 Offer II 015. 字符串中的所有变位词](https://leetcode.cn/problems/VabMRr/?show=1) |
+| - | [剑指 Offer II 016. 不含重复字符的最长子字符串](https://leetcode.cn/problems/wtcaE1/?show=1) |
+| - | [剑指 Offer II 017. 含有所有字符的最短字符串](https://leetcode.cn/problems/M1oyTv/?show=1) |
+| - | [剑指 Offer II 057. 值和下标之差都在给定的范围内](https://leetcode.cn/problems/7WqeDu/?show=1) |
+
+
+
+
+
+**_____________**
+
+**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
+
+![](https://labuladong.github.io/algo/images/souyisou2.png)
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\203\247\351\245\274\346\216\222\345\272\217.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\203\247\351\245\274\346\216\222\345\272\217.md"
index 8fb40940b8983bb89b0b578f88fdf845f8eefea8..ee418b2d006c7b8c87756465039e957e55ccef0f 100644
--- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\203\247\351\245\274\346\216\222\345\272\217.md"
+++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\203\247\351\245\274\346\216\222\345\272\217.md"
@@ -1,9 +1,5 @@
# 烧饼排序
-
-
-
-
@@ -13,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -146,6 +142,10 @@ void reverse(int[] arr, int i, int j) {
不妨分享一下你的思考。
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\256\227\346\263\225\345\255\246\344\271\240\344\271\213\350\267\257.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\256\227\346\263\225\345\255\246\344\271\240\344\271\213\350\267\257.md"
deleted file mode 100644
index 40fe42f94cd3fcde10a490c535b8cdfb127b4445..0000000000000000000000000000000000000000
--- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\256\227\346\263\225\345\255\246\344\271\240\344\271\213\350\267\257.md"
+++ /dev/null
@@ -1,105 +0,0 @@
-# 算法学习之路
-
-
-
-
-
-
-
-
-
-![](../pictures/souyisou.png)
-
-**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
-
-**-----------**
-
-之前发的那篇关于框架性思维的文章,我也发到了不少其他圈子,受到了大家的普遍好评,这一点我真的没想到,首先感谢大家的认可,我会更加努力,写出通俗易懂的算法文章。
-
-有很多朋友问我数据结构和算法到底该怎么学,尤其是很多朋友说自己是「小白」,感觉这些东西好难啊,就算看了之前的「框架思维」,也感觉自己刷题乏力,希望我能聊聊我从一个非科班小白一路是怎么学过来的。
-
-首先要给怀有这样疑问的朋友鼓掌,因为你现在已经「知道自己不知道」,而且开始尝试学习、刷题、寻求帮助,能做到这一点本身就是及其困难的。
-
-关于「框架性思维」,对于一个小白来说,可能暂时无法完全理解(如果你能理解,说明你水平已经不错啦,不是小白啦)。就像软件工程,对于我这种没带过项目的人来说,感觉其内容枯燥乏味,全是废话,但是对于一个带过团队的人,他就会觉得软件工程里的每一句话都是精华。暂时不太理解没关系,留个印象,功夫到了很快就明白了。
-
-下面写一写我一路过来的一些经验。如果你已经看过很多「如何高效刷题」「如何学习算法」的文章,却还是没有开始行动并坚持下去,本文的第五点就是写给你的。
-
-我觉得之所以有时候认为自己是「小白」,是由于知识某些方面的空白造成的。具体到数据结构的学习,无非就是两个问题搞得不太清楚:**这是啥?有啥用?**
-
-举个例子,比如说你看到了「栈」这个名词,老师可能会讲这些关键词:先进后出、函数堆栈等等。但是,对于初学者,这些描述属于文学词汇,没有实际价值,没有解决最基本的两个问题。如何回答这两个基本问题呢?回答「这是啥」需要看教科书,回答「有啥用」需要刷算法题。
-
-**一、这是啥?**
-
-这个问题最容易解决,就像一层窗户纸,你只要随便找本书看两天,自己动手实现一个「队列」「栈」之类的数据结构,就能捅破这层窗户纸。
-
-这时候你就能理解「框架思维」文章中的前半部分了:数据结构无非就是数组、链表为骨架的一些特定操作而已;每个数据结构实现的功能无非增删查改罢了。
-
-比如说「列队」这个数据结构,无非就是基于数组或者链表,实现 enqueue 和 dequeue 两个方法。这两个方法就是增和删呀,连查和改的方法都不需要。
-
-**二、有啥用?**
-
-解决这个问题,就涉及算法的设计了,是个持久战,需要经常进行抽象思考,刷算法题,培养「计算机思维」。
-
-之前的文章讲了,算法就是对数据结构准确而巧妙的运用。常用算法问题也就那几大类,算法题无非就是不断变换场景,给那几个算法框架套上不同的皮。刷题,就是在锻炼你的眼力,看你能不能看穿问题表象揪出相应的解法框架。
-
-比如说,让你求解一个迷宫,你要把这个问题层层抽象:迷宫 -> 图的遍历 -> N 叉树的遍历 -> 二叉树的遍历。然后让框架指导你写具体的解法。
-
-抽象问题,直击本质,是刷题中你需要刻意培养的能力。
-
-**三、如何看书**
-
-直接推荐一本公认的好书,《算法第 4 版》,我一般简写成《算法4》。不要蜻蜓点水,这本书你能选择性的看上 50%,基本上就达到平均水平了。别怕这本书厚,因为起码有三分之一不用看,下面讲讲怎么看这本书。
-
-看书仍然遵循递归的思想:自顶向下,逐步求精。
-
-这本书知识结构合理,讲解也清楚,所以可以按顺序学习。**书中正文的算法代码一定要亲自敲一遍**,因为这些真的是扎实的基础,要认真理解。不要以为自己看一遍就看懂了,不动手的话理解不了的。但是,开头部分的基础可以酌情跳过;书中的数学证明,如不影响对算法本身的理解,完全可以跳过;章节最后的练习题,也可以全部跳过。这样一来,这本书就薄了很多。
-
-相信读者现在已经认可了「框架性思维」的重要性,这种看书方式也是一种框架性策略,抓大放小,着重理解整体的知识架构,而忽略证明、练习题这种细节问题,即**保持自己对新知识的好奇心,避免陷入无限的细节被劝退。**
-
-当然,《算法4》到后面的内容也比较难了,比如那几个著名的串算法,以及正则表达式算法。这些属于「经典算法」,看个人接受能力吧,单说刷 LeetCode 的话,基本用不上,量力而行即可。
-
-**四、如何刷题**
-
-首先声明一下,**算法和数学水平没关系,和编程语言也没关系**,你爱用什么语言用什么。算法,主要是培养一种新的思维方式。所谓「计算机思维」,就跟你考驾照一样,你以前骑自行车,有一套自行车的规则和技巧,现在你开汽车,就需要适应并练习开汽车的规则和技巧。
-
-LeetCode 上的算法题和前面说的「经典算法」不一样,我们权且称为「解闷算法」吧,因为很多题目都比较有趣,有种在做奥数题或者脑筋急转弯的感觉。比如说,让你用队列实现一个栈,或者用栈实现一个队列,以及不用加号做加法,开脑洞吧?
-
-当然,这些问题虽然看起来无厘头,实际生活中也用不到,但是想解决这些问题依然要靠数据结构以及对基础知识的理解,也许这就是很多公司面试都喜欢出这种「智力题」的原因。下面说几点技巧吧。
-
-**尽量刷英文版的 LeetCode**,中文版的“力扣”是阉割版,不仅很多题目没有答案,而且连个讨论区都没有。英文版的是真的很良心了,很多问题都有官方解答,详细易懂。而且讨论区(Discuss)也沉淀了大量优质内容,甚至好过官方解答。真正能打开你思路的,很可能是讨论区各路大神的思路荟萃。
-
-PS:**如果有的英文题目实在看不懂,有个小技巧**,你在题目页面的 url 里加一个 -cn,即 https://leetcode.com/xxx 改成 https://leetcode-cn.com/xxx,这样就能切换到相应的中文版页面查看。
-
-对于初学者,**强烈建议从 Explore 菜单里最下面的 Learn 开始刷**,这个专题就是专门教你学习数据结构和基本算法的,教学篇和相应的练习题结合,不要太良心。
-
-最近 Learn 专题里新增了一些内容,我们挑数据结构相关的内容刷就行了,像 Ruby,Machine Learning 就没必要刷了。刷完 Learn 专题的基础内容,基本就有能力去 Explore 菜单的 Interview 专题刷面试题,或者去 Problem 菜单,在真正的题海里遨游了。
-
-无论刷 Explore 还是 Problems 菜单,**最好一个分类一个分类的刷,不要蜻蜓点水**。比如说这几天就刷链表,刷完链表再去连刷几天二叉树。这样做是为了帮助你提取「框架」。一旦总结出针对一类问题的框架,解决同类问题可谓是手到擒来。
-
-**五、道理我都懂,还是不能坚持下去**
-
-这其实无关算法了,还是老生常谈的执行力的问题。不说什么破鸡汤了,我觉得**解决办法就是「激起欲望」**,注意我说的是欲望,而不是常说的兴趣,拿我自己说说吧。
-
-半年前我开始刷题,目的和大部分人都一样的,就是为毕业找工作做准备。只不过,大部分人是等到临近毕业了才开始刷,而我离毕业还有一阵子。这不是炫耀我多有觉悟,而是我承认自己的极度平凡。
-
-首先,我真的想找到一份不错的工作(谁都想吧?),我想要高薪呀!否则我在朋友面前,女神面前放下的骚话,最终都会反过来啪啪地打我的脸。我也是要恰饭,要面子,要虚荣心的嘛。赚钱,虚荣心,足以激起我的欲望了。
-
-但是,我不擅长 deadline 突击,我理解东西真的慢,所以干脆笨鸟先飞了。智商不够,拿时间来补,我没能力两个月突击,干脆拉长战线,打他个两年游击战,我还不信耗不死算法这个强敌。事实证明,你如果认真学习一个月,就能够取得肉眼可见的进步了。
-
-现在,我依然在坚持刷题,而且为了另外一个原因,这个公众号。我没想到自己的文字竟然能够帮助到他人,甚至能得到认可。这也是虚荣心啊,我不能让读者失望啊,我想让更多的人认可(夸)我呀!
-
-以上,不光是坚持刷算法题吧,很多场景都适用。执行力是要靠「欲望」支撑的,我也是一凡人,只有那些看得见摸得着的东西才能使我快乐呀。读者不妨也尝试把刷题学习和自己的切身利益联系起来,这恐怕是坚持下去最简单直白的理由了。
-
-
-
-**_____________**
-
-**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**。
-
-**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。
-
-
-
-
-
-======其他语言代码======
\ No newline at end of file
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\350\212\261\345\274\217\351\201\215\345\216\206.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\350\212\261\345\274\217\351\201\215\345\216\206.md"
new file mode 100644
index 0000000000000000000000000000000000000000..b370ee1c84d9c6d1c885de8e3b3d3db386ebc074
--- /dev/null
+++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\350\212\261\345\274\217\351\201\215\345\216\206.md"
@@ -0,0 +1,331 @@
+# 二维数组的花式遍历
+
+
+
+
+
+
+
+
+![](https://labuladong.github.io/algo/images/souyisou1.png)
+
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
+
+
+
+读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
+
+| LeetCode | 力扣 | 难度 |
+| :----: | :----: | :----: |
+| [151. Reverse Words in a String](https://leetcode.com/problems/reverse-words-in-a-string/) | [151. 翻转字符串里的单词](https://leetcode.cn/problems/reverse-words-in-a-string/) | 🟠
+| [48. Rotate Image](https://leetcode.com/problems/rotate-image/) | [48. 旋转图像](https://leetcode.cn/problems/rotate-image/) | 🟠
+| [54. Spiral Matrix](https://leetcode.com/problems/spiral-matrix/) | [54. 螺旋矩阵](https://leetcode.cn/problems/spiral-matrix/) | 🟠
+| [59. Spiral Matrix II](https://leetcode.com/problems/spiral-matrix-ii/) | [59. 螺旋矩阵 II](https://leetcode.cn/problems/spiral-matrix-ii/) | 🟠
+| - | [剑指 Offer 29. 顺时针打印矩阵](https://leetcode.cn/problems/shun-shi-zhen-da-yin-ju-zhen-lcof/) | 🟢
+
+**-----------**
+
+有不少读者说,看过很多公众号历史文章之后,掌握了框架思维,可以解决大部分有套路框架可循的题目。
+
+但是框架思维也不是万能的,有一些特定技巧呢,属于会者不难,难者不会的类型,只能通过多刷题进行总结和积累。
+
+那么本文我分享一些巧妙的二维数组的花式操作,你只要有个印象,以后遇到类似题目就不会懵圈了。
+
+### 顺/逆时针旋转矩阵
+
+对二维数组进行旋转是常见的笔试题,力扣第 48 题「旋转图像」就是很经典的一道:
+
+![](https://labuladong.github.io/algo/images/花式遍历/title.png)
+
+题目很好理解,就是让你将一个二维矩阵顺时针旋转 90 度,**难点在于要「原地」修改**,函数签名如下:
+
+```java
+void rotate(int[][] matrix)
+```
+
+如何「原地」旋转二维矩阵?稍想一下,感觉操作起来非常复杂,可能要设置巧妙的算法机制来「一圈一圈」旋转矩阵:
+
+![](https://labuladong.github.io/algo/images/花式遍历/1.png)
+
+**但实际上,这道题不能走寻常路**,在讲巧妙解法之前,我们先看另一道谷歌曾经考过的算法题热热身:
+
+给你一个包含若干单词和空格的字符串 `s`,请你写一个算法,**原地**反转所有单词的顺序。
+
+比如说,给你输入这样一个字符串:
+
+```shell
+s = "hello world labuladong"
+```
+
+你的算法需要**原地**反转这个字符串中的单词顺序:
+
+```shell
+s = "labuladong world hello"
+```
+
+常规的方式是把 `s` 按空格 `split` 成若干单词,然后 `reverse` 这些单词的顺序,最后把这些单词 `join` 成句子。但这种方式使用了额外的空间,并不是「原地反转」单词。
+
+**正确的做法是,先将整个字符串 `s` 反转**:
+
+```shell
+s = "gnodalubal dlrow olleh"
+```
+
+**然后将每个单词分别反转**:
+
+```shell
+s = "labuladong world hello"
+```
+
+这样,就实现了原地反转所有单词顺序的目的。力扣第 151 题「颠倒字符串中的单词」就是类似的问题,你可以顺便去做一下。
+
+我讲这道题的目的是什么呢?
+
+**旨在说明,有时候咱们拍脑袋的常规思维,在计算机看来可能并不是最优雅的;但是计算机觉得最优雅的思维,对咱们来说却不那么直观**。也许这就是算法的魅力所在吧。
+
+回到之前说的顺时针旋转二维矩阵的问题,常规的思路就是去寻找原始坐标和旋转后坐标的映射规律,但我们是否可以让思维跳跃跳跃,尝试把矩阵进行反转、镜像对称等操作,可能会出现新的突破口。
+
+**我们可以先将 `n x n` 矩阵 `matrix` 按照左上到右下的对角线进行镜像对称**:
+
+![](https://labuladong.github.io/algo/images/花式遍历/2.jpeg)
+
+**然后再对矩阵的每一行进行反转**:
+
+![](https://labuladong.github.io/algo/images/花式遍历/3.jpeg)
+
+**发现结果就是 `matrix` 顺时针旋转 90 度的结果**:
+
+![](https://labuladong.github.io/algo/images/花式遍历/4.jpeg)
+
+将上述思路翻译成代码,即可解决本题:
+
+```java
+// 将二维矩阵原地顺时针旋转 90 度
+public void rotate(int[][] matrix) {
+ int n = matrix.length;
+ // 先沿对角线镜像对称二维矩阵
+ for (int i = 0; i < n; i++) {
+ for (int j = i; j < n; j++) {
+ // swap(matrix[i][j], matrix[j][i]);
+ int temp = matrix[i][j];
+ matrix[i][j] = matrix[j][i];
+ matrix[j][i] = temp;
+ }
+ }
+ // 然后反转二维矩阵的每一行
+ for (int[] row : matrix) {
+ reverse(row);
+ }
+}
+
+// 反转一维数组
+void reverse(int[] arr) {
+ int i = 0, j = arr.length - 1;
+ while (j > i) {
+ // swap(arr[i], arr[j]);
+ int temp = arr[i];
+ arr[i] = arr[j];
+ arr[j] = temp;
+ i++;
+ j--;
+ }
+}
+```
+
+肯定有读者会问,如果没有做过这道题,怎么可能想到这种思路呢?
+
+仔细想想,旋转二维矩阵的难点在于将「行」变成「列」,将「列」变成「行」,而只有按照对角线的对称操作是可以轻松完成这一点的,对称操作之后就很容易发现规律了。
+
+**既然说道这里,我们可以发散一下,如何将矩阵逆时针旋转 90 度呢**?
+
+思路是类似的,只要通过另一条对角线镜像对称矩阵,然后再反转每一行,就得到了逆时针旋转矩阵的结果:
+
+![](https://labuladong.github.io/algo/images/花式遍历/5.jpeg)
+
+翻译成代码如下:
+
+```java
+// 将二维矩阵原地逆时针旋转 90 度
+void rotate2(int[][] matrix) {
+ int n = matrix.length;
+ // 沿左下到右上的对角线镜像对称二维矩阵
+ for (int i = 0; i < n; i++) {
+ for (int j = 0; j < n - i; j++) {
+ // swap(matrix[i][j], matrix[n-j-1][n-i-1])
+ int temp = matrix[i][j];
+ matrix[i][j] = matrix[n - j - 1][n - i - 1];
+ matrix[n - j - 1][n - i - 1] = temp;
+ }
+ }
+ // 然后反转二维矩阵的每一行
+ for (int[] row : matrix) {
+ reverse(row);
+ }
+}
+
+void reverse(int[] arr) { /* 见上文 */}
+```
+
+至此,旋转矩阵的问题就解决了。
+
+### 矩阵的螺旋遍历
+
+我的公众号动态规划系列文章经常需要遍历二维 `dp` 数组,但难点在于状态转移方程而不是数组的遍历,顶多就是倒序遍历。
+
+但接下来我们讲一下力扣第 54 题「螺旋矩阵」,看一看二维矩阵可以如何花式遍历:
+
+![](https://labuladong.github.io/algo/images/花式遍历/title2.png)
+
+函数签名如下:
+
+```java
+List spiralOrder(int[][] matrix)
+```
+
+**解题的核心思路是按照右、下、左、上的顺序遍历数组,并使用四个变量圈定未遍历元素的边界**:
+
+![](https://labuladong.github.io/algo/images/花式遍历/6.png)
+
+随着螺旋遍历,相应的边界会收缩,直到螺旋遍历完整个数组:
+
+![](https://labuladong.github.io/algo/images/花式遍历/7.png)
+
+只要有了这个思路,翻译出代码就很容易了:
+
+```java
+List spiralOrder(int[][] matrix) {
+ int m = matrix.length, n = matrix[0].length;
+ int upper_bound = 0, lower_bound = m - 1;
+ int left_bound = 0, right_bound = n - 1;
+ List res = new LinkedList<>();
+ // res.size() == m * n 则遍历完整个数组
+ while (res.size() < m * n) {
+ if (upper_bound <= lower_bound) {
+ // 在顶部从左向右遍历
+ for (int j = left_bound; j <= right_bound; j++) {
+ res.add(matrix[upper_bound][j]);
+ }
+ // 上边界下移
+ upper_bound++;
+ }
+
+ if (left_bound <= right_bound) {
+ // 在右侧从上向下遍历
+ for (int i = upper_bound; i <= lower_bound; i++) {
+ res.add(matrix[i][right_bound]);
+ }
+ // 右边界左移
+ right_bound--;
+ }
+
+ if (upper_bound <= lower_bound) {
+ // 在底部从右向左遍历
+ for (int j = right_bound; j >= left_bound; j--) {
+ res.add(matrix[lower_bound][j]);
+ }
+ // 下边界上移
+ lower_bound--;
+ }
+
+ if (left_bound <= right_bound) {
+ // 在左侧从下向上遍历
+ for (int i = lower_bound; i >= upper_bound; i--) {
+ res.add(matrix[i][left_bound]);
+ }
+ // 左边界右移
+ left_bound++;
+ }
+ }
+ return res;
+}
+```
+
+力扣第 59 题「螺旋矩阵 II」也是类似的题目,只不过是反过来,让你按照螺旋的顺序生成矩阵:
+
+![](https://labuladong.github.io/algo/images/花式遍历/title3.png)
+
+函数签名如下:
+
+```java
+int[][] generateMatrix(int n)
+```
+
+有了上面的铺垫,稍微改一下代码即可完成这道题:
+
+```java
+int[][] generateMatrix(int n) {
+ int[][] matrix = new int[n][n];
+ int upper_bound = 0, lower_bound = n - 1;
+ int left_bound = 0, right_bound = n - 1;
+ // 需要填入矩阵的数字
+ int num = 1;
+
+ while (num <= n * n) {
+ if (upper_bound <= lower_bound) {
+ // 在顶部从左向右遍历
+ for (int j = left_bound; j <= right_bound; j++) {
+ matrix[upper_bound][j] = num++;
+ }
+ // 上边界下移
+ upper_bound++;
+ }
+
+ if (left_bound <= right_bound) {
+ // 在右侧从上向下遍历
+ for (int i = upper_bound; i <= lower_bound; i++) {
+ matrix[i][right_bound] = num++;
+ }
+ // 右边界左移
+ right_bound--;
+ }
+
+ if (upper_bound <= lower_bound) {
+ // 在底部从右向左遍历
+ for (int j = right_bound; j >= left_bound; j--) {
+ matrix[lower_bound][j] = num++;
+ }
+ // 下边界上移
+ lower_bound--;
+ }
+
+ if (left_bound <= right_bound) {
+ // 在左侧从下向上遍历
+ for (int i = lower_bound; i >= upper_bound; i--) {
+ matrix[i][left_bound] = num++;
+ }
+ // 左边界右移
+ left_bound++;
+ }
+ }
+ return matrix;
+}
+```
+
+至此,两道螺旋矩阵的题目也解决了。
+
+以上就是遍历二维数组的一些技巧,其他数组技巧可参见之前的文章 [前缀和数组](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://aep.h5.xeknow.com/s/1XJHEO),以视频课为主,手把手带你实现常用的数据结构及相关算法,旨在帮助算法基础较为薄弱的读者深入理解常用数据结构的底层原理,在算法学习中少走弯路。
+
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| [1260. Shift 2D Grid](https://leetcode.com/problems/shift-2d-grid/?show=1) | [1260. 二维网格迁移](https://leetcode.cn/problems/shift-2d-grid/?show=1) |
+
+
+
+
+
+**_____________**
+
+**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
+
+![](https://labuladong.github.io/algo/images/souyisou2.png)
\ No newline at end of file
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\351\200\222\345\275\222\350\257\246\350\247\243.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\351\200\222\345\275\222\350\257\246\350\247\243.md"
deleted file mode 100644
index 4e259d8f93cc2dd5f2f0452592482d4cc0f2bc5d..0000000000000000000000000000000000000000
--- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\351\200\222\345\275\222\350\257\246\350\247\243.md"
+++ /dev/null
@@ -1,351 +0,0 @@
-# 递归详解
-
-
-
-
-
-
-
-
-
-![](../pictures/souyisou.png)
-
-**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
-
-**-----------**
-
-首先说明一个问题,简单阐述一下递归,分治算法,动态规划,贪心算法这几个东西的区别和联系,心里有个印象就好。
-
-递归是一种编程技巧,一种解决问题的思维方式;分治算法和动态规划很大程度上是递归思想基础上的(虽然动态规划的最终版本大都不是递归了,但解题思想还是离不开递归),解决更具体问题的两类算法思想;贪心算法是动态规划算法的一个子集,可以更高效解决一部分更特殊的问题。
-
-分治算法将在这节讲解,以最经典的归并排序为例,它把待排序数组不断二分为规模更小的子问题处理,这就是 “分而治之” 这个词的由来。显然,排序问题分解出的子问题是不重复的,如果有的问题分解后的子问题有重复的(重叠子问题性质),那么就交给动态规划算法去解决!
-
-## 递归详解
-
-介绍分治之前,首先要弄清楚递归这个概念。
-
-递归的基本思想是某个函数直接或者间接地调用自身,这样就把原问题的求解转换为许多性质相同但是规模更小的子问题。我们只需要关注如何把原问题划分成符合条件的子问题,而不需要去研究这个子问题是如何被解决的。递归和枚举的区别在于:枚举是横向地把问题划分,然后依次求解子问题,而递归是把问题逐级分解,是纵向的拆分。
-
-以下会举例说明我对递归的一点理解,**如果你不想看下去了,请记住这几个问题怎么回答:**
-
-1. 如何给一堆数字排序? 答:分成两半,先排左半边再排右半边,最后合并就行了,至于怎么排左边和右边,请重新阅读这句话。
-2. 孙悟空身上有多少根毛? 答:一根毛加剩下的毛。
-3. 你今年几岁? 答:去年的岁数加一岁,1999 年我出生。
-
-递归代码最重要的两个特征:结束条件和自我调用。自我调用是在解决子问题,而结束条件定义了最简子问题的答案。
-
-```c++
-int func(你今年几岁) {
- // 最简子问题,结束条件
- if (你1999年几岁) return 我0岁;
- // 自我调用,缩小规模
- return func(你去年几岁) + 1;
-}
-```
-
-其实仔细想想,**递归运用最成功的是什么?我认为是数学归纳法。**我们高中都学过数学归纳法,使用场景大概是:我们推不出来某个求和公式,但是我们试了几个比较小的数,似乎发现了一点规律,然后编了一个公式,看起来应该是正确答案。但是数学是很严谨的,你哪怕穷举了一万个数都是正确的,但是第一万零一个数正确吗?这就要数学归纳法发挥神威了,可以假设我们编的这个公式在第 k 个数时成立,如果证明在第 k + 1 时也成立,那么我们编的这个公式就是正确的。
-
-那么数学归纳法和递归有什么联系?我们刚才说了,递归代码必须要有结束条件,如果没有的话就会进入无穷无尽的自我调用,直到内存耗尽。而数学证明的难度在于,你可以尝试有穷种情况,但是难以将你的结论延伸到无穷大。这里就可以看出联系了 —— 无穷。
-
-递归代码的精髓在于调用自己去解决规模更小的子问题,直到到达结束条件;而数学归纳法之所以有用,就在于不断把我们的猜测向上加一,扩大结论的规模,没有结束条件,从而把结论延伸到无穷无尽,也就完成了猜测正确性的证明。
-
-### 为什么要写递归
-
-首先为了训练逆向思考的能力。递推的思维是正常人的思维,总是看着眼前的问题思考对策,解决问题是将来时;递归的思维,逼迫我们倒着思考,看到问题的尽头,把解决问题的过程看做过去时。
-
-第二,练习分析问题的结构,当问题可以被分解成相同结构的小问题时,你能敏锐发现这个特点,进而高效解决问题。
-
-第三,跳出细节,从整体上看问题。再说说归并排序,其实可以不用递归来划分左右区域的,但是代价就是代码极其难以理解,大概看一下代码(归并排序在后面讲,这里大致看懂意思就行,体会递归的妙处):
-
-```java
-void sort(Comparable[] a){
- int N = a.length;
- // 这么复杂,是对排序的不尊重。我拒绝研究这样的代码。
- for (int sz = 1; sz < N; sz = sz + sz)
- for (int lo = 0; lo < N - sz; lo += sz + sz)
- merge(a, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1));
-}
-
-/* 我还是选择递归,简单,漂亮 */
-void sort(Comparable[] a, int lo, int hi) {
- if (lo >= hi) return;
- int mid = lo + (hi - lo) / 2;
- sort(a, lo, mid); // 排序左半边
- sort(a, mid + 1, hi); // 排序右半边
- merge(a, lo, mid, hi); // 合并两边
-}
-
-```
-
-看起来简洁漂亮是一方面,关键是**可解释性很强**:把左半边排序,把右半边排序,最后合并两边。而非递归版本看起来不知所云,充斥着各种难以理解的边界计算细节,特别容易出 bug 且难以调试,人生苦短,我更倾向于递归版本。
-
-显然有时候递归处理是高效的,比如归并排序,**有时候是低效的**,比如数孙悟空身上的毛,因为堆栈会消耗额外空间,而简单的递推不会消耗空间。比如这个例子,给一个链表头,计算它的长度:
-
-```java
-/* 典型的递推遍历框架,需要额外空间 O(1) */
-public int size(Node head) {
- int size = 0;
- for (Node p = head; p != null; p = p.next) size++;
- return size;
-}
-/* 我偏要递归,万物皆递归,需要额外空间 O(N) */
-public int size(Node head) {
- if (head == null) return 0;
- return size(head.next) + 1;
-}
-```
-
-### 写递归的技巧
-
-我的一点心得是:**明白一个函数的作用并相信它能完成这个任务,千万不要试图跳进细节。**千万不要跳进这个函数里面企图探究更多细节,否则就会陷入无穷的细节无法自拔,人脑能压几个栈啊。
-
-先举个最简单的例子:遍历二叉树。
-
-```cpp
-void traverse(TreeNode* root) {
- if (root == nullptr) return;
- traverse(root->left);
- traverse(root->right);
-}
-```
-
-这几行代码就足以扫荡任何一棵二叉树了。我想说的是,对于递归函数`traverse(root)`,我们只要相信:给它一个根节点`root`,它就能遍历这棵树,因为写这个函数不就是为了这个目的吗?所以我们只需要把这个节点的左右节点再甩给这个函数就行了,因为我相信它能完成任务的。那么遍历一棵N叉数呢?太简单了好吧,和二叉树一模一样啊。
-
-```cpp
-void traverse(TreeNode* root) {
- if (root == nullptr) return;
- for (child : root->children)
- traverse(child);
-}
-```
-
-至于遍历的什么前、中、后序,那都是显而易见的,对于N叉树,显然没有中序遍历。
-
-
-
-以下**详解 LeetCode 的一道题来说明**:给一颗二叉树,和一个目标值,节点上的值有正有负,返回树中和等于目标值的路径条数,让你编写 pathSum 函数:
-
-```
-/* 来源于 LeetCode PathSum III: https://leetcode.com/problems/path-sum-iii/ */
-root = [10,5,-3,3,2,null,11,3,-2,null,1],
-sum = 8
-
- 10
- / \
- 5 -3
- / \ \
- 3 2 11
- / \ \
-3 -2 1
-
-Return 3. The paths that sum to 8 are:
-
-1. 5 -> 3
-2. 5 -> 2 -> 1
-3. -3 -> 11
-```
-
-```cpp
-/* 看不懂没关系,底下有更详细的分析版本,这里突出体现递归的简洁优美 */
-int pathSum(TreeNode root, int sum) {
- if (root == null) return 0;
- return count(root, sum) +
- pathSum(root.left, sum) + pathSum(root.right, sum);
-}
-int count(TreeNode node, int sum) {
- if (node == null) return 0;
- return (node.val == sum) +
- count(node.left, sum - node.val) + count(node.right, sum - node.val);
-}
-```
-
-题目看起来很复杂吧,不过代码却极其简洁,这就是递归的魅力。我来简单总结这个问题的**解决过程**:
-
-首先明确,递归求解树的问题必然是要遍历整棵树的,所以**二叉树的遍历框架**(分别对左右孩子递归调用函数本身)必然要出现在主函数 pathSum 中。那么对于每个节点,他们应该干什么呢?他们应该看看,自己和脚底下的小弟们包含多少条符合条件的路径。好了,这道题就结束了。
-
-按照前面说的技巧,根据刚才的分析来定义清楚每个递归函数应该做的事:
-
-PathSum 函数:给他一个节点和一个目标值,他返回以这个节点为根的树中,和为目标值的路径总数。
-
-count 函数:给他一个节点和一个目标值,他返回以这个节点为根的树中,能凑出几个以该节点为路径开头,和为目标值的路径总数。
-
-```cpp
-/* 有了以上铺垫,详细注释一下代码 */
-int pathSum(TreeNode root, int sum) {
- if (root == null) return 0;
- int pathImLeading = count(root, sum); // 自己为开头的路径数
- int leftPathSum = pathSum(root.left, sum); // 左边路径总数(相信他能算出来)
- int rightPathSum = pathSum(root.right, sum); // 右边路径总数(相信他能算出来)
- return leftPathSum + rightPathSum + pathImLeading;
-}
-int count(TreeNode node, int sum) {
- if (node == null) return 0;
- // 我自己能不能独当一面,作为一条单独的路径呢?
- int isMe = (node.val == sum) ? 1 : 0;
- // 左边的小老弟,你那边能凑几个 sum - node.val 呀?
- int leftBrother = count(node.left, sum - node.val);
- // 右边的小老弟,你那边能凑几个 sum - node.val 呀?
- int rightBrother = count(node.right, sum - node.val);
- return isMe + leftBrother + rightBrother; // 我这能凑这么多个
-}
-```
-
-还是那句话,明白每个函数能做的事,并相信他们能够完成。
-
-总结下,PathSum 函数提供的二叉树遍历框架,在遍历中对每个节点调用 count 函数,看出先序遍历了吗(这道题什么序都是一样的);count 函数也是一个二叉树遍历,用于寻找以该节点开头的目标值路径。好好体会吧!
-
-## 分治算法
-
-**归并排序**,典型的分治算法;分治,典型的递归结构。
-
-分治算法可以分三步走:分解 -> 解决 -> 合并
-
-1. 分解原问题为结构相同的子问题。
-2. 分解到某个容易求解的边界之后,进行第归求解。
-3. 将子问题的解合并成原问题的解。
-
-归并排序,我们就叫这个函数`merge_sort`吧,按照我们上面说的,要明确该函数的职责,即**对传入的一个数组排序**。OK,那么这个问题能不能分解呢?当然可以!给一个数组排序,不就等于给该数组的两半分别排序,然后合并就完事了。
-
-```cpp
-void merge_sort(一个数组) {
- if (可以很容易处理) return;
- merge_sort(左半个数组);
- merge_sort(右半个数组);
- merge(左半个数组, 右半个数组);
-}
-```
-
-好了,这个算法也就这样了,完全没有任何难度。记住之前说的,相信函数的能力,传给他半个数组,那么这半个数组就已经被排好了。而且你会发现这不就是个二叉树遍历模板吗?为什么是后序遍历?因为我们分治算法的套路是 **分解 -> 解决(触底) -> 合并(回溯)** 啊,先左右分解,再处理合并,回溯就是在退栈,就相当于后序遍历了。至于`merge`函数,参考两个有序链表的合并,简直一模一样,下面直接贴代码吧。
-
-下面参考《算法4》的 Java 代码,很漂亮。由此可见,不仅算法思想思想重要,编码技巧也是挺重要的吧!多思考,多模仿。
-
-```java
-public class Merge {
- // 不要在 merge 函数里构造新数组了,因为 merge 函数会被多次调用,影响性能
- // 直接一次性构造一个足够大的数组,简洁,高效
- private static Comparable[] aux;
-
- public static void sort(Comparable[] a) {
- aux = new Comparable[a.length];
- sort(a, 0, a.length - 1);
- }
-
- private static void sort(Comparable[] a, int lo, int hi) {
- if (lo >= hi) return;
- int mid = lo + (hi - lo) / 2;
- sort(a, lo, mid);
- sort(a, mid + 1, hi);
- merge(a, lo, mid, hi);
- }
-
- private static void merge(Comparable[] a, int lo, int mid, int hi) {
- int i = lo, j = mid + 1;
- for (int k = lo; k <= hi; k++)
- aux[k] = a[k];
- for (int k = lo; k <= hi; k++) {
- if (i > mid) { a[k] = aux[j++]; }
- else if (j > hi) { a[k] = aux[i++]; }
- else if (less(aux[j], aux[i])) { a[k] = aux[j++]; }
- else { a[k] = aux[i++]; }
- }
- }
-
- private static boolean less(Comparable v, Comparable w) {
- return v.compareTo(w) < 0;
- }
-}
-```
-
-LeetCode 上有分治算法的专项练习,可复制到浏览器去做题:
-
-https://leetcode.com/tag/divide-and-conquer/
-
-
-
-
-**_____________**
-
-**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**。
-
-**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。
-
-
-
-
-
-======其他语言代码======
-
-### javascript
-
-[437. 路径总和 III](https://leetcode-cn.com/problems/path-sum-iii/)
-
-```js
-/**
- * Definition for a binary tree node.
- * function TreeNode(val, left, right) {
- * this.val = (val===undefined ? 0 : val)
- * this.left = (left===undefined ? null : left)
- * this.right = (right===undefined ? null : right)
- * }
- */
-/**
- * @param {TreeNode} root
- * @param {number} sum
- * @return {number}
- */
-var pathSum = function(root, sum) {
- // 二叉树-题目要求只能从父节点到子节点 所以用先序遍历
-
- // 路径总数
- let ans = 0
-
- // 存储前缀和
- let map = new Map()
-
- // 先序遍历二叉树
- function dfs(presum,node) {
- if(!node)return 0 // 遍历出口
-
- // 将当前前缀和添加到map
- map.set(presum,(map.get(presum) || 0) +1 )
- // 从根节点到当前节点的值
- let target = presum + node.val
-
- // target-sum = 需要的前缀和长度
- // 然而前缀和之前我们都存过了 检索map里key为该前缀和的value
- // map的值相当于有多少个节点到当前节点=sum 也就是有几条路径
- ans+=(map.get(target - sum) || 0)
-
- // 按顺序遍历左右节点
- dfs(target,node.left)
- dfs(target,node.right)
-
- // 这层遍历完就把该层的前缀和去掉
- map.set(presum,map.get(presum) -1 )
- }
- dfs(0,root)
- return ans
-};
-```
-
-归并排序
-
-```js
-var sort = function (arr) {
- if (arr.length <= 1) return arr;
-
- let mid = parseInt(arr.length / 2);
- // 递归调用自身,拆分的数组都是排好序的,最后传入merge合并处理
- return merge(sort(arr.slice(0, mid)), sort(arr.slice(mid)));
-}
-// 将两个排好序的数组合并成一个顺序数组
-var merge = function (left, right) {
- let res = [];
- while (left.length > 0 && right.length > 0) {
- // 不断比较left和right数组的第一项,小的取出存入res
- left[0] < right[0] ? res.push(left.shift()) : res.push(right.shift());
- }
- return res.concat(left, right);
-}
-```
-
diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\351\233\206\345\220\210\345\210\222\345\210\206.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\351\233\206\345\220\210\345\210\222\345\210\206.md"
new file mode 100644
index 0000000000000000000000000000000000000000..8092d38a92392d5faccef3a0f99c08203734cb5a
--- /dev/null
+++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\351\233\206\345\220\210\345\210\222\345\210\206.md"
@@ -0,0 +1,573 @@
+# 经典回溯算法:集合划分问题
+
+
+
+
+
+
+
+
+![](https://labuladong.github.io/algo/images/souyisou1.png)
+
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
+
+
+
+读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
+
+| LeetCode | 力扣 | 难度 |
+| :----: | :----: | :----: |
+| [698. Partition to K Equal Sum Subsets](https://leetcode.com/problems/partition-to-k-equal-sum-subsets/) | [698. 划分为k个相等的子集](https://leetcode.cn/problems/partition-to-k-equal-sum-subsets/) | 🟠
+
+**-----------**
+
+之前说过回溯算法是笔试中最好用的算法,只要你没什么思路,就用回溯算法暴力求解,即便不能通过所有测试用例,多少能过一点。
+
+回溯算法的技巧也不难,前文 [回溯算法框架套路](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版) 说过,回溯算法就是穷举一棵决策树的过程,只要在递归之前「做选择」,在递归之后「撤销选择」就行了。
+
+**但是,就算暴力穷举,不同的思路也有优劣之分**。
+
+本文就来看一道非常经典的回溯算法问题,力扣第 698 题「划分为k个相等的子集」。这道题可以帮你更深刻理解回溯算法的思维,得心应手地写出回溯函数。
+
+题目非常简单:
+
+给你输入一个数组 `nums` 和一个正整数 `k`,请你判断 `nums` 是否能够被平分为元素和相同的 `k` 个子集。
+
+函数签名如下:
+
+```java
+boolean canPartitionKSubsets(int[] nums, int k);
+```
+
+我们之前 [背包问题之子集划分](https://labuladong.github.io/article/fname.html?fname=背包子集) 写过一次子集划分问题,不过那道题只需要我们把集合划分成两个相等的集合,可以转化成背包问题用动态规划技巧解决。
+
+> 思考题:为什么划分成两个相等的子集可以转化成背包问题用动态规划思路解决,而划分成 `k` 个相等的子集就不可以转化成背包问题,只能用回溯算法暴力穷举?请先尝试自己思考,我会在文末给出答案。
+
+但是如果划分成多个相等的集合,解法一般只能通过暴力穷举,时间复杂度爆表,是练习回溯算法和递归思维的好机会。
+
+### 一、思路分析
+
+首先,我们回顾一下以前学过的排列组合知识:
+
+1、`P(n, k)`(也有很多书写成 `A(n, k)`)表示从 `n` 个不同元素中拿出 `k` 个元素的排列(Permutation/Arrangement);`C(n, k)` 表示从 `n` 个不同元素中拿出 `k` 个元素的组合(Combination)总数。
+
+2、「排列」和「组合」的主要区别在于是否考虑顺序的差异。
+
+3、排列、组合总数的计算公式:
+
+![](https://labuladong.github.io/algo/images/集合划分/math.png)
+
+好,现在我问一个问题,这个排列公式 `P(n, k)` 是如何推导出来的?为了搞清楚这个问题,我需要讲一点组合数学的知识。
+
+排列组合问题的各种变体都可以抽象成「球盒模型」,`P(n, k)` 就可以抽象成下面这个场景:
+
+![](https://labuladong.github.io/algo/images/集合划分/7.jpeg)
+
+即,将 `n` 个标记了不同序号的球(标号为了体现顺序的差异),放入 `k` 个标记了不同序号的盒子中(其中 `n >= k`,每个盒子最终都装有恰好一个球),共有 `P(n, k)` 种不同的方法。
+
+现在你来,往盒子里放球,你怎么放?其实有两种视角。
+
+**首先,你可以站在盒子的视角**,每个盒子必然要选择一个球。
+
+这样,第一个盒子可以选择 `n` 个球中的任意一个,然后你需要让剩下 `k - 1` 个盒子在 `n - 1` 个球中选择:
+
+![](https://labuladong.github.io/algo/images/集合划分/8.jpeg)
+
+**另外,你也可以站在球的视角**,因为并不是每个球都会被装进盒子,所以球的视角分两种情况:
+
+1、第一个球可以不装进任何一个盒子,这样的话你就需要将剩下 `n - 1` 个球放入 `k` 个盒子。
+
+2、第一个球可以装进 `k` 个盒子中的任意一个,这样的话你就需要将剩下 `n - 1` 个球放入 `k - 1` 个盒子。
+
+结合上述两种情况,可以得到:
+
+![](https://labuladong.github.io/algo/images/集合划分/9.jpeg)
+
+你看,两种视角得到两个不同的递归式,但这两个递归式解开的结果都是我们熟知的阶乘形式:
+
+![](https://labuladong.github.io/algo/images/集合划分/math1.png)
+
+至于如何解递归式,涉及数学的内容比较多,这里就不做深入探讨了,有兴趣的读者可以自行学习组合数学相关知识。
+
+回到正题,这道算法题让我们求子集划分,子集问题和排列组合问题有所区别,但我们可以借鉴「球盒模型」的抽象,用两种不同的视角来解决这道子集划分问题。
+
+把装有 `n` 个数字的数组 `nums` 分成 `k` 个和相同的集合,你可以想象将 `n` 个数字分配到 `k` 个「桶」里,最后这 `k` 个「桶」里的数字之和要相同。
+
+前文 [回溯算法框架套路](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版) 说过,回溯算法的关键在哪里?
+
+关键是要知道怎么「做选择」,这样才能利用递归函数进行穷举。
+
+那么模仿排列公式的推导思路,将 `n` 个数字分配到 `k` 个桶里,我们也可以有两种视角:
+
+**视角一,如果我们切换到这 `n` 个数字的视角,每个数字都要选择进入到 `k` 个桶中的某一个**。
+
+![](https://labuladong.github.io/algo/images/集合划分/5.jpeg)
+
+**视角二,如果我们切换到这 `k` 个桶的视角,对于每个桶,都要遍历 `nums` 中的 `n` 个数字,然后选择是否将当前遍历到的数字装进自己这个桶里**。
+
+![](https://labuladong.github.io/algo/images/集合划分/6.jpeg)
+
+你可能问,这两种视角有什么不同?
+
+**用不同的视角进行穷举,虽然结果相同,但是解法代码的逻辑完全不同,进而算法的效率也会不同;对比不同的穷举视角,可以帮你更深刻地理解回溯算法,我们慢慢道来**。
+
+### 二、以数字的视角
+
+用 for 循环迭代遍历 `nums` 数组大家肯定都会:
+
+```java
+for (int index = 0; index < nums.length; index++) {
+ System.out.println(nums[index]);
+}
+```
+
+递归遍历数组你会不会?其实也很简单:
+
+```java
+void traverse(int[] nums, int index) {
+ if (index == nums.length) {
+ return;
+ }
+ System.out.println(nums[index]);
+ traverse(nums, index + 1);
+}
+```
+
+只要调用 `traverse(nums, 0)`,和 for 循环的效果是完全一样的。
+
+那么回到这道题,以数字的视角,选择 `k` 个桶,用 for 循环写出来是下面这样:
+
+```java
+// k 个桶(集合),记录每个桶装的数字之和
+int[] bucket = new int[k];
+
+// 穷举 nums 中的每个数字
+for (int index = 0; index < nums.length; index++) {
+ // 穷举每个桶
+ for (int i = 0; i < k; i++) {
+ // nums[index] 选择是否要进入第 i 个桶
+ // ...
+ }
+}
+```
+
+如果改成递归的形式,就是下面这段代码逻辑:
+
+```java
+// k 个桶(集合),记录每个桶装的数字之和
+int[] bucket = new int[k];
+
+// 穷举 nums 中的每个数字
+void backtrack(int[] nums, int index) {
+ // base case
+ if (index == nums.length) {
+ return;
+ }
+ // 穷举每个桶
+ for (int i = 0; i < bucket.length; i++) {
+ // 选择装进第 i 个桶
+ bucket[i] += nums[index];
+ // 递归穷举下一个数字的选择
+ backtrack(nums, index + 1);
+ // 撤销选择
+ bucket[i] -= nums[index];
+ }
+}
+```
+
+虽然上述代码仅仅是穷举逻辑,还不能解决我们的问题,但是只要略加完善即可:
+
+```java
+// 主函数
+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;
+
+ // k 个桶(集合),记录每个桶装的数字之和
+ int[] bucket = new int[k];
+ // 理论上每个桶(集合)中数字的和
+ int target = sum / k;
+ // 穷举,看看 nums 是否能划分成 k 个和为 target 的子集
+ return backtrack(nums, 0, bucket, target);
+}
+
+// 递归穷举 nums 中的每个数字
+boolean backtrack(
+ int[] nums, int index, int[] bucket, int target) {
+
+ if (index == nums.length) {
+ // 检查所有桶的数字之和是否都是 target
+ for (int i = 0; i < bucket.length; i++) {
+ if (bucket[i] != target) {
+ return false;
+ }
+ }
+ // nums 成功平分成 k 个子集
+ return true;
+ }
+
+ // 穷举 nums[index] 可能装入的桶
+ for (int i = 0; i < bucket.length; i++) {
+ // 剪枝,桶装装满了
+ if (bucket[i] + nums[index] > target) {
+ continue;
+ }
+ // 将 nums[index] 装入 bucket[i]
+ bucket[i] += nums[index];
+ // 递归穷举下一个数字的选择
+ if (backtrack(nums, index + 1, bucket, target)) {
+ return true;
+ }
+ // 撤销选择
+ bucket[i] -= nums[index];
+ }
+
+ // nums[index] 装入哪个桶都不行
+ return false;
+}
+```
+
+有之前的铺垫,相信这段代码是比较容易理解的,其实我们可以再做一个优化。
+
+主要看 `backtrack` 函数的递归部分:
+
+```java
+for (int i = 0; i < bucket.length; i++) {
+ // 剪枝
+ if (bucket[i] + nums[index] > target) {
+ continue;
+ }
+
+ if (backtrack(nums, index + 1, bucket, target)) {
+ return true;
+ }
+}
+```
+
+**如果我们让尽可能多的情况命中剪枝的那个 if 分支,就可以减少递归调用的次数,一定程度上减少时间复杂度**。
+
+如何尽可能多的命中这个 if 分支呢?要知道我们的 `index` 参数是从 0 开始递增的,也就是递归地从 0 开始遍历 `nums` 数组。
+
+如果我们提前对 `nums` 数组排序,把大的数字排在前面,那么大的数字会先被分配到 `bucket` 中,对于之后的数字,`bucket[i] + nums[index]` 会更大,更容易触发剪枝的 if 条件。
+
+所以可以在之前的代码中再添加一些代码:
+
+```java
+boolean canPartitionKSubsets(int[] nums, int k) {
+ // 其他代码不变
+ // ...
+ /* 降序排序 nums 数组 */
+ Arrays.sort(nums);
+ for (i = 0, j = nums.length - 1; i < j; i++, j--) {
+ // 交换 nums[i] 和 nums[j]
+ int temp = nums[i];
+ nums[i] = nums[j];
+ nums[j] = temp;
+ }
+ /*******************/
+ return backtrack(nums, 0, bucket, target);
+}
+```
+
+由于 Java 的语言特性,这段代码通过先升序排序再反转,达到降序排列的目的。
+
+这个解法可以得到正确答案,但耗时比较多,已经无法通过所有测试用例了,接下来看看另一种视角的解法。
+
+### 三、以桶的视角
+
+文章开头说了,**以桶的视角进行穷举,每个桶需要遍历 `nums` 中的所有数字,决定是否把当前数字装进桶中;当装满一个桶之后,还要装下一个桶,直到所有桶都装满为止**。
+
+这个思路可以用下面这段代码表示出来:
+
+```java
+// 装满所有桶为止
+while (k > 0) {
+ // 记录当前桶中的数字之和
+ int bucket = 0;
+ for (int i = 0; i < nums.length; i++) {
+ // 决定是否将 nums[i] 放入当前桶中
+ if (canAdd(bucket, num[i])) {
+ bucket += nums[i];
+ }
+ if (bucket == target) {
+ // 装满了一个桶,装下一个桶
+ k--;
+ break;
+ }
+ }
+}
+```
+
+那么我们也可以把这个 while 循环改写成递归函数,不过比刚才略微复杂一些,首先写一个 `backtrack` 递归函数出来:
+
+```java
+boolean backtrack(int k, int bucket,
+ int[] nums, int start, boolean[] used, int target);
+```
+
+不要被这么多参数吓到,我会一个个解释这些参数。**如果你能够透彻理解本文,也能得心应手地写出这样的回溯函数**。
+
+这个 `backtrack` 函数的参数可以这样解释:
+
+现在 `k` 号桶正在思考是否应该把 `nums[start]` 这个元素装进来;目前 `k` 号桶里面已经装的数字之和为 `bucket`;`used` 标志某一个元素是否已经被装到桶中;`target` 是每个桶需要达成的目标和。
+
+根据这个函数定义,可以这样调用 `backtrack` 函数:
+
+```java
+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;
+
+ boolean[] used = new boolean[nums.length];
+ int target = sum / k;
+ // k 号桶初始什么都没装,从 nums[0] 开始做选择
+ return backtrack(k, 0, nums, 0, used, target);
+}
+```
+
+实现 `backtrack` 函数的逻辑之前,再重复一遍,从桶的视角:
+
+1、需要遍历 `nums` 中所有数字,决定哪些数字需要装到当前桶中。
+
+2、如果当前桶装满了(桶内数字和达到 `target`),则让下一个桶开始执行第 1 步。
+
+下面的代码就实现了这个逻辑:
+
+```java
+boolean backtrack(int k, int bucket,
+ int[] nums, int start, boolean[] used, int target) {
+ // base case
+ if (k == 0) {
+ // 所有桶都被装满了,而且 nums 一定全部用完了
+ // 因为 target == sum / k
+ return true;
+ }
+ if (bucket == target) {
+ // 装满了当前桶,递归穷举下一个桶的选择
+ // 让下一个桶从 nums[0] 开始选数字
+ return backtrack(k - 1, 0 ,nums, 0, used, target);
+ }
+
+ // 从 start 开始向后探查有效的 nums[i] 装入当前桶
+ for (int i = start; i < nums.length; i++) {
+ // 剪枝
+ if (used[i]) {
+ // nums[i] 已经被装入别的桶中
+ continue;
+ }
+ if (nums[i] + bucket > target) {
+ // 当前桶装不下 nums[i]
+ continue;
+ }
+ // 做选择,将 nums[i] 装入当前桶中
+ used[i] = true;
+ bucket += nums[i];
+ // 递归穷举下一个数字是否装入当前桶
+ if (backtrack(k, bucket, nums, i + 1, used, target)) {
+ return true;
+ }
+ // 撤销选择
+ used[i] = false;
+ bucket -= nums[i];
+ }
+ // 穷举了所有数字,都无法装满当前桶
+ return false;
+}
+```
+
+**这段代码是可以得出正确答案的,但是效率很低,我们可以思考一下是否还有优化的空间**。
+
+首先,在这个解法中每个桶都可以认为是没有差异的,但是我们的回溯算法却会对它们区别对待,这里就会出现重复计算的情况。
+
+什么意思呢?我们的回溯算法,说到底就是穷举所有可能的组合,然后看是否能找出和为 `target` 的 `k` 个桶(子集)。
+
+那么,比如下面这种情况,`target = 5`,算法会在第一个桶里面装 `1, 4`:
+
+![](https://labuladong.github.io/algo/images/集合划分/1.jpeg)
+
+现在第一个桶装满了,就开始装第二个桶,算法会装入 `2, 3`:
+
+![](https://labuladong.github.io/algo/images/集合划分/2.jpeg)
+
+然后以此类推,对后面的元素进行穷举,凑出若干个和为 5 的桶(子集)。
+
+但问题是,如果最后发现无法凑出和为 `target` 的 `k` 个子集,算法会怎么做?
+
+回溯算法会回溯到第一个桶,重新开始穷举,现在它知道第一个桶里装 `1, 4` 是不可行的,它会尝试把 `2, 3` 装到第一个桶里:
+
+![](https://labuladong.github.io/algo/images/集合划分/3.jpeg)
+
+现在第一个桶装满了,就开始装第二个桶,算法会装入 `1, 4`:
+
+![](https://labuladong.github.io/algo/images/集合划分/4.jpeg)
+
+好,到这里你应该看出来问题了,这种情况其实和之前的那种情况是一样的。也就是说,到这里你其实已经知道不需要再穷举了,必然凑不出来和为 `target` 的 `k` 个子集。
+
+但我们的算法还是会傻乎乎地继续穷举,因为在她看来,第一个桶和第二个桶里面装的元素不一样,那这就是两种不一样的情况呀。
+
+那么我们怎么让算法的智商提高,识别出这种情况,避免冗余计算呢?
+
+你注意这两种情况的 `used` 数组肯定长得一样,所以 `used` 数组可以认为是回溯过程中的「状态」。
+
+**所以,我们可以用一个 `memo` 备忘录,在装满一个桶时记录当前 `used` 的状态,如果当前 `used` 的状态是曾经出现过的,那就不用再继续穷举,从而起到剪枝避免冗余计算的作用**。
+
+有读者肯定会问,`used` 是一个布尔数组,怎么作为键进行存储呢?这其实是小问题,比如我们可以把数组转化成字符串,这样就可以作为哈希表的键进行存储了。
+
+看下代码实现,只要稍微改一下 `backtrack` 函数即可:
+
+```java
+// 备忘录,存储 used 数组的状态
+HashMap memo = new HashMap<>();
+
+boolean backtrack(int k, int bucket, int[] nums, int start, boolean[] used, int target) {
+ // base case
+ if (k == 0) {
+ return true;
+ }
+ // 将 used 的状态转化成形如 [true, false, ...] 的字符串
+ // 便于存入 HashMap
+ String state = Arrays.toString(used);
+
+ if (bucket == target) {
+ // 装满了当前桶,递归穷举下一个桶的选择
+ boolean res = backtrack(k - 1, 0, nums, 0, used, target);
+ // 将当前状态和结果存入备忘录
+ memo.put(state, res);
+ return res;
+ }
+
+ if (memo.containsKey(state)) {
+ // 如果当前状态曾今计算过,就直接返回,不要再递归穷举了
+ return memo.get(state);
+ }
+
+ // 其他逻辑不变...
+}
+```
+
+这样提交解法,发现执行效率依然比较低,这次不是因为算法逻辑上的冗余计算,而是代码实现上的问题。
+
+**因为每次递归都要把 `used` 数组转化成字符串,这对于编程语言来说也是一个不小的消耗,所以我们还可以进一步优化**。
+
+注意题目给的数据规模 `nums.length <= 16`,也就是说 `used` 数组最多也不会超过 16,那么我们完全可以用「位图」的技巧,用一个 int 类型的 `used` 变量来替代 `used` 数组。
+
+具体来说,我们可以用整数 `used` 的第 `i` 位(`(used >> i) & 1`)的 1/0 来表示 `used[i]` 的 true/false。
+
+这样一来,不仅节约了空间,而且整数 `used` 也可以直接作为键存入 HashMap,省去数组转字符串的消耗。
+
+看下最终的解法代码:
+
+```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 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++) {
+ // 剪枝
+ 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;
+}
+```
+
+至此,这道题的第二种思路也完成了。
+
+### 四、最后总结
+
+本文写的这两种思路都可以算出正确答案,不过第一种解法即便经过了排序优化,也明显比第二种解法慢很多,这是为什么呢?
+
+我们来分析一下这两个算法的时间复杂度,假设 `nums` 中的元素个数为 `n`。
+
+先说第一个解法,也就是从数字的角度进行穷举,`n` 个数字,每个数字有 `k` 个桶可供选择,所以组合出的结果个数为 `k^n`,时间复杂度也就是 `O(k^n)`。
+
+第二个解法,每个桶要遍历 `n` 个数字,对每个数字有「装入」或「不装入」两种选择,所以组合的结果有 `2^n` 种;而我们有 `k` 个桶,所以总的时间复杂度为 `O(k*2^n)`。
+
+**当然,这是对最坏复杂度上界的粗略估算,实际的复杂度肯定要好很多,毕竟我们添加了这么多剪枝逻辑**。不过,从复杂度的上界已经可以看出第一种思路要慢很多了。
+
+所以,谁说回溯算法没有技巧性的?虽然回溯算法就是暴力穷举,但穷举也分聪明的穷举方式和低效的穷举方式,关键看你以谁的「视角」进行穷举。
+
+通俗来说,我们应该尽量「少量多次」,就是说宁可多做几次选择(乘法关系),也不要给太大的选择空间(指数关系);做 `n` 次「`k` 选一」仅重复一次(`O(k^n)`),比 `n` 次「二选一」重复 `k` 次(`O(k*2^n)`)效率低很多。
+
+好了,这道题我们从两种视角进行穷举,虽然代码量看起来多,但核心逻辑都是类似的,相信你通过本文能够更深刻地理解回溯算法。
+
+> 文中思考题答案:为什么划分两个相等的子集可以转化成背包问题?
+
+> [0-1 背包问题](https://labuladong.github.io/article/fname.html?fname=背包问题) 的场景中,有一个背包和若干物品,每个物品有**两个选择**,分别是「装进背包」和「不装进背包」。把原集合 `S` 划分成两个相等子集 `S_1, S_2` 的场景下,`S` 中的每个元素也有**两个选择**,分别是「装进 `S_1`」和「不装进 `S_1`(装进 `S_2`)」,这时候的穷举思路其实和背包问题相同。
+
+> 但如果你想把 `S` 划分成 `k` 个相等的子集,相当于 `S` 中的每个元素有 **`k` 个选择**,这和标准背包问题的场景有本质区别,是无法套用背包问题的解题思路的。
+
+
+
+
+
+引用本文的文章
+
+ - [动态规划问题的两种穷举视角](https://labuladong.github.io/article/fname.html?fname=动归两种视角)
+ - [谁能想到,斗地主也能玩出算法](https://labuladong.github.io/article/fname.html?fname=斗地主)
+
+
+
+
+
+
+
+**_____________**
+
+**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
+
+![](https://labuladong.github.io/algo/images/souyisou2.png)
\ No newline at end of file
diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/LRU\347\256\227\346\263\225.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/LRU\347\256\227\346\263\225.md"
index ececcc8b21b6ba1bb616161e422d38d997390da2..85f16b416c49efba560f6798f74b5ff4471f0063 100644
--- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/LRU\347\256\227\346\263\225.md"
+++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/LRU\347\256\227\346\263\225.md"
@@ -1,11 +1,5 @@
# LRU 缓存淘汰算法设计
-
-
-
-
-
-
@@ -15,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -348,6 +342,37 @@ class LRUCache {
* [手把手带你实现 LFU 算法](https://labuladong.github.io/article/fname.html?fname=LFU)
+
+
+
+
+引用本文的文章
+
+ - [一文看懂 session 和 cookie](https://labuladong.github.io/article/fname.html?fname=session和cookie)
+ - [常数时间删除/查找数组中的任意元素](https://labuladong.github.io/article/fname.html?fname=随机集合)
+ - [数据结构设计:最大栈](https://labuladong.github.io/article/fname.html?fname=最大栈)
+ - [算法就像搭乐高:带你手撸 LFU 算法](https://labuladong.github.io/article/fname.html?fname=LFU)
+ - [算法笔试「骗分」套路](https://labuladong.github.io/article/fname.html?fname=刷题技巧)
+
+
+
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| - | [剑指 Offer II 031. 最近最少使用缓存](https://leetcode.cn/problems/OrIXps/?show=1) |
+
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/koko\345\201\267\351\246\231\350\225\211.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/koko\345\201\267\351\246\231\350\225\211.md"
deleted file mode 100644
index 4f7e50bb58587ee9460b716e593e1f95c7ef53ce..0000000000000000000000000000000000000000
--- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/koko\345\201\267\351\246\231\350\225\211.md"
+++ /dev/null
@@ -1,373 +0,0 @@
-# 如何运用二分查找算法
-
-
-
-
-
-
-
-
-
-![](../pictures/souyisou.png)
-
-**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
-
-读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
-
-[875.爱吃香蕉的珂珂](https://leetcode-cn.com/problems/koko-eating-bananas)
-
-[1011.在D天内送达包裹的能力](https://leetcode-cn.com/problems/capacity-to-ship-packages-within-d-days)
-
-**-----------**
-
-二分查找到底有能运用在哪里?
-
-最常见的就是教科书上的例子,在**有序数组**中搜索给定的某个目标值的索引。再推广一点,如果目标值存在重复,修改版的二分查找可以返回目标值的左侧边界索引或者右侧边界索引。
-
-PS:以上提到的三种二分查找算法形式在前文「二分查找详解」有代码详解,如果没看过强烈建议看看。
-
-抛开有序数组这个枯燥的数据结构,二分查找如何运用到实际的算法问题中呢?当搜索空间有序的时候,就可以通过二分搜索「剪枝」,大幅提升效率。
-
-说起来玄乎得很,本文先用一个具体的「Koko 吃香蕉」的问题来举个例子。
-
-### 一、问题分析
-
-![](../pictures/二分应用/title1.png)
-
-也就是说,Koko 每小时最多吃一堆香蕉,如果吃不下的话留到下一小时再吃;如果吃完了这一堆还有胃口,也只会等到下一小时才会吃下一堆。在这个条件下,让我们确定 Koko 吃香蕉的**最小速度**(根/小时)。
-
-如果直接给你这个情景,你能想到哪里能用到二分查找算法吗?如果没有见过类似的问题,恐怕是很难把这个问题和二分查找联系起来的。
-
-那么我们先抛开二分查找技巧,想想如何暴力解决这个问题呢?
-
-首先,算法要求的是「`H` 小时内吃完香蕉的最小速度」,我们不妨称为 `speed`,请问 `speed` 最大可能为多少,最少可能为多少呢?
-
-显然最少为 1,最大为 `max(piles)`,因为一小时最多只能吃一堆香蕉。那么暴力解法就很简单了,只要从 1 开始穷举到 `max(piles)`,一旦发现发现某个值可以在 `H` 小时内吃完所有香蕉,这个值就是最小速度:
-
-```java
-int minEatingSpeed(int[] piles, int H) {
- // piles 数组的最大值
- int max = getMax(piles);
- for (int speed = 1; speed < max; speed++) {
- // 以 speed 是否能在 H 小时内吃完香蕉
- if (canFinish(piles, speed, H))
- return speed;
- }
- return max;
-}
-```
-
-注意这个 for 循环,就是在**连续的空间线性搜索,这就是二分查找可以发挥作用的标志**。由于我们要求的是最小速度,所以可以用一个**搜索左侧边界的二分查找**来代替线性搜索,提升效率:
-
-```java
-int minEatingSpeed(int[] piles, int H) {
- // 套用搜索左侧边界的算法框架
- int left = 1, right = getMax(piles) + 1;
- while (left < right) {
- // 防止溢出
- int mid = left + (right - left) / 2;
- if (canFinish(piles, mid, H)) {
- right = mid;
- } else {
- left = mid + 1;
- }
- }
- return left;
-}
-```
-
-PS:如果对于这个二分查找算法的细节问题有疑问,建议看下前文「二分查找详解」搜索左侧边界的算法模板,这里不展开了。
-
-剩下的辅助函数也很简单,可以一步步拆解实现:
-
-```java
-// 时间复杂度 O(N)
-boolean canFinish(int[] piles, int speed, int H) {
- int time = 0;
- for (int n : piles) {
- time += timeOf(n, speed);
- }
- return time <= H;
-}
-
-int timeOf(int n, int speed) {
- return (n / speed) + ((n % speed > 0) ? 1 : 0);
-}
-
-int getMax(int[] piles) {
- int max = 0;
- for (int n : piles)
- max = Math.max(n, max);
- return max;
-}
-```
-
-至此,借助二分查找技巧,算法的时间复杂度为 O(NlogN)。
-
-### 二、扩展延伸
-
-类似的,再看一道运输问题:
-
-![](../pictures/二分应用/title2.png)
-
-要在 `D` 天内运输完所有货物,货物不可分割,如何确定运输的最小载重呢(下文称为 `cap`)?
-
-其实本质上和 Koko 吃香蕉的问题一样的,首先确定 `cap` 的最小值和最大值分别为 `max(weights)` 和 `sum(weights)`。
-
-我们要求**最小载重**,所以可以用搜索左侧边界的二分查找算法优化线性搜索:
-
-```java
-// 寻找左侧边界的二分查找
-int shipWithinDays(int[] weights, int D) {
- // 载重可能的最小值
- int left = getMax(weights);
- // 载重可能的最大值 + 1
- int right = getSum(weights) + 1;
- while (left < right) {
- int mid = left + (right - left) / 2;
- if (canFinish(weights, D, mid)) {
- right = mid;
- } else {
- left = mid + 1;
- }
- }
- return left;
-}
-
-// 如果载重为 cap,是否能在 D 天内运完货物?
-boolean canFinish(int[] w, int D, int cap) {
- int i = 0;
- for (int day = 0; day < D; day++) {
- int maxCap = cap;
- while ((maxCap -= w[i]) >= 0) {
- i++;
- if (i == w.length)
- return true;
- }
- }
- return false;
-}
-```
-
-通过这两个例子,你是否明白了二分查找在实际问题中的应用?
-
-```java
-for (int i = 0; i < n; i++)
- if (isOK(i))
- return ans;
-```
-
-**_____________**
-
-**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**。
-
-**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。
-
-
-
-
-======其他语言代码======
-
-[875.爱吃香蕉的珂珂](https://leetcode-cn.com/problems/koko-eating-bananas)
-
-[1011.在D天内送达包裹的能力](https://leetcode-cn.com/problems/capacity-to-ship-packages-within-d-days)
-
-
-
-### c++
-[cchroot](https://github.com/cchroot) 提供 C++ 代码:
-
-```c++
-class Solution {
-public:
- int minEatingSpeed(vector& piles, int H) {
- // 二分法查找最小速度
- // 初始化最小速度为 1,最大速度为题目设定的最大值 10^9
- // 这里也可以遍历 piles 数组,获取数组中的最大值,设置 right 为数组中的最大值即可(因为每堆香蕉1小时吃完是最快的)
- // log2(10^9) 约等于30,次数不多,所以这里暂时就不采取遍历获取最大值了
- int left = 1, right = pow(10, 9);
- while (left < right) { // 二分法基本的防止溢出
- int mid = left + (right - left) / 2;
- // 以 mid 的速度吃香蕉,是否能在 H 小时内吃完香蕉
- if (!canFinish(piles, mid, H))
- left = mid + 1;
- else
- right = mid;
- }
- return left;
- }
-
- // 以 speed 的速度是否能把香蕉吃完
- bool canFinish(vector& piles, int speed, int H) {
- int time = 0;
- // 遍历累加时间 time
- for (int p: piles)
- time += (p - 1) / speed + 1;
- return time <= H; // time 小于等于 H 说明能在 H 小时吃完返回 true, 否则返回 false
- }
-};
-```
-
-### python
-[tonytang731](https://https://github.com/tonytang731) 提供 Python3 代码:
-
-```python
-import math
-
-class Solution:
- def minEatingSpeed(self, piles, H):
- # 初始化起点和终点, 最快的速度可以一次拿完最大的一堆
- start = 1
- end = max(piles)
-
- # while loop进行二分查找
- while start + 1 < end:
- mid = start + (end - start) // 2
-
- # 如果中点所需时间大于H, 我们需要加速, 将起点设为中点
- if self.timeH(piles, mid) > H:
- start = mid
- # 如果中点所需时间小于H, 我们需要减速, 将终点设为中点
- else:
- end = mid
-
- # 提交前确认起点是否满足条件,我们要尽量慢拿
- if self.timeH(piles, start) <= H:
- return start
-
- # 若起点不符合, 则中点是答案
- return end
-
-
-
- def timeH(self, piles, K):
- # 初始化时间
- H = 0
-
- #求拿每一堆需要多长时间
- for pile in piles:
- H += math.ceil(pile / K)
-
- return H
-```
-
-
-
-### javascript
-
-用js写二分的时候,一定要注意使用`Math.floor((right - left) / 2)`或者`paserInt()`将结果整数化!由于js不声明变量类型,很多时候就很难发现自己浮点数、整数使用的问题。
-
-[875.爱吃香蕉的珂珂](https://leetcode-cn.com/problems/koko-eating-bananas)
-
-```js
-/**
- * @param {number[]} piles
- * @param {number} H
- * @return {number}
- */
-var minEatingSpeed = function (piles, H) {
- // 套用搜索左侧边界的算法框架
- let left = 1, right = getMax(piles) + 1;
-
- while (left < right) {
- // 防止溢出
- let mid = left + Math.floor((right - left) / 2);
- if (canFinish(piles, mid, H)) {
- right = mid;
- } else {
- left = mid + 1;
- }
- }
- return left;
-};
-
-// 时间复杂度 O(N)
-let canFinish = (piles, speed, H) => {
- let time = 0;
- for (let n of piles) {
- time += timeOf(n, speed);
- }
- return time <= H;
-}
-
-// 计算所需时间
-let timeOf = (n, speed) => {
- return Math.floor(
- (n / speed) + ((n % speed > 0) ? 1 : 0)
- );
-}
-
-let getMax = (piles) => {
- let max = 0;
- for (let n of piles) {
- max = Math.max(n, max);
- }
- return max;
-}
-
-```
-
-
-
-[传送门:1011.在D天内送达包裹的能力](https://leetcode-cn.com/problems/capacity-to-ship-packages-within-d-days)
-
-```js
-// 第1011题
-/**
- * @param {number[]} weights
- * @param {number} D
- * @return {number}
- */
-// 寻找左侧边界的二分查找
-var shipWithinDays = function (weights, D) {
- // 载重可能的最小值
- let left = getMax(weights);
-
- // 载重可能的最大值 + 1
- let right = getSum(weights) + 1;
-
- while (left < right) {
- let mid = left + Math.floor((right - left) / 2);
- if (canFinish(weights, D, mid)) {
- right = mid;
- } else {
- left = mid + 1;
- }
- }
- return left;
-}
-
-// 如果载重为 cap,是否能在 D 天内运完货物?
-let canFinish = (w, D, cap) => {
- let i = 0;
- for (let day = 0; day < D; day++) {
- let maxCap = cap;
- while ((maxCap -= w[i]) >= 0) {
- i++;
- if (i === w.length)
- return true;
- }
- }
- return false;
-}
-
-let getMax = (piles) => {
- let max = 0;
- for (let n of piles) {
- max = Math.max(n, max);
- }
- return max;
-}
-
-/**
- * @param {number[]} weights
- // 获取货物总重量
- */
-let getSum = (weights) => {
- return weights.reduce((total, cur) => {
- total += cur;
- return total
- }, 0)
-}
-```
-
diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/k\344\270\252\344\270\200\347\273\204\345\217\215\350\275\254\351\223\276\350\241\250.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/k\344\270\252\344\270\200\347\273\204\345\217\215\350\275\254\351\223\276\350\241\250.md"
index d2c71534bb84808cc3f363e653f995116ce7a470..cdd03c094ec2661411be59c14af2265c58b38016 100644
--- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/k\344\270\252\344\270\200\347\273\204\345\217\215\350\275\254\351\223\276\350\241\250.md"
+++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/k\344\270\252\344\270\200\347\273\204\345\217\215\350\275\254\351\223\276\350\241\250.md"
@@ -1,9 +1,5 @@
# 如何k个一组反转链表
-
-
-
-
@@ -13,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -155,6 +151,21 @@ ListNode reverseKGroup(ListNode head, int k) {
> 最后打个广告,我亲自制作了一门 [数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO),以视频课为主,手把手带你实现常用的数据结构及相关算法,旨在帮助算法基础较为薄弱的读者深入理解常用数据结构的底层原理,在算法学习中少走弯路。
+
+
+
+
+引用本文的文章
+
+ - [东哥带你刷二叉树(思路篇)](https://labuladong.github.io/article/fname.html?fname=二叉树系列1)
+ - [算法笔试「骗分」套路](https://labuladong.github.io/article/fname.html?fname=刷题技巧)
+
+
+
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\270\200\350\241\214\344\273\243\347\240\201\350\247\243\345\206\263\347\232\204\346\231\272\345\212\233\351\242\230.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\270\200\350\241\214\344\273\243\347\240\201\350\247\243\345\206\263\347\232\204\346\231\272\345\212\233\351\242\230.md"
index 8187fff56b1100ebc8ea4ea88f6d2104d650eb75..ebea93a1d32d0d69eea9de85d5bdda777a65b37b 100644
--- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\270\200\350\241\214\344\273\243\347\240\201\350\247\243\345\206\263\347\232\204\346\231\272\345\212\233\351\242\230.md"
+++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\270\200\350\241\214\344\273\243\347\240\201\350\247\243\345\206\263\347\232\204\346\231\272\345\212\233\351\242\230.md"
@@ -1,9 +1,5 @@
# 一行代码就能解决的算法题
-
-
-
-
@@ -13,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -145,6 +141,22 @@ int bulbSwitch(int n) {
就算有的 `n` 平方根结果是小数,强转成 int 型,也相当于一个最大整数上界,比这个上界小的所有整数,平方后的索引都是最后亮着的灯的索引。所以说我们直接把平方根转成整数,就是这个问题的答案。
+
+
+
+
+引用本文的文章
+
+ - [丑数系列算法详解](https://labuladong.github.io/article/fname.html?fname=丑数)
+ - [我的刷题心得](https://labuladong.github.io/article/fname.html?fname=算法心得)
+ - [经典动态规划:博弈问题](https://labuladong.github.io/article/fname.html?fname=动态规划之博弈问题)
+
+
+
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\345\210\244\345\256\232\345\255\220\345\272\217\345\210\227.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\345\210\244\345\256\232\345\255\220\345\272\217\345\210\227.md"
index 9218a52cfb96a672e2a07675d4d783d90b58b6a2..4e4931209e8ca6194218052bdf01339102c4583d 100644
--- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\345\210\244\345\256\232\345\255\220\345\272\217\345\210\227.md"
+++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\345\210\244\345\256\232\345\255\220\345\272\217\345\210\227.md"
@@ -1,9 +1,5 @@
# 二分查找高效判定子序列
-
-
-
-
@@ -13,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -230,6 +226,10 @@ int left_bound(ArrayList arr, int target) {
}
```
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\350\277\220\347\224\250.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\350\277\220\347\224\250.md"
new file mode 100644
index 0000000000000000000000000000000000000000..5eb132beb5397f24826e73d3bb83fc5995beab51
--- /dev/null
+++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\350\277\220\347\224\250.md"
@@ -0,0 +1,142 @@
+# 二分查找的实际运用
+
+
+
+
+
+
+
+
+![](https://labuladong.github.io/algo/images/souyisou1.png)
+
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
+
+
+
+读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
+
+| LeetCode | 力扣 | 难度 |
+| :----: | :----: | :----: |
+| [1011. Capacity To Ship Packages Within D Days](https://leetcode.com/problems/capacity-to-ship-packages-within-d-days/) | [1011. 在 D 天内送达包裹的能力](https://leetcode.cn/problems/capacity-to-ship-packages-within-d-days/) | 🟠
+| [410. Split Array Largest Sum](https://leetcode.com/problems/split-array-largest-sum/) | [410. 分割数组的最大值](https://leetcode.cn/problems/split-array-largest-sum/) | 🔴
+| [875. Koko Eating Bananas](https://leetcode.com/problems/koko-eating-bananas/) | [875. 爱吃香蕉的珂珂](https://leetcode.cn/problems/koko-eating-bananas/) | 🟠
+| - | [剑指 Offer II 073. 狒狒吃香蕉](https://leetcode.cn/problems/nZZqjQ/) | 🟠
+
+**-----------**
+
+我们前文 [我写了首诗,把二分搜索变成了默写题](https://labuladong.github.io/article/fname.html?fname=二分查找详解) 详细介绍了二分搜索的细节问题,探讨了「搜索一个元素」,「搜索左侧边界」,「搜索右侧边界」这三个情况,教你如何写出正确无 bug 的二分搜索算法。
+
+**但是前文总结的二分搜索代码框架仅仅局限于「在有序数组中搜索指定元素」这个基本场景,具体的算法问题没有这么直接,可能你都很难看出这个问题能够用到二分搜索**。
+
+所以本文就来总结一套二分搜索算法运用的框架套路,帮你在遇到二分搜索算法相关的实际问题时,能够有条理地思考分析,步步为营,写出答案。
+
+### 原始的二分搜索代码
+
+二分搜索的原型就是在「**有序数组**」中搜索一个元素 `target`,返回该元素对应的索引。
+
+如果该元素不存在,那可以返回一个什么特殊值,这种细节问题只要微调算法实现就可实现。
+
+还有一个重要的问题,如果「**有序数组**」中存在多个 `target` 元素,那么这些元素肯定挨在一起,这里就涉及到算法应该返回最左侧的那个 `target` 元素的索引还是最右侧的那个 `target` 元素的索引,也就是所谓的「搜索左侧边界」和「搜索右侧边界」,这个也可以通过微调算法的代码来实现。
+
+**我们前文 [我写了首诗,把二分搜索变成了默写题](https://labuladong.github.io/article/fname.html?fname=二分查找详解) 详细探讨了上述问题,对这块还不清楚的读者建议复习前文**,已经搞清楚基本二分搜索算法的读者可以继续看下去。
+
+**在具体的算法问题中,常用到的是「搜索左侧边界」和「搜索右侧边界」这两种场景**,很少有让你单独「搜索一个元素」。
+
+因为算法题一般都让你求最值,比如让你求吃香蕉的「最小速度」,让你求轮船的「最低运载能力」,求最值的过程,必然是搜索一个边界的过程,所以后面我们就详细分析一下这两种搜索边界的二分算法代码。
+
+「搜索左侧边界」的二分搜索算法的具体代码实现如下:
+
+```java
+// 搜索左侧边界
+int left_bound(int[] nums, int target) {
+ if (nums.length == 0) return -1;
+ int left = 0, right = nums.length;
+
+ while (left < right) {
+ int mid = left + (right - left) / 2;
+ if (nums[mid] == target) {
+ // 当找到 target 时,收缩右侧边界
+ right = mid;
+ } else if (nums[mid] < target) {
+ left = mid + 1;
+ } else if (nums[mid] > target) {
+ right = mid;
+ }
+ }
+ return left;
+}
+```
+
+假设输入的数组 `nums = [1,2,3,3,3,5,7]`,想搜索的元素 `target = 3`,那么算法就会返回索引 2。
+
+如果画一个图,就是这样:
+
+![](https://labuladong.github.io/algo/images/二分运用/1.jpeg)
+
+「搜索右侧边界」的二分搜索算法的具体代码实现如下:
+
+```java
+// 搜索右侧边界
+int right_bound(int[] nums, int target) {
+ if (nums.length == 0) return -1;
+ int left = 0, right = nums.length;
+
+ while (left < right) {
+ int mid = left + (right - left) / 2;
+ if (nums[mid] == target) {
+ // 当找到 target 时,收缩左侧边界
+ left = mid + 1;
+ } else if (nums[mid] < target) {
+ left = mid + 1;
+ } else if (nums[mid] > target) {
+ right = mid;
+ }
+ }
+ return left - 1;
+}
+```
+
+输入同上,那么算法就会返回索引 4,如果画一个图,就是这样:
+
+![](https://labuladong.github.io/algo/images/二分运用/2.jpeg)
+
+好,上述内容都属于复习,我想读到这里的读者应该都能理解。记住上述的图像,所有能够抽象出上述图像的问题,都可以使用二分搜索解决。
+
+
+
+
+
+引用本文的文章
+
+ - [丑数系列算法详解](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=阶乘题目)
+
+
+
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| [1201. Ugly Number III](https://leetcode.com/problems/ugly-number-iii/?show=1) | [1201. 丑数 III](https://leetcode.cn/problems/ugly-number-iii/?show=1) |
+| - | [剑指 Offer II 073. 狒狒吃香蕉](https://leetcode.cn/problems/nZZqjQ/?show=1) |
+
+
+
+
+
+**_____________**
+
+应合作方要求,本文不便在此发布,请扫码关注回复关键词「二分」或 [点这里](https://appktavsiei5995.pc.xiaoe-tech.com/detail/i_627cce2de4b01a4851fe0ed1/1) 查看:
+
+![](https://labuladong.github.io/algo/images/qrcode.jpg)
\ No newline at end of file
diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\210\244\346\226\255\345\233\236\346\226\207\351\223\276\350\241\250.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\210\244\346\226\255\345\233\236\346\226\207\351\223\276\350\241\250.md"
index 2697f54e2bf3e0a89360d4352d1a45c22a990108..4d0b2b86da206ef58372e4f76e487e28315ebc6a 100644
--- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\210\244\346\226\255\345\233\236\346\226\207\351\223\276\350\241\250.md"
+++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\210\244\346\226\255\345\233\236\346\226\207\351\223\276\350\241\250.md"
@@ -1,9 +1,5 @@
# 如何高效判断回文链表
-
-
-
-
@@ -13,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -265,6 +261,23 @@ p.next = reverse(q);
> 最后打个广告,我亲自制作了一门 [数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO),以视频课为主,手把手带你实现常用的数据结构及相关算法,旨在帮助算法基础较为薄弱的读者深入理解常用数据结构的底层原理,在算法学习中少走弯路。
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| - | [剑指 Offer II 027. 回文链表](https://leetcode.cn/problems/aMhZSa/?show=1) |
+
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\220\210\346\263\225\346\213\254\345\217\267\345\210\244\345\256\232.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\220\210\346\263\225\346\213\254\345\217\267\345\210\244\345\256\232.md"
deleted file mode 100644
index c7d1516bfc6e2d796f9db1dfb21a6cef20d5f095..0000000000000000000000000000000000000000
--- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\220\210\346\263\225\346\213\254\345\217\267\345\210\244\345\256\232.md"
+++ /dev/null
@@ -1,202 +0,0 @@
-# 如何判定括号合法性
-
-
-
-
-
-
-
-
-
-![](../pictures/souyisou.png)
-
-**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
-
-读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
-
-[20.有效的括号](https://leetcode-cn.com/problems/valid-parentheses)
-
-**-----------**
-
-对括号的合法性判断是一个很常见且实用的问题,比如说我们写的代码,编辑器和编译器都会检查括号是否正确闭合。而且我们的代码可能会包含三种括号 `[](){}`,判断起来有一点难度。
-
-本文就来聊一道关于括号合法性判断的算法题,相信能加深你对**栈**这种数据结构的理解。
-
-题目很简单,输入一个字符串,其中包含 `[](){}` 六种括号,请你判断这个字符串组成的括号是否合法。
-
-```
-Input: "()[]{}"
-Output: true
-
-Input: "([)]"
-Output: false
-
-Input: "{[]}"
-Output: true
-```
-
-解决这个问题之前,我们先降低难度,思考一下,**如果只有一种括号 `()`**,应该如何判断字符串组成的括号是否合法呢?
-
-### 一、处理一种括号
-
-字符串中只有圆括号,如果想让括号字符串合法,那么必须做到:
-
-**每个右括号 `)` 的左边必须有一个左括号 `(` 和它匹配**。
-
-比如说字符串 `()))((` 中,中间的两个右括号**左边**就没有左括号匹配,所以这个括号组合是不合法的。
-
-那么根据这个思路,我们可以写出算法:
-
-```cpp
-bool isValid(string str) {
- // 待匹配的左括号数量
- int left = 0;
- for (char c : str) {
- if (c == '(')
- left++;
- else // 遇到右括号
- left--;
-
- if (left < 0)
- return false;
- }
- return left == 0;
-}
-```
-
-如果只有圆括号,这样就能正确判断合法性。对于三种括号的情况,我一开始想模仿这个思路,定义三个变量 `left1`,`left2`,`left3` 分别处理每种括号,虽然要多写不少 if else 分支,但是似乎可以解决问题。
-
-但实际上直接照搬这种思路是不行的,比如说只有一个括号的情况下 `(())` 是合法的,但是多种括号的情况下, `[(])` 显然是不合法的。
-
-仅仅记录每种左括号出现的次数已经不能做出正确判断了,我们要加大存储的信息量,可以利用栈来模仿类似的思路。
-
-### 二、处理多种括号
-
-栈是一种先进后出的数据结构,处理括号问题的时候尤其有用。
-
-我们这道题就用一个名为 `left` 的栈代替之前思路中的 `left` 变量,**遇到左括号就入栈,遇到右括号就去栈中寻找最近的左括号,看是否匹配**。
-
-```cpp
-bool isValid(string str) {
- stack left;
- for (char c : str) {
- if (c == '(' || c == '{' || c == '[')
- left.push(c);
- else // 字符 c 是右括号
- if (!left.empty() && leftOf(c) == left.top())
- left.pop();
- else
- // 和最近的左括号不匹配
- return false;
- }
- // 是否所有的左括号都被匹配了
- return left.empty();
-}
-
-char leftOf(char c) {
- if (c == '}') return '{';
- if (c == ')') return '(';
- return '[';
-}
-```
-
-**_____________**
-
-**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**。
-
-**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。
-
-
-
-
-======其他语言代码======
-
-[20.有效的括号](https://leetcode-cn.com/problems/valid-parentheses)
-
-### python
-```python
-def isValid(self, s: str) -> bool:
- left = []
- leftOf = {
- ')':'(',
- ']':'[',
- '}':'{'
- }
- for c in s:
- if c in '([{':
- left.append(c)
- elif left and leftOf[c]==left[-1]: # 右括号 + left不为空 + 和最近左括号能匹配
- left.pop()
- else: # 右括号 + (left为空 / 和堆顶括号不匹配)
- return False
-
- # left中所有左括号都被匹配则return True 反之False
- return not left
-```
-
-
-
-### java
-
-```java
-//基本思想:每次遇到左括号时都将相对应的右括号')',']'或'}'推入堆栈
-//如果在字符串中出现右括号,则需要检查堆栈是否为空,以及顶部元素是否与该右括号相同。如果不是,则该字符串无效。
-//最后,我们还需要检查堆栈是否为空
-public boolean isValid(String s) {
- Deque stack = new ArrayDeque<>();
- for(char c : s.toCharArray()){
- //是左括号就将相对应的右括号入栈
- if(c=='(') {
- stack.offerLast(')');
- }else if(c=='{'){
- stack.offerLast('}');
- }else if(c=='['){
- stack.offerLast(']');
- }else if(stack.isEmpty() || stack.pollLast()!=c){//出现右括号,检查堆栈是否为空,以及顶部元素是否与该右括号相同
- return false;
- }
- }
- return stack.isEmpty();
-}
-
-
-```
-
-
-
-### javascript
-
-```js
-/**
- * @param {string} s是
- * @return {boolean}
- */
-var isValid = function (s) {
- let left = []
- for (let c of s) {
- if (c === '(' || c === '{' || c === '[') {
- left.push(c);
- } else {
- // 字符c是右括号
- //出现右括号,检查堆栈是否为空,以及顶部元素是否与该右括号相同
- if (left.length !== 0 && leftOf(c) === left[left.length - 1]) {
- left.pop();
- } else {
- // 和最近的左括号不匹配
- return false;
- }
- }
- }
- return left.length === 0;
-};
-
-
-let leftOf = function (c) {
- if (c === '}') return '{';
- if (c === ')') return '(';
- return '[';
-}
-```
-
-
-
diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\220\215\344\272\272\351\227\256\351\242\230.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\220\215\344\272\272\351\227\256\351\242\230.md"
new file mode 100644
index 0000000000000000000000000000000000000000..c298104debc55cbb768c0b41dff870f567027ee1
--- /dev/null
+++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\220\215\344\272\272\351\227\256\351\242\230.md"
@@ -0,0 +1,274 @@
+# 众里寻他千百度:找网红算法
+
+
+
+
+
+
+
+
+![](https://labuladong.github.io/algo/images/souyisou1.png)
+
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
+
+
+
+读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
+
+| LeetCode | 力扣 | 难度 |
+| :----: | :----: | :----: |
+| [277. Find the Celebrity](https://leetcode.com/problems/find-the-celebrity/)🔒 | [277. 搜寻名人](https://leetcode.cn/problems/find-the-celebrity/)🔒 | 🟠
+
+**-----------**
+
+今天来讨论经典的「名流问题」:
+
+给你 `n` 个人的社交关系(你知道任意两个人之间是否认识),然后请你找出这些人中的「名人」。
+
+所谓「名人」有两个条件:
+
+1、所有其他人都认识「名人」。
+
+2、「名人」不认识任何其他人。
+
+这是一个图相关的算法问题,社交关系嘛,本质上就可以抽象成一幅图。
+
+如果把每个人看做图中的节点,「认识」这种关系看做是节点之间的有向边,那么名人就是这幅图中一个特殊的节点:
+
+![](https://labuladong.github.io/algo/images/名人问题/1.jpeg)
+
+**这个节点没有一条指向其他节点的有向边;且其他所有节点都有一条指向这个节点的有向边**。
+
+或者说的专业一点,名人节点的出度为 0,入度为 `n - 1`。
+
+那么,这 `n` 个人的社交关系是如何表示的呢?
+
+前文 [图论算法基础](https://labuladong.github.io/article/fname.html?fname=图) 说过,图有两种存储形式,一种是邻接表,一种是邻接矩阵,邻接表的主要优势是节约存储空间;邻接矩阵的主要优势是可以迅速判断两个节点是否相邻。
+
+对于名人问题,显然会经常需要判断两个人之间是否认识,也就是两个节点是否相邻,所以我们可以用邻接矩阵来表示人和人之间的社交关系。
+
+那么,把名流问题描述成算法的形式就是这样的:
+
+给你输入一个大小为 `n x n` 的二维数组(邻接矩阵) `graph` 表示一幅有 `n` 个节点的图,每个人都是图中的一个节点,编号为 `0` 到 `n - 1`。
+
+如果 `graph[i][j] == 1` 代表第 `i` 个人认识第 `j` 个人,如果 `graph[i][j] == 0` 代表第 `i` 个人不认识第 `j` 个人。
+
+有了这幅图表示人与人之间的关系,请你计算,这 `n` 个人中,是否存在「名人」?
+
+如果存在,算法返回这个名人的编号,如果不存在,算法返回 -1。
+
+函数签名如下:
+
+```java
+int findCelebrity(int[][] graph);
+```
+
+比如输入的邻接矩阵长这样:
+
+![](https://labuladong.github.io/algo/images/名人问题/2.jpeg)
+
+那么算法应该返回 2。
+
+力扣第 277 题「搜寻名人」就是这个经典问题,不过并不是直接把邻接矩阵传给你,而是只告诉你总人数 `n`,同时提供一个 API `knows` 来查询人和人之间的社交关系:
+
+```java
+// 可以直接调用,能够返回 i 是否认识 j
+boolean knows(int i, int j);
+
+// 请你实现:返回「名人」的编号
+int findCelebrity(int n) {
+ // todo
+}
+```
+
+很明显,`knows` API 本质上还是在访问邻接矩阵。为了简单起见,我们后面就按力扣的题目形式来探讨一下这个经典问题。
+
+### 暴力解法
+
+我们拍拍脑袋就能写出一个简单粗暴的算法:
+
+```java
+int findCelebrity(int n) {
+ for (int cand = 0; cand < n; cand++) {
+ int other;
+ for (other = 0; other < n; other++) {
+ if (cand == other) continue;
+ // 保证其他人都认识 cand,且 cand 不认识任何其他人
+ // 否则 cand 就不可能是名人
+ if (knows(cand, other) || !knows(other, cand)) {
+ break;
+ }
+ }
+ if (other == n) {
+ // 找到名人
+ return cand;
+ }
+ }
+ // 没有一个人符合名人特性
+ return -1;
+}
+```
+
+`cand` 是候选人(candidate)的缩写,我们的暴力算法就是从头开始穷举,把每个人都视为候选人,判断是否符合「名人」的条件。
+
+刚才也说了,`knows` 函数底层就是在访问一个二维的邻接矩阵,一次调用的时间复杂度是 O(1),所以这个暴力解法整体的最坏时间复杂度是 O(N^2)。
+
+那么,是否有其他高明的办法来优化时间复杂度呢?其实是有优化空间的,你想想,我们现在最耗时的地方在哪里?
+
+对于每一个候选人 `cand`,我们都要用一个内层 for 循环去判断这个 `cand` 到底符不符合「名人」的条件。
+
+这个内层 for 循环看起来就蠢,虽然判断一个人「是名人」必须用一个 for 循环,但判断一个人「不是名人」就不用这么麻烦了。
+
+**因为「名人」的定义保证了「名人」的唯一性,所以我们可以利用排除法,先排除那些显然不是「名人」的人,从而避免 for 循环的嵌套,降低时间复杂度**。
+
+### 优化解法
+
+我再重复一遍所谓「名人」的定义:
+
+1、所有其他人都认识名人。
+
+2、名人不认识任何其他人。
+
+这个定义就很有意思,它保证了人群中最多有一个名人。
+
+这很好理解,如果有两个人同时是名人,那么这两条定义就自相矛盾了。
+
+**换句话说,只要观察任意两个候选人的关系,我一定能确定其中的一个人不是名人,把他排除**。
+
+至于另一个候选人是不是名人,只看两个人的关系肯定是不能确定的,但这不重要,重要的是排除掉一个必然不是名人的候选人,缩小了包围圈。
+
+这是优化的核心,也是比较难理解的,所以我们先来说说为什么观察任意两个候选人的关系,就能排除掉一个。
+
+你想想,两个人之间的关系可能是什么样的?
+
+无非就是四种:你认识我我不认识你,我认识你你不认识我,咱俩互相认识,咱两互相不认识。
+
+如果把人比作节点,红色的有向边表示不认识,绿色的有向边表示认识,那么两个人的关系无非是如下四种情况:
+
+![](https://labuladong.github.io/algo/images/名人问题/3.jpeg)
+
+不妨认为这两个人的编号分别是 `cand` 和 `other`,然后我们逐一分析每种情况,看看怎么排除掉一个人。
+
+对于情况一,`cand` 认识 `other`,所以 `cand` 肯定不是名人,排除。因为名人不可能认识别人。
+
+对于情况二,`other` 认识 `cand`,所以 `other` 肯定不是名人,排除。
+
+对于情况三,他俩互相认识,肯定都不是名人,可以随便排除一个。
+
+对于情况四,他俩互不认识,肯定都不是名人,可以随便排除一个。因为名人应该被所有其他人认识。
+
+综上,只要观察任意两个之间的关系,就至少能确定一个人不是名人,上述情况判断可以用如下代码表示:
+
+```java
+if (knows(cand, other) || !knows(other, cand)) {
+ // cand 不可能是名人
+} else {
+ // other 不可能是名人
+}
+```
+
+如果能够理解这一个特点,那么写出优化解法就简单了。
+
+**我们可以不断从候选人中选两个出来,然后排除掉一个,直到最后只剩下一个候选人,这时候再使用一个 for 循环判断这个候选人是否是货真价实的「名人」**。
+
+这个思路的完整代码如下:
+
+```java
+int findCelebrity(int n) {
+ if (n == 1) return 0;
+ // 将所有候选人装进队列
+ LinkedList q = new LinkedList<>();
+ for (int i = 0; i < n; i++) {
+ q.addLast(i);
+ }
+ // 一直排除,直到只剩下一个候选人停止循环
+ while (q.size() >= 2) {
+ // 每次取出两个候选人,排除一个
+ int cand = q.removeFirst();
+ int other = q.removeFirst();
+ if (knows(cand, other) || !knows(other, cand)) {
+ // cand 不可能是名人,排除,让 other 归队
+ q.addFirst(other);
+ } else {
+ // other 不可能是名人,排除,让 cand 归队
+ q.addFirst(cand);
+ }
+ }
+
+ // 现在排除得只剩一个候选人,判断他是否真的是名人
+ int cand = q.removeFirst();
+ for (int other = 0; other < n; other++) {
+ if (other == cand) {
+ continue;
+ }
+ // 保证其他人都认识 cand,且 cand 不认识任何其他人
+ if (!knows(other, cand) || knows(cand, other)) {
+ return -1;
+ }
+ }
+ // cand 是名人
+ return cand;
+}
+```
+
+这个算法避免了嵌套 for 循环,时间复杂度降为 O(N) 了,不过引入了一个队列来存储候选人集合,使用了 O(N) 的空间复杂度。
+
+> PS:`LinkedList` 的作用只是充当一个容器把候选人装起来,每次找出两个进行比较和淘汰,但至于具体找出哪两个,都是无所谓的,也就是说候选人归队的顺序无所谓,我们用的是 `addFirst` 只是方便后续的优化,你完全可以用 `addLast`,结果都是一样的。
+
+是否可以进一步优化,把空间复杂度也优化掉?
+
+### 最终解法
+
+如果你能够理解上面的优化解法,其实可以不需要额外的空间解决这个问题,代码如下:
+
+```java
+int findCelebrity(int n) {
+ // 先假设 cand 是名人
+ int cand = 0;
+ for (int other = 1; other < n; other++) {
+ if (!knows(other, cand) || knows(cand, other)) {
+ // cand 不可能是名人,排除
+ // 假设 other 是名人
+ cand = other;
+ } else {
+ // other 不可能是名人,排除
+ // 什么都不用做,继续假设 cand 是名人
+ }
+ }
+
+ // 现在的 cand 是排除的最后结果,但不能保证一定是名人
+ for (int other = 0; other < n; other++) {
+ if (cand == other) continue;
+ // 需要保证其他人都认识 cand,且 cand 不认识任何其他人
+ if (!knows(other, cand) || knows(cand, other)) {
+ return -1;
+ }
+ }
+
+ return cand;
+}
+```
+
+我们之前的解法用到了 `LinkedList` 充当一个队列,用于存储候选人集合,而这个优化解法利用 `other` 和 `cand` 的交替变化,模拟了我们之前操作队列的过程,避免了使用额外的存储空间。
+
+现在,解决名人问题的解法时间复杂度为 O(N),空间复杂度为 O(1),已经是最优解法了。
+
+
+
+
+
+引用本文的文章
+
+ - [二分图判定算法](https://labuladong.github.io/article/fname.html?fname=二分图)
+
+
+
+
+
+
+
+**_____________**
+
+**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
+
+![](https://labuladong.github.io/algo/images/souyisou2.png)
diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\246\202\344\275\225\345\216\273\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\347\232\204\351\207\215\345\244\215\345\205\203\347\264\240.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\246\202\344\275\225\345\216\273\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\347\232\204\351\207\215\345\244\215\345\205\203\347\264\240.md"
deleted file mode 100644
index 35218b3c26a760012d77978745c95f871e6f08ea..0000000000000000000000000000000000000000
--- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\246\202\344\275\225\345\216\273\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\347\232\204\351\207\215\345\244\215\345\205\203\347\264\240.md"
+++ /dev/null
@@ -1,190 +0,0 @@
-# 如何去除有序数组的重复元素
-
-我们知道对于数组来说,在尾部插入、删除元素是比较高效的,时间复杂度是 O(1),但是如果在中间或者开头插入、删除元素,就会涉及数据的搬移,时间复杂度为 O(N),效率较低。
-
-所以对于一般处理数组的算法问题,我们要尽可能只对数组尾部的元素进行操作,以避免额外的时间复杂度。
-
-这篇文章讲讲如何对一个有序数组去重,先看下题目:
-
-![](../pictures/%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E5%8E%BB%E9%87%8D/title.png)
-
-显然,由于数组已经排序,所以重复的元素一定连在一起,找出它们并不难,但如果毎找到一个重复元素就立即删除它,就是在数组中间进行删除操作,整个时间复杂度是会达到 O(N^2)。而且题目要求我们原地修改,也就是说不能用辅助数组,空间复杂度得是 O(1)。
-
-其实,**对于数组相关的算法问题,有一个通用的技巧:要尽量避免在中间删除元素,那我就先想办法把这个元素换到最后去**。这样的话,最终待删除的元素都拖在数组尾部,一个一个 pop 掉就行了,每次操作的时间复杂度也就降低到 O(1) 了。
-
-按照这个思路呢,又可以衍生出解决类似需求的通用方式:双指针技巧。具体一点说,应该是快慢指针。
-
-我们让慢指针 `slow` 走左后面,快指针 `fast` 走在前面探路,找到一个不重复的元素就告诉 `slow` 并让 `slow` 前进一步。这样当 `fast` 指针遍历完整个数组 `nums` 后,**`nums[0..slow]` 就是不重复元素,之后的所有元素都是重复元素**。
-
-```java
-int removeDuplicates(int[] nums) {
- int n = nums.length;
- if (n == 0) return 0;
- int slow = 0, fast = 1;
- while (fast < n) {
- if (nums[fast] != nums[slow]) {
- slow++;
- // 维护 nums[0..slow] 无重复
- nums[slow] = nums[fast];
- }
- fast++;
- }
- // 长度为索引 + 1
- return slow + 1;
-}
-```
-
-看下算法执行的过程:
-
-![](../pictures/%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E5%8E%BB%E9%87%8D/1.gif)
-
-再简单扩展一下,如果给你一个有序链表,如何去重呢?其实和数组是一模一样的,唯一的区别是把数组赋值操作变成操作指针而已:
-
-```java
-ListNode deleteDuplicates(ListNode head) {
- if (head == null) return null;
- ListNode slow = head, fast = head.next;
- while (fast != null) {
- if (fast.val != slow.val) {
- // nums[slow] = nums[fast];
- slow.next = fast;
- // slow++;
- slow = slow.next;
- }
- // fast++
- fast = fast.next;
- }
- // 断开与后面重复元素的连接
- slow.next = null;
- return head;
-}
-```
-
-![](../pictures/%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E5%8E%BB%E9%87%8D/2.gif)
-
-
-**_____________**
-
-**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**。
-
-**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。
-
-
-
-
-
-======其他语言代码======
-
-[26. 删除排序数组中的重复项](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/)
-
-[83. 删除排序链表中的重复元素](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list/)
-
-### python
-
-[eric wang](https://www.github.com/eric496) 提供有序数组 Python3 代码
-
-```python
-def removeDuplicates(self, nums: List[int]) -> int:
- n = len(nums)
-
- if n == 0:
- return 0
-
- slow, fast = 0, 1
-
- while fast < n:
- if nums[fast] != nums[slow]:
- slow += 1
- nums[slow] = nums[fast]
-
- fast += 1
-
- return slow + 1
-```
-
-[eric wang](https://www.github.com/eric496) 提供有序链表 Python3 代码
-
-```python
-def deleteDuplicates(self, head: ListNode) -> ListNode:
- if not head:
- return head
-
- slow, fast = head, head.next
-
- while fast:
- if fast.val != slow.val:
- slow.next = fast
- slow = slow.next
-
- fast = fast.next
-
- # 断开与后面重复元素的连接
- slow.next = None
- return head
-```
-
-
-
-### javascript
-
-[26. 删除排序数组中的重复项](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/)
-
-```js
-/**
- * @param {number[]} nums
- * @return {number}
- */
-var removeDuplicates = function(nums) {
- let n = nums.length;
- if (n === 0) return 0;
- if (n === 1) return 1;
- let slow = 0, fast = 1;
- while (fast < n) {
- if (nums[fast] !== nums[slow]) {
- slow++;
- // 维护nums[0...slow]无重复
- nums[slow] = nums[fast];
- }
- fast++;
- }
-
- // 长度为索引+1
- return slow + 1;
-};
-```
-
-[83. 删除排序链表中的重复元素](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list/)
-
-```js
-/**
- * Definition for singly-linked list.
- * function ListNode(val, next) {
- * this.val = (val===undefined ? 0 : val)
- * this.next = (next===undefined ? null : next)
- * }
- */
-/**
- * @param {ListNode} head
- * @return {ListNode}
- */
-var deleteDuplicates = function (head) {
- if (head == null) return null;
- let slow = head, fast = head.next;
- while (fast != null) {
- if(fast.val !== slow.val){
- // nums[slow] = nums[fast];
- slow.next = fast;
- // slow++;
- slow = slow.next;
- }
-
- // fast++
- fast = fast.next;
- }
-
- // 断开与后面重复元素的连接
- slow.next = null;
- return head;
-}
-```
-
diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\255\220\351\233\206\346\216\222\345\210\227\347\273\204\345\220\210.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\255\220\351\233\206\346\216\222\345\210\227\347\273\204\345\220\210.md"
index 7f5a951b821f9376a14dd153d64469715ed3b23c..60908ccece4e01101952832aa2b7b9244903fb6e 100644
--- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\255\220\351\233\206\346\216\222\345\210\227\347\273\204\345\220\210.md"
+++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\255\220\351\233\206\346\216\222\345\210\227\347\273\204\345\220\210.md"
@@ -1,9 +1,5 @@
# 一文秒杀所有排列组合子集问题
-
-
-
-
@@ -13,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -922,6 +918,43 @@ void backtrack(int[] nums) {
如果你能够看到这里,真得给你鼓掌,相信你以后遇到各种乱七八糟的算法题,也能一眼看透它们的本质,以不变应万变。另外,考虑到篇幅,本文并没有对这些算法进行复杂度的分析,你可以使用我在 [算法时空复杂度分析实用指南](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=时间复杂度)
+
+
+
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| [17. Letter Combinations of a Phone Number](https://leetcode.com/problems/letter-combinations-of-a-phone-number/?show=1) | [17. 电话号码的字母组合](https://leetcode.cn/problems/letter-combinations-of-a-phone-number/?show=1) |
+| [491. Increasing Subsequences](https://leetcode.com/problems/increasing-subsequences/?show=1) | [491. 递增子序列](https://leetcode.cn/problems/increasing-subsequences/?show=1) |
+| - | [剑指 Offer 38. 字符串的排列](https://leetcode.cn/problems/zi-fu-chuan-de-pai-lie-lcof/?show=1) |
+| - | [剑指 Offer II 079. 所有子集](https://leetcode.cn/problems/TVdhkn/?show=1) |
+| - | [剑指 Offer II 080. 含有 k 个元素的组合](https://leetcode.cn/problems/uUsW3B/?show=1) |
+| - | [剑指 Offer II 081. 允许重复选择元素的组合](https://leetcode.cn/problems/Ygoe9J/?show=1) |
+| - | [剑指 Offer II 083. 没有重复元素集合的全排列](https://leetcode.cn/problems/VvJkup/?show=1) |
+
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\256\211\346\216\222\344\274\232\350\256\256\345\256\244.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\256\211\346\216\222\344\274\232\350\256\256\345\256\244.md"
new file mode 100644
index 0000000000000000000000000000000000000000..89b1b3171aeeeb5d526103b0ffd3aed57fe43b09
--- /dev/null
+++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\256\211\346\216\222\344\274\232\350\256\256\345\256\244.md"
@@ -0,0 +1,202 @@
+# 扫描线技巧解决会议室安排问题
+
+
+
+
+
+
+
+
+![](https://labuladong.github.io/algo/images/souyisou1.png)
+
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
+
+
+
+读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
+
+| LeetCode | 力扣 | 难度 |
+| :----: | :----: | :----: |
+| [253. Meeting Rooms II](https://leetcode.com/problems/meeting-rooms-ii/)🔒 | [253. 会议室 II](https://leetcode.cn/problems/meeting-rooms-ii/)🔒 | 🟠
+
+**-----------**
+
+之前面试,被问到一道非常经典且非常实用的算法题目:会议室安排问题。
+
+力扣上类似的问题是会员题目,你可能没办法做,但对于这种经典的算法题,掌握思路还是必要的。
+
+先说下题目,力扣第 253 题「会议室 II」:
+
+给你输入若干形如 `[begin, end]` 的区间,代表若干会议的开始时间和结束时间,请你计算至少需要申请多少间会议室。
+
+函数签名如下:
+
+```java
+// 返回需要申请的会议室数量
+int minMeetingRooms(int[][] meetings);
+```
+
+比如给你输入 `meetings = [[0,30],[5,10],[15,20]]`,算法应该返回 2,因为后两个会议和第一个会议时间是冲突的,至少申请两个会议室才能让所有会议顺利进行。
+
+如果会议之间的时间有重叠,那就得额外申请会议室来开会,想求至少需要多少间会议室,就是让你计算同一时刻最多有多少会议在同时进行。
+
+换句话说,**如果把每个会议的起始时间看做一个线段区间,那么题目就是让你求最多有几个重叠区间**,仅此而已。
+
+对于这种时间安排的问题,本质上讲就是区间调度问题,十有八九得排序,然后找规律来解决。
+
+### 题目延伸
+
+我们之前写过很多区间调度相关的文章,这里就顺便帮大家梳理一下这类问题的思路:
+
+**第一个场景**,假设现在只有一个会议室,还有若干会议,你如何将尽可能多的会议安排到这个会议室里?
+
+这个问题需要将这些会议(区间)按结束时间(右端点)排序,然后进行处理,详见前文 [贪心算法做时间管理](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=区间问题合集)。
+
+**第六个场景**,假设现在只有一个会议室,还有若干会议,如何安排会议才能使这个会议室的闲置时间最少?
+
+这个问题需要动动脑筋,说白了这就是个 0-1 背包问题的变形:
+
+会议室可以看做一个背包,每个会议可以看做一个物品,物品的价值就是会议的时长,请问你如何选择物品(会议)才能最大化背包中的价值(会议室的使用时长)?
+
+当然,这里背包的约束不是一个最大重量,而是各个物品(会议)不能互相冲突。把各个会议按照结束时间进行排序,然后参考前文 [0-1 背包问题详解](https://labuladong.github.io/article/fname.html?fname=背包问题) 的思路即可解决,等我以后有机会可以写一写这个问题。
+
+**第七个场景**,就是本文想讲的场景,给你若干会议,让你合理申请会议室。
+
+好了,举例了这么多,来看看今天的这个问题如何解决。
+
+### 题目分析
+
+重复一下题目的本质:
+
+**给你输入若干时间区间,让你计算同一时刻「最多」有几个区间重叠**。
+
+题目的关键点在于,给你任意一个时刻,你是否能够说出这个时刻有几个会议?
+
+如果可以做到,那我遍历所有的时刻,找个最大值,就是需要申请的会议室数量。
+
+有没有一种数据结构或者算法,给我输入若干区间,我能知道每个位置有多少个区间重叠?
+
+老读者肯定可以联想到之前说过的一个算法技巧:[差分数组技巧](https://labuladong.github.io/article/fname.html?fname=差分技巧)。
+
+把时间线想象成一个初始值为 0 的数组,每个时间区间 `[i, j]` 就相当于一个子数组,这个时间区间有一个会议,那我就把这个子数组中的元素都加一。
+
+最后,每个时刻有几个会议我不就知道了吗?我遍历整个数组,不就知道至少需要几间会议室了吗?
+
+举例来说,如果输入 `meetings = [[0,30],[5,10],[15,20]]`,那么我们就给数组中 `[0,30],[5,10],[15,20]` 这几个索引区间分别加一,最后遍历数组,求个最大值就行了。
+
+还记得吗,差分数组技巧可以在 O(1) 时间对整个区间的元素进行加减,所以可以拿来解决这道题。
+
+不过,这个解法的效率不算高,所以我这里不准备具体写差分数组的解法,参照 [差分数组技巧](https://labuladong.github.io/article/fname.html?fname=差分技巧) 的原理,有兴趣的读者可以自己尝试去实现。
+
+**基于差分数组的思路,我们可以推导出一种更高效,更优雅的解法**。
+
+我们首先把这些会议的时间区间进行投影:
+
+![](https://labuladong.github.io/algo/images/安排会议室/1.jpeg)
+
+红色的点代表每个会议的开始时间点,绿色的点代表每个会议的结束时间点。
+
+现在假想有一条带着计数器的线,在时间线上从左至右进行扫描,每遇到红色的点,计数器 `count` 加一,每遇到绿色的点,计数器 `count` 减一:
+
+![](https://labuladong.github.io/algo/images/安排会议室/2.jpeg)
+
+**这样一来,每个时刻有多少个会议在同时进行,就是计数器 `count` 的值,`count` 的最大值,就是需要申请的会议室数量**。
+
+对差分数组技巧熟悉的读者一眼就能看出来了,这个扫描线其实就是差分数组的遍历过程,所以我们说这是差分数组技巧衍生出来的解法。
+
+### 代码实现
+
+那么,如何写代码实现这个扫描的过程呢?
+
+首先,对区间进行投影,就相当于对每个区间的起点和终点分别进行排序:
+
+![](https://labuladong.github.io/algo/images/安排会议室/3.jpeg)
+
+```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);
+
+ // ...
+}
+```
+
+然后就简单了,扫描线从左向右前进,遇到红点就对计数器加一,遇到绿点就对计数器减一,计数器 `count` 的最大值就是答案:
+
+```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++;
+ }
+ // 记录扫描过程中的最大值
+ res = Math.max(res, count);
+ }
+
+ return res;
+}
+```
+
+这里使用的是 [双指针技巧](https://labuladong.github.io/article/fname.html?fname=双指针技巧),根据 `i, j` 的相对位置模拟扫描线前进的过程。
+
+至此,这道题就做完了。当然,这个题目也可以变形,比如给你若干会议,问你 `k` 个会议室够不够用,其实你套用本文的解法代码,也可以很轻松解决。
+
+接下来可阅读:
+
+* [区间问题系列合集](https://labuladong.github.io/article/fname.html?fname=区间问题合集)
+
+
+
+
+
+**_____________**
+
+**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
+
+![](https://labuladong.github.io/algo/images/souyisou2.png)
\ No newline at end of file
diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\262\233\345\261\277\351\242\230\347\233\256.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\262\233\345\261\277\351\242\230\347\233\256.md"
new file mode 100644
index 0000000000000000000000000000000000000000..85b30e811c780b3b2755988c39287f918b7b264d
--- /dev/null
+++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\262\233\345\261\277\351\242\230\347\233\256.md"
@@ -0,0 +1,534 @@
+# DFS 算法秒杀岛屿系列题目
+
+
+
+
+
+
+
+
+![](https://labuladong.github.io/algo/images/souyisou1.png)
+
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
+
+
+
+读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
+
+| LeetCode | 力扣 | 难度 |
+| :----: | :----: | :----: |
+| [1020. Number of Enclaves](https://leetcode.com/problems/number-of-enclaves/) | [1020. 飞地的数量](https://leetcode.cn/problems/number-of-enclaves/) | 🟠
+| [1254. Number of Closed Islands](https://leetcode.com/problems/number-of-closed-islands/) | [1254. 统计封闭岛屿的数目](https://leetcode.cn/problems/number-of-closed-islands/) | 🟠
+| [1905. Count Sub Islands](https://leetcode.com/problems/count-sub-islands/) | [1905. 统计子岛屿](https://leetcode.cn/problems/count-sub-islands/) | 🟠
+| [200. Number of Islands](https://leetcode.com/problems/number-of-islands/) | [200. 岛屿数量](https://leetcode.cn/problems/number-of-islands/) | 🟠
+| [694. Number of Distinct Islands](https://leetcode.com/problems/number-of-distinct-islands/)🔒 | [694. 不同岛屿的数量](https://leetcode.cn/problems/number-of-distinct-islands/)🔒 | 🟠
+| [695. Max Area of Island](https://leetcode.com/problems/max-area-of-island/) | [695. 岛屿的最大面积](https://leetcode.cn/problems/max-area-of-island/) | 🟠
+| - | [剑指 Offer II 105. 岛屿的最大面积](https://leetcode.cn/problems/ZL6zAn/) | 🟠
+
+**-----------**
+
+岛屿系列算法问题是经典的面试高频题,虽然基本的问题并不难,但是这类问题有一些有意思的扩展,比如求子岛屿数量,求形状不同的岛屿数量等等,本文就来把这些问题一网打尽。
+
+**岛屿系列题目的核心考点就是用 DFS/BFS 算法遍历二维数组**。
+
+本文主要来讲解如何用 DFS 算法来秒杀岛屿系列题目,不过用 BFS 算法的核心思路是完全一样的,无非就是把 DFS 改写成 BFS 而已。
+
+那么如何在二维矩阵中使用 DFS 搜索呢?如果你把二维矩阵中的每一个位置看做一个节点,这个节点的上下左右四个位置就是相邻节点,那么整个矩阵就可以抽象成一幅网状的「图」结构。
+
+根据 [学习数据结构和算法的框架思维](https://labuladong.github.io/article/fname.html?fname=学习数据结构和算法的高效方法),完全可以根据二叉树的遍历框架改写出二维矩阵的 DFS 代码框架:
+
+```java
+// 二叉树遍历框架
+void traverse(TreeNode root) {
+ traverse(root.left);
+ traverse(root.right);
+}
+
+// 二维矩阵遍历框架
+void dfs(int[][] grid, int i, int j, boolean[][] visited) {
+ int m = grid.length, n = grid[0].length;
+ if (i < 0 || j < 0 || i >= m || j >= n) {
+ // 超出索引边界
+ return;
+ }
+ if (visited[i][j]) {
+ // 已遍历过 (i, j)
+ return;
+ }
+ // 进入节点 (i, j)
+ visited[i][j] = true;
+ dfs(grid, i - 1, j, visited); // 上
+ dfs(grid, i + 1, j, visited); // 下
+ dfs(grid, i, j - 1, visited); // 左
+ dfs(grid, i, j + 1, visited); // 右
+}
+```
+
+因为二维矩阵本质上是一幅「图」,所以遍历的过程中需要一个 `visited` 布尔数组防止走回头路,如果你能理解上面这段代码,那么搞定所有岛屿系列题目都很简单。
+
+这里额外说一个处理二维数组的常用小技巧,你有时会看到使用「方向数组」来处理上下左右的遍历,和前文 [图遍历框架](https://labuladong.github.io/article/fname.html?fname=图) 的代码很类似:
+
+```java
+// 方向数组,分别代表上、下、左、右
+int[][] dirs = new int[][]{{-1,0}, {1,0}, {0,-1}, {0,1}};
+
+void dfs(int[][] grid, int i, int j, boolean[][] visited) {
+ int m = grid.length, n = grid[0].length;
+ if (i < 0 || j < 0 || i >= m || j >= n) {
+ // 超出索引边界
+ return;
+ }
+ if (visited[i][j]) {
+ // 已遍历过 (i, j)
+ return;
+ }
+
+ // 进入节点 (i, j)
+ visited[i][j] = true;
+ // 递归遍历上下左右的节点
+ for (int[] d : dirs) {
+ int next_i = i + d[0];
+ int next_j = j + d[1];
+ dfs(grid, next_i, next_j, visited);
+ }
+ // 离开节点 (i, j)
+}
+```
+
+这种写法无非就是用 for 循环处理上下左右的遍历罢了,你可以按照个人喜好选择写法。
+
+### 岛屿数量
+
+这是力扣第 200 题「岛屿数量」,最简单也是最经典的一道问题,题目会输入一个二维数组 `grid`,其中只包含 `0` 或者 `1`,`0` 代表海水,`1` 代表陆地,且假设该矩阵四周都是被海水包围着的。
+
+我们说连成片的陆地形成岛屿,那么请你写一个算法,计算这个矩阵 `grid` 中岛屿的个数,函数签名如下:
+
+```java
+int numIslands(char[][] grid);
+```
+
+比如说题目给你输入下面这个 `grid` 有四片岛屿,算法应该返回 4:
+
+![](https://labuladong.github.io/algo/images/岛屿/1.jpg)
+
+思路很简单,关键在于如何寻找并标记「岛屿」,这就要 DFS 算法发挥作用了,我们直接看解法代码:
+
+```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);
+ }
+ }
+ }
+ 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) 变成海水
+ grid[i][j] = '0';
+ // 淹没上下左右的陆地
+ dfs(grid, i + 1, j);
+ dfs(grid, i, j + 1);
+ dfs(grid, i - 1, j);
+ dfs(grid, i, j - 1);
+}
+```
+
+**为什么每次遇到岛屿,都要用 DFS 算法把岛屿「淹了」呢?主要是为了省事,避免维护 `visited` 数组**。
+
+因为 `dfs` 函数遍历到值为 `0` 的位置会直接返回,所以只要把经过的位置都设置为 `0`,就可以起到不走回头路的作用。
+
+> PS:这类 DFS 算法还有个别名叫做 [FloodFill 算法](https://labuladong.github.io/article/fname.html?fname=FloodFill算法详解及应用),现在有没有觉得 FloodFill 这个名字还挺贴切的~
+
+这个最最基本的算法问题就说到这,我们来看看后面的题目有什么花样。
+
+### 封闭岛屿的数量
+
+上一题说二维矩阵四周可以认为也是被海水包围的,所以靠边的陆地也算作岛屿。
+
+力扣第 1254 题「统计封闭岛屿的数目」和上一题有两点不同:
+
+1、用 `0` 表示陆地,用 `1` 表示海水。
+
+2、让你计算「封闭岛屿」的数目。所谓「封闭岛屿」就是上下左右全部被 `1` 包围的 `0`,也就是说**靠边的陆地不算作「封闭岛屿」**。
+
+函数签名如下:
+
+```java
+int closedIsland(int[][] grid)
+```
+
+比如题目给你输入如下这个二维矩阵:
+
+![](https://labuladong.github.io/algo/images/岛屿/2.png)
+
+算法返回 2,只有图中灰色部分的 `0` 是四周全都被海水包围着的「封闭岛屿」。
+
+**那么如何判断「封闭岛屿」呢?其实很简单,把上一题中那些靠边的岛屿排除掉,剩下的不就是「封闭岛屿」了吗**?
+
+有了这个思路,就可以直接看代码了,注意这题规定 `0` 表示陆地,用 `1` 表示海水:
+
+```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++) {
+ for (int j = 0; j < n; j++) {
+ if (grid[i][j] == 0) {
+ res++;
+ dfs(grid, i, j);
+ }
+ }
+ }
+ 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) 变成海水
+ grid[i][j] = 1;
+ // 淹没上下左右的陆地
+ dfs(grid, i + 1, j);
+ dfs(grid, i, j + 1);
+ dfs(grid, i - 1, j);
+ dfs(grid, i, j - 1);
+}
+```
+
+只要提前把靠边的陆地都淹掉,然后算出来的就是封闭岛屿了。
+
+> PS:处理这类岛屿题目除了 DFS/BFS 算法之外,Union Find 并查集算法也是一种可选的方法,前文 [Union Find 算法运用](https://labuladong.github.io/article/fname.html?fname=UnionFind算法详解) 就用 Union Find 算法解决了一道类似的问题。
+
+这道岛屿题目的解法稍微改改就可以解决力扣第 1020 题「飞地的数量」,这题不让你求封闭岛屿的数量,而是求封闭岛屿的面积总和。
+
+其实思路都是一样的,先把靠边的陆地淹掉,然后去数剩下的陆地数量就行了,注意第 1020 题中 `1` 代表陆地,`0` 代表海水:
+
+```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++) {
+ for (int j = 0; j < n; j++) {
+ if (grid[i][j] == 1) {
+ res += 1;
+ }
+ }
+ }
+
+ return res;
+}
+
+// 和之前的实现类似
+void dfs(int[][] grid, int i, int j) {
+ // ...
+}
+```
+
+篇幅所限,具体代码我就不写了,我们继续看其他的岛屿题目。
+
+### 岛屿的最大面积
+
+这是力扣第 695 题「岛屿的最大面积」,`0` 表示海水,`1` 表示陆地,现在不让你计算岛屿的个数了,而是让你计算最大的那个岛屿的面积,函数签名如下:
+
+```java
+int maxAreaOfIsland(int[][] grid)
+```
+
+比如题目给你输入如下一个二维矩阵:
+
+![](https://labuladong.github.io/algo/images/岛屿/3.jpg)
+
+其中面积最大的是橘红色的岛屿,算法返回它的面积 6。
+
+**这题的大体思路和之前完全一样,只不过 `dfs` 函数淹没岛屿的同时,还应该想办法记录这个岛屿的面积**。
+
+我们可以给 `dfs` 函数设置返回值,记录每次淹没的陆地的个数,直接看解法吧:
+
+```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));
+ }
+ }
+ }
+ 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;
+
+ return dfs(grid, i + 1, j)
+ + dfs(grid, i, j + 1)
+ + dfs(grid, i - 1, j)
+ + dfs(grid, i, j - 1) + 1;
+}
+```
+
+解法和之前相比差不多,我也不多说了,接下来的两道岛屿题目是比较有技巧性的,我们重点来看一下。
+
+### 子岛屿数量
+
+如果说前面的题目都是模板题,那么力扣第 1905 题「统计子岛屿」可能得动动脑子了:
+
+![](https://labuladong.github.io/algo/images/岛屿/4.jpg)
+
+**这道题的关键在于,如何快速判断子岛屿**?肯定可以借助 [Union Find 并查集算法](https://labuladong.github.io/article/fname.html?fname=UnionFind算法详解) 来判断,不过本文重点在 DFS 算法,就不展开并查集算法了。
+
+什么情况下 `grid2` 中的一个岛屿 `B` 是 `grid1` 中的一个岛屿 `A` 的子岛?
+
+当岛屿 `B` 中所有陆地在岛屿 `A` 中也是陆地的时候,岛屿 `B` 是岛屿 `A` 的子岛。
+
+**反过来说,如果岛屿 `B` 中存在一片陆地,在岛屿 `A` 的对应位置是海水,那么岛屿 `B` 就不是岛屿 `A` 的子岛**。
+
+那么,我们只要遍历 `grid2` 中的所有岛屿,把那些不可能是子岛的岛屿排除掉,剩下的就是子岛。
+
+依据这个思路,可以直接写出下面的代码:
+
+```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);
+ }
+ }
+ }
+ // 现在 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;
+}
+
+// 从 (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);
+}
+```
+
+这道题的思路和计算「封闭岛屿」数量的思路有些类似,只不过后者排除那些靠边的岛屿,前者排除那些不可能是子岛的岛屿。
+
+### 不同的岛屿数量
+
+这是本文的最后一道岛屿题目,作为压轴题,当然是最有意思的。
+
+力扣第 694 题「不同的岛屿数量」,题目还是输入一个二维矩阵,`0` 表示海水,`1` 表示陆地,这次让你计算 **不同的 (distinct)** 岛屿数量,函数签名如下:
+
+```java
+int numDistinctIslands(int[][] grid)
+```
+
+比如题目输入下面这个二维矩阵:
+
+![](https://labuladong.github.io/algo/images/岛屿/5.jpg)
+
+其中有四个岛屿,但是左下角和右上角的岛屿形状相同,所以不同的岛屿共有三个,算法返回 3。
+
+很显然我们得想办法把二维矩阵中的「岛屿」进行转化,变成比如字符串这样的类型,然后利用 HashSet 这样的数据结构去重,最终得到不同的岛屿的个数。
+
+如果想把岛屿转化成字符串,说白了就是序列化,序列化说白了就是遍历嘛,前文 [二叉树的序列化和反序列化](https://labuladong.github.io/article/fname.html?fname=二叉树的序列化) 讲了二叉树和字符串互转,这里也是类似的。
+
+**首先,对于形状相同的岛屿,如果从同一起点出发,`dfs` 函数遍历的顺序肯定是一样的**。
+
+因为遍历顺序是写死在你的递归函数里面的,不会动态改变:
+
+```java
+void dfs(int[][] grid, int i, int j) {
+ // 递归顺序:
+ dfs(grid, i - 1, j); // 上
+ dfs(grid, i + 1, j); // 下
+ dfs(grid, i, j - 1); // 左
+ dfs(grid, i, j + 1); // 右
+}
+```
+
+所以,遍历顺序从某种意义上说就可以用来描述岛屿的形状,比如下图这两个岛屿:
+
+![](https://labuladong.github.io/algo/images/岛屿/6.png)
+
+假设它们的遍历顺序是:
+
+> 下,右,上,撤销上,撤销右,撤销下
+
+如果我用分别用 `1, 2, 3, 4` 代表上下左右,用 `-1, -2, -3, -4` 代表上下左右的撤销,那么可以这样表示它们的遍历顺序:
+
+> 2, 4, 1, -1, -4, -2
+
+**你看,这就相当于是岛屿序列化的结果,只要每次使用 `dfs` 遍历岛屿的时候生成这串数字进行比较,就可以计算到底有多少个不同的岛屿了**。
+
+> PS:细心的读者问到,为什么记录「撤销」操作才能唯一表示遍历顺序呢?不记录撤销操作好像也可以?实际上不是的。
+>
+> 比方说「下,右,撤销右,撤销下」和「下,撤销下,右,撤销右」显然是两个不同的遍历顺序,但如果不记录撤销操作,那么它俩都是「下,右」,成了相同的遍历顺序,显然是不对的。
+
+所以我们需要稍微改造 `dfs` 函数,添加一些函数参数以便记录遍历顺序:
+
+```java
+void dfs(int[][] grid, int i, int j, StringBuilder sb, int dir) {
+ int m = grid.length, n = grid[0].length;
+ if (i < 0 || j < 0 || i >= m || j >= n
+ || grid[i][j] == 0) {
+ return;
+ }
+ // 前序遍历位置:进入 (i, j)
+ grid[i][j] = 0;
+ sb.append(dir).append(',');
+
+ dfs(grid, i - 1, j, sb, 1); // 上
+ dfs(grid, i + 1, j, sb, 2); // 下
+ dfs(grid, i, j - 1, sb, 3); // 左
+ dfs(grid, i, j + 1, sb, 4); // 右
+
+ // 后序遍历位置:离开 (i, j)
+ sb.append(-dir).append(',');
+}
+```
+
+`dir` 记录方向,`dfs` 函数递归结束后,`sb` 记录着整个遍历顺序。有了这个 `dfs` 函数就好办了,我们可以直接写出最后的解法代码:
+
+```java
+int numDistinctIslands(int[][] grid) {
+ int m = grid.length, n = grid[0].length;
+ // 记录所有岛屿的序列化结果
+ HashSet islands = new HashSet<>();
+ for (int i = 0; i < m; i++) {
+ for (int j = 0; j < n; j++) {
+ if (grid[i][j] == 1) {
+ // 淹掉这个岛屿,同时存储岛屿的序列化结果
+ StringBuilder sb = new StringBuilder();
+ // 初始的方向可以随便写,不影响正确性
+ dfs(grid, i, j, sb, 666);
+ islands.add(sb.toString());
+ }
+ }
+ }
+ // 不相同的岛屿数量
+ return islands.size();
+}
+```
+
+这样,这道题就解决了,至于为什么初始调用 `dfs` 函数时的 `dir` 参数可以随意写,这里涉及 DFS 和回溯算法的一个细微差别,前文 [图算法基础](https://labuladong.github.io/article/fname.html?fname=图) 有写,这里就不展开了。
+
+以上就是全部岛屿系列题目的解题思路,也许前面的题目大部分人会做,但是最后两题还是比较巧妙的,希望本文对你有帮助。
+
+
+
+
+
+引用本文的文章
+
+ - [并查集(Union-Find)算法](https://labuladong.github.io/article/fname.html?fname=UnionFind算法详解)
+
+
+
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| - | [剑指 Offer 13. 机器人的运动范围](https://leetcode.cn/problems/ji-qi-ren-de-yun-dong-fan-wei-lcof/?show=1) |
+| - | [剑指 Offer II 105. 岛屿的最大面积](https://leetcode.cn/problems/ZL6zAn/?show=1) |
+
+
+
+
+
+**_____________**
+
+**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
+
+![](https://labuladong.github.io/algo/images/souyisou2.png)
\ No newline at end of file
diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\272\247\344\275\215\350\260\203\345\272\246.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\272\247\344\275\215\350\260\203\345\272\246.md"
index 40360a1bd116e10551013b9fb40f394589f12fb2..831668e151ee630c3080b6263e98fd27c2ce8066 100644
--- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\272\247\344\275\215\350\260\203\345\272\246.md"
+++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\272\247\344\275\215\350\260\203\345\272\246.md"
@@ -1,9 +1,5 @@
# 如何调度考生的座位
-
-
-
-
@@ -13,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -228,6 +224,10 @@ private int distance(int[] intv) {
希望本文对大家有帮助。
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\211\223\345\215\260\347\264\240\346\225\260.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\211\223\345\215\260\347\264\240\346\225\260.md"
index 64c7141c95867742cb17b70c1e3db468c3eff861..60794c355d2db9c3f9da83cb8af1530a4db9d618 100644
--- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\211\223\345\215\260\347\264\240\346\225\260.md"
+++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\211\223\345\215\260\347\264\240\346\225\260.md"
@@ -1,9 +1,5 @@
# 如何高效寻找素数
-
-
-
-
@@ -13,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -178,6 +174,33 @@ int countPrimes(int n) {
以上就是素数算法相关的全部内容。怎么样,是不是看似简单的问题却有不少细节可以打磨呀?
+
+
+
+
+引用本文的文章
+
+ - [丑数系列算法详解](https://labuladong.github.io/article/fname.html?fname=丑数)
+
+
+
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| [264. Ugly Number II](https://leetcode.com/problems/ugly-number-ii/?show=1) | [264. 丑数 II](https://leetcode.cn/problems/ugly-number-ii/?show=1) |
+
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\216\245\351\233\250\346\260\264.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\216\245\351\233\250\346\260\264.md"
index ae394ae1c1dfd1815e40ea94208f71e0d8b5c145..d566a6f94ee83cdd3b1a728e02c23751fd9aaa21 100644
--- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\216\245\351\233\250\346\260\264.md"
+++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\216\245\351\233\250\346\260\264.md"
@@ -1,9 +1,7 @@
# 接雨水问题详解
-
+
-
-
@@ -13,7 +11,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -278,6 +276,10 @@ if (height[left] < height[right]) {
至此,这道题也解决了。
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\344\270\262.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\344\270\262.md"
deleted file mode 100644
index 41fef26bac5ee1ac0587de0506b08b820acf3811..0000000000000000000000000000000000000000
--- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\344\270\262.md"
+++ /dev/null
@@ -1,246 +0,0 @@
-# 如何寻找最长回文子串
-
-
-
-
-
-
-
-
-
-
-![](../pictures/souyisou.png)
-
-**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
-
-读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
-
-[5.最长回文子串](https://leetcode-cn.com/problems/longest-palindromic-substring)
-
-**-----------**
-
-回文串是面试常常遇到的问题(虽然问题本身没啥意义),本文就告诉你回文串问题的核心思想是什么。
-
-首先,明确一下什:**回文串就是正着读和反着读都一样的字符串**。
-
-比如说字符串 `aba` 和 `abba` 都是回文串,因为它们对称,反过来还是和本身一样。反之,字符串 `abac` 就不是回文串。
-
-可以看到回文串的的长度可能是奇数,也可能是偶数,这就添加了回文串问题的难度,解决该类问题的核心是**双指针**。下面就通过一道最长回文子串的问题来具体理解一下回文串问题:
-
-![](../pictures/回文/title.png)
-
-```cpp
-string longestPalindrome(string s) {}
-```
-
-### 一、思考
-
-对于这个问题,我们首先应该思考的是,给一个字符串 `s`,如何在 `s` 中找到一个回文子串?
-
-有一个很有趣的思路:既然回文串是一个正着反着读都一样的字符串,那么如果我们把 `s` 反转,称为 `s'`,然后在 `s` 和 `s'` 中寻找**最长公共子串**,这样应该就能找到最长回文子串。
-
-比如说字符串 `abacd`,反过来是 `dcaba`,它的最长公共子串是 `aba`,也就是最长回文子串。
-
-但是这个思路是错误的,比如说字符串 `aacxycaa`,反转之后是 `aacyxcaa`,最长公共子串是 `aac`,但是最长回文子串应该是 `aa`。
-
-虽然这个思路不正确,但是**这种把问题转化为其他形式的思考方式是非常值得提倡的**。
-
-下面,就来说一下正确的思路,如何使用双指针。
-
-**寻找回文串的问题核心思想是:从中间开始向两边扩散来判断回文串**。对于最长回文子串,就是这个意思:
-
-```python
-for 0 <= i < len(s):
- 找到以 s[i] 为中心的回文串
- 更新答案
-```
-
-但是呢,我们刚才也说了,回文串的长度可能是奇数也可能是偶数,如果是 `abba`这种情况,没有一个中心字符,上面的算法就没辙了。所以我们可以修改一下:
-
-```python
-for 0 <= i < len(s):
- 找到以 s[i] 为中心的回文串
- 找到以 s[i] 和 s[i+1] 为中心的回文串
- 更新答案
-```
-
-PS:读者可能发现这里的索引会越界,等会会处理。
-
-### 二、代码实现
-
-按照上面的思路,先要实现一个函数来寻找最长回文串,这个函数是有点技巧的:
-
-```cpp
-string palindrome(string& s, int l, int r) {
- // 防止索引越界
- while (l >= 0 && r < s.size()
- && s[l] == s[r]) {
- // 向两边展开
- l--; r++;
- }
- // 返回以 s[l] 和 s[r] 为中心的最长回文串
- return s.substr(l + 1, r - l - 1);
-}
-```
-
-为什么要传入两个指针 `l` 和 `r` 呢?**因为这样实现可以同时处理回文串长度为奇数和偶数的情况**:
-
-```python
-for 0 <= i < len(s):
- # 找到以 s[i] 为中心的回文串
- palindrome(s, i, i)
- # 找到以 s[i] 和 s[i+1] 为中心的回文串
- palindrome(s, i, i + 1)
- 更新答案
-```
-
-下面看下 `longestPalindrome` 的完整代码:
-
-```cpp
-string longestPalindrome(string s) {
- string res;
- for (int i = 0; i < s.size(); i++) {
- // 以 s[i] 为中心的最长回文子串
- string s1 = palindrome(s, i, i);
- // 以 s[i] 和 s[i+1] 为中心的最长回文子串
- string s2 = palindrome(s, i, i + 1);
- // res = longest(res, s1, s2)
- res = res.size() > s1.size() ? res : s1;
- res = res.size() > s2.size() ? res : s2;
- }
- return res;
-}
-```
-
-至此,这道最长回文子串的问题就解决了,时间复杂度 O(N^2),空间复杂度 O(1)。
-
-值得一提的是,这个问题可以用动态规划方法解决,时间复杂度一样,但是空间复杂度至少要 O(N^2) 来存储 DP table。这道题是少有的动态规划非最优解法的问题。
-
-另外,这个问题还有一个巧妙的解法,时间复杂度只需要 O(N),不过该解法比较复杂,我个人认为没必要掌握。该算法的名字叫 Manacher's Algorithm(马拉车算法),有兴趣的读者可以自行搜索一下。
-
-
-**_____________**
-
-**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**。
-
-**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。
-
-
-
-
-======其他语言代码======
-
-[5.最长回文子串](https://leetcode-cn.com/problems/longest-palindromic-substring)
-
-### java
-
-[cchromt](https://github.com/cchroot) 提供 Java 代码:
-
-```java
-// 中心扩展算法
-class Solution {
- public String longestPalindrome(String s) {
- // 如果字符串长度小于2,则直接返回其本身
- if (s.length() < 2) {
- return s;
- }
- String res = "";
- for (int i = 0; i < s.length() - 1; i++) {
- // 以 s.charAt(i) 为中心的最长回文子串
- String s1 = palindrome(s, i, i);
- // 以 s.charAt(i) 和 s.charAt(i+1) 为中心的最长回文子串
- String s2 = palindrome(s, i, i + 1);
- res = res.length() > s1.length() ? res : s1;
- res = res.length() > s2.length() ? res : s2;
- }
- return res;
- }
-
- public String palindrome(String s, int left, int right) {
- // 索引未越界的情况下,s.charAt(left) == s.charAt(right) 则继续向两边拓展
- while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
- left--;
- right++;
- }
- // 这里要注意,跳出 while 循环时,恰好满足 s.charAt(i) != s.charAt(j),因此截取的的字符串为[left+1, right-1]
- return s.substring(left + 1, right);
- }
-}
-```
-
-
-[enrilwang](https://github.com/enrilwang) 提供 Python 代码:
-
-```python
-# 中心扩展算法
-class Solution:
- def longestPalindrome(self, s: str) -> str:
- #用n来装字符串长度,res来装答案
- n = len(s)
- res = str()
- #字符串长度小于2,就返回本身
- if n < 2: return s
- for i in range(n-1):
- #oddstr是以i为中心的最长回文子串
- oddstr = self.centerExtend(s,i,i)
- #evenstr是以i和i+1为中心的最长回文子串
- evenstr = self.centerExtend(s,i,i+1)
- temp = oddstr if len(oddstr)>len(evenstr) else evenstr
- if len(temp)>len(res):res=temp
-
- return res
-
- def centerExtend(self,s:str,left,right)->str:
-
- while left >= 0 and right < len(s) and s[left] == s[right]:
- left -= 1
- right += 1
- #这里要注意,跳出while循环时,恰好s[left] != s[right]
- return s[left+1:right]
-
-
-```
-
-
-做完这题,大家可以去看看 [647. 回文子串](https://leetcode-cn.com/problems/palindromic-substrings/) ,也是类似的题目
-
-
-
-### javascript
-
-```js
-/**
- * @param {string} s
- * @return {string}
- */
-var longestPalindrome = function (s) {
- let res = "";
- for (let i = 0; i < s.length; i++) {
- // 以s[i]为中心的最长回文子串
- let s1 = palindrome(s,i,i);
-
- // 以 s[i] 和 s[i+1] 为中心的最长回文子串
- let s2 = palindrome(s, i, i + 1);
-
- // res = longest(res, s1, s2)
- res = res.length > s1.length ? res : s1;
- res = res.length > s2.length ? res : s2;
- }
-};
-
-// 寻找最长回文串
-let palindrome = (s, l, r) => {
- // 防止索引越界
- while (l >= 0 && r < s.length && s[l] === s[r]) {
- // 向两边展开
- l--;
- r++;
- }
-
- // 返回以s[l]和s[r]为中心的最长回文串
- return s.substr(l + 1, r - l - 1)
-}
-```
-
-
-
diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\260\264\345\241\230\346\212\275\346\240\267.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\260\264\345\241\230\346\212\275\346\240\267.md"
index d19837f57d07c4b8b28425f6b034718ddbba0143..2977f7a291a06f23d959d59ff1d9630e6e4ef0e5 100644
--- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\260\264\345\241\230\346\212\275\346\240\267.md"
+++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\260\264\345\241\230\346\212\275\346\240\267.md"
@@ -1,9 +1,5 @@
# 随机算法之水塘抽样算法
-
-
-
-
@@ -13,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -128,6 +124,23 @@ int[] getRandom(ListNode head, int k) {
答案见 [我的这篇文章](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=随机集合)
+
+
+
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\266\210\345\244\261\347\232\204\345\205\203\347\264\240.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\266\210\345\244\261\347\232\204\345\205\203\347\264\240.md"
deleted file mode 100644
index 54c0731cf6751ca279028840e159d5d69f9195cb..0000000000000000000000000000000000000000
--- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\266\210\345\244\261\347\232\204\345\205\203\347\264\240.md"
+++ /dev/null
@@ -1,293 +0,0 @@
-# 如何寻找消失的元素
-
-
-
-
-
-
-
-
-
-![](../pictures/souyisou.png)
-
-**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
-
-读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
-
-[448.找到所有数组中消失的数字](https://leetcode-cn.com/problems/find-all-numbers-disappeared-in-an-array)
-
-**-----------**
-
-之前也有文章写过几个有趣的智力题,今天再聊一道巧妙的题目。
-
-题目非常简单:
-
-![](../pictures/缺失元素/title.png)
-
-给一个长度为 n 的数组,其索引应该在 `[0,n)`,但是现在你要装进去 n + 1 个元素 `[0,n]`,那么肯定有一个元素装不下嘛,请你找出这个缺失的元素。
-
-这道题不难的,我们应该很容易想到,把这个数组排个序,然后遍历一遍,不就很容易找到缺失的那个元素了吗?
-
-或者说,借助数据结构的特性,用一个 HashSet 把数组里出现的数字都储存下来,再遍历 `[0,n]` 之间的数字,去 HashSet 中查询,也可以很容易查出那个缺失的元素。
-
-排序解法的时间复杂度是 O(NlogN),HashSet 的解法时间复杂度是 O(N),但是还需要 O(N) 的空间复杂度存储 HashSet。
-
-**第三种方法是位运算**。
-
-对于异或运算(`^`),我们知道它有一个特殊性质:一个数和它本身做异或运算结果为 0,一个数和 0 做异或运算还是它本身。
-
-而且异或运算满足交换律和结合律,也就是说:
-
-2 ^ 3 ^ 2 = 3 ^ (2 ^ 2) = 3 ^ 0 = 3
-
-而这道题索就可以通过这些性质巧妙算出缺失的那个元素。比如说 `nums = [0,3,1,4]`:
-
-![](../pictures/缺失元素/1.jpg)
-
-
-为了容易理解,我们假设先把索引补一位,然后让每个元素和自己相等的索引相对应:
-
-![](../pictures/缺失元素/2.jpg)
-
-
-这样做了之后,就可以发现除了缺失元素之外,所有的索引和元素都组成一对儿了,现在如果把这个落单的索引 2 找出来,也就找到了缺失的那个元素。
-
-如何找这个落单的数字呢,**只要把所有的元素和索引做异或运算,成对儿的数字都会消为 0,只有这个落单的元素会剩下**,也就达到了我们的目的。
-
-```java
-int missingNumber(int[] nums) {
- int n = nums.length;
- int res = 0;
- // 先和新补的索引异或一下
- res ^= n;
- // 和其他的元素、索引做异或
- for (int i = 0; i < n; i++)
- res ^= i ^ nums[i];
- return res;
-}
-```
-
-![](../pictures/缺失元素/3.jpg)
-
-由于异或运算满足交换律和结合律,所以总是能把成对儿的数字消去,留下缺失的那个元素的。
-
-至此,时间复杂度 O(N),空间复杂度 O(1),已经达到了最优,我们是否就应该打道回府了呢?
-
-如果这样想,说明我们受算法的毒害太深,随着我们学习的知识越来越多,反而容易陷入思维定式,这个问题其实还有一个特别简单的解法:**等差数列求和公式**。
-
-题目的意思可以这样理解:现在有个等差数列 0, 1, 2,..., n,其中少了某一个数字,请你把它找出来。那这个数字不就是 `sum(0,1,..n) - sum(nums)` 嘛?
-
-```java
-int missingNumber(int[] nums) {
- int n = nums.length;
- // 公式:(首项 + 末项) * 项数 / 2
- int expect = (0 + n) * (n + 1) / 2;
-
- int sum = 0;
- for (int x : nums)
- sum += x;
- return expect - sum;
-}
-```
-
-你看,这种解法应该是最简单的,但说实话,我自己也没想到这个解法,而且我去问了几个大佬,他们也没想到这个最简单的思路。相反,如果去问一个初中生,他也许很快就能想到。
-
-做到这一步了,我们是否就应该打道回府了呢?
-
-如果这样想,说明我们对细节的把控还差点火候。在用求和公式计算 `expect` 时,你考虑过**整型溢出**吗?如果相乘的结果太大导致溢出,那么结果肯定是错误的。
-
-刚才我们的思路是把两个和都加出来然后相减,为了避免溢出,干脆一边求和一边减算了。很类似刚才位运算解法的思路,仍然假设 `nums = [0,3,1,4]`,先补一位索引再让元素跟索引配对:
-
-![](../pictures/缺失元素/xor.png)
-
-
-我们让每个索引减去其对应的元素,再把相减的结果加起来,不就是那个缺失的元素吗?
-
-```java
-public int missingNumber(int[] nums) {
- int n = nums.length;
- int res = 0;
- // 新补的索引
- res += n - 0;
- // 剩下索引和元素的差加起来
- for (int i = 0; i < n; i++)
- res += i - nums[i];
- return res;
-}
-```
-
-由于加减法满足交换律和结合律,所以总是能把成对儿的数字消去,留下缺失的那个元素的。
-
-至此这道算法题目经历九曲十八弯,终于再也没有什么坑了。
-
-
-**_____________**
-
-**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**。
-
-**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**。
-
-
-
-
-======其他语言代码======
-
-[剑指 Offer 53 - II. 0~n-1中缺失的数字](https://leetcode-cn.com/problems/que-shi-de-shu-zi-lcof/)
-
-[448.找到所有数组中消失的数字](https://leetcode-cn.com/problems/find-all-numbers-disappeared-in-an-array)
-
-
-
-### python
-
-```python
-def missingNumber(self, nums: List[int]) -> int:
- #思路1,位运算
- res = len(nums)
- for i,num in enumerate(nums):
- res ^= i^num
- return res
-```
-
-```python
-def missingNumber(self, nums: List[int]) -> int:
- #思路2,求和
- n = len(nums)
- return n*(n+1)//2-sum(nums)
-```
-
-```python
-def missingNumber(self, nums: List[int]) -> int:
- #思路3,防止整形溢出的优化
- res = len(nums)
- for i,num in enumerate(nums):
- res+=i-num
- return res
-```
-
-事实上,在python3中不存在整数溢出的问题(只要内存放得下),思路3的优化提升并不大,不过看上去有内味了哈...
-
-### c++
-
-[happy-yuxuan](https://github.com/happy-yuxuan) 提供 三种方法的 C++ 代码:
-
-```c++
-// 方法:异或元素和索引
-int missingNumber(vector& nums) {
- int n = nums.size();
- int res = 0;
- // 先和新补的索引异或一下
- res ^= n;
- // 和其他的元素、索引做异或
- for (int i = 0; i < n; i++)
- res ^= i ^ nums[i];
- return res;
-}
-```
-
-```c++
-// 方法:等差数列求和
-int missingNumber(vector& nums) {
- int n = nums.size();
- // 公式:(首项 + 末项) * 项数 / 2
- int expect = (0 + n) * (n + 1) / 2;
- int sum = 0;
- for (int x : nums)
- sum += x;
- return expect - sum;
-}
-```
-
-```c++
-// 方法:防止整型溢出
-int missingNumber(vector& nums) {
- int n = nums.size();
- int res = 0;
- // 新补的索引
- res += n - 0;
- // 剩下索引和元素的差加起来
- for (int i = 0; i < n; i++)
- res += i - nums[i];
- return res;
-}
-```
-
-
-
-### javascript
-
-[传送门:剑指 Offer 53 - II. 0~n-1中缺失的数字](https://leetcode-cn.com/problems/que-shi-de-shu-zi-lcof/)
-
-**位运算**
-
-```js
-/**
- * @param {number[]} nums
- * @return {number}
- */
-var missingNumber = function(nums) {
- let n = nums.length;
- let res = 0;
-
- // 先和新补的索引异或一下
- res ^= n;
-
- // 和其它的元素、索引做异或
- for (let i = 0; i < n; i++) {
- res ^= i ^ nums[i];
- }
- return res;
-};
-```
-
-**直接相减**
-
-```js
-/**
- * @param {number[]} nums
- * @return {number}
- */
-var missingNumber = function(nums) {
- let n = nums.length;
- let res = 0;
- // 新补的索引
- res += n - 0;
-
- // 剩下索引和元素的差加起来
- for (let i = 0; i < n; i++) {
- res += i - nums[i];
- }
- return res;
-};
-```
-
-
-
-[传送门:448. 找到所有数组中消失的数字](https://leetcode-cn.com/problems/find-all-numbers-disappeared-in-an-array/)
-
-这道题的核心思路是将访问过的元素变成负数,第二次遍历直接收集正数并加入结果集中。
-
-```js
-/**
- * @param {number[]} nums
- * @return {number[]}
- */
-var findDisappearedNumbers = function (nums) {
- for (let i = 0; i < nums.length; i++) {
- let newIndex = Math.abs(nums[i]) - 1;
- if (nums[newIndex] > 0) {
- nums[newIndex] *= -1;
- }
- }
-
- let result = [];
- for (let i = 1; i <= nums.length; i++) {
- if (nums[i - 1] > 0) {
- result.push(i);
- }
- }
- return result;
-};
-
-```
-
diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\347\274\272\345\244\261\345\222\214\351\207\215\345\244\215\347\232\204\345\205\203\347\264\240.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\347\274\272\345\244\261\345\222\214\351\207\215\345\244\215\347\232\204\345\205\203\347\264\240.md"
index 391d744b92f36f22c3ef59ee60f9be390e80ec55..1677d0580cbc383ec13ea0b6d54be801faff3b46 100644
--- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\347\274\272\345\244\261\345\222\214\351\207\215\345\244\215\347\232\204\345\205\203\347\264\240.md"
+++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\347\274\272\345\244\261\345\222\214\351\207\215\345\244\215\347\232\204\345\205\203\347\264\240.md"
@@ -1,9 +1,5 @@
# 如何寻找缺失和重复的元素
-
-
-
-
@@ -13,7 +9,7 @@
![](https://labuladong.github.io/algo/images/souyisou1.png)
-**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。**
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
@@ -25,7 +21,7 @@
**-----------**
-今天就聊一道很看起来简单却十分巧妙的问题,寻找缺失和重复的元素。之前的一篇文章 [常用的位操作](算法思维系列/常用的位操作.md) 中也写过类似的问题,不过这次的和上次的问题使用的技巧不同。
+今天就聊一道很看起来简单却十分巧妙的问题,寻找缺失和重复的元素。之前的一篇文章 [常用的位操作](https://labuladong.github.io/article/fname.html?fname=常用的位操作) 中也写过类似的问题,不过这次的和上次的问题使用的技巧不同。
这是力扣第 645 题「错误的集合」,我来描述一下这个题目:
@@ -132,6 +128,24 @@ int[] findErrorNums(int[] nums) {
异或运算也是常用的,因为异或性质 `a ^ a = 0, a ^ 0 = a`,如果将索引和元素同时异或,就可以消除成对儿的索引和元素,留下的就是重复或者缺失的元素。可以看看前文 [常用的位运算](https://labuladong.github.io/article/fname.html?fname=常用的位操作),介绍过这种方法。
+
+
+
+
+
+引用本文的题目
+
+安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:
+
+| LeetCode | 力扣 |
+| :----: | :----: |
+| [442. Find All Duplicates in an Array](https://leetcode.com/problems/find-all-duplicates-in-an-array/?show=1) | [442. 数组中重复的数据](https://leetcode.cn/problems/find-all-duplicates-in-an-array/?show=1) |
+| [448. Find All Numbers Disappeared in an Array](https://leetcode.com/problems/find-all-numbers-disappeared-in-an-array/?show=1) | [448. 找到所有数组中消失的数字](https://leetcode.cn/problems/find-all-numbers-disappeared-in-an-array/?show=1) |
+
+
+
+
+
**_____________**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\351\232\217\346\234\272\346\235\203\351\207\215.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\351\232\217\346\234\272\346\235\203\351\207\215.md"
new file mode 100644
index 0000000000000000000000000000000000000000..f89fdc28a1ea5aa67b5b9a910f337614376cfbdb
--- /dev/null
+++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\351\232\217\346\234\272\346\235\203\351\207\215.md"
@@ -0,0 +1,225 @@
+# 带权重的随机选择算法
+
+
+
+
+
+
+
+
+![](https://labuladong.github.io/algo/images/souyisou1.png)
+
+**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
+
+
+
+读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
+
+| LeetCode | 力扣 | 难度 |
+| :----: | :----: | :----: |
+| [528. Random Pick with Weight](https://leetcode.com/problems/random-pick-with-weight/) | [528. 按权重随机选择](https://leetcode.cn/problems/random-pick-with-weight/) | 🟠
+| - | [剑指 Offer II 071. 按权重生成随机数](https://leetcode.cn/problems/cuyjEf/) | 🟠
+
+**-----------**
+
+写这篇的文章的原因是玩 LOL 手游。
+
+我有个朋友抱怨说打排位匹配的队友太菜了,我就说我打排位觉得队友都挺行的啊,好像不怎么坑?
+
+朋友意味深长地说了句:一般隐藏分比较高的玩家,排位如果排不到实力相当的队友,就会排到一些菜狗。
+
+嗯?我想了几秒钟感觉这小伙子不对劲,他意思是说我隐藏分低,还是说我就是那条菜狗?
+
+我立马要求和他开黑打一把,证明我不是菜狗,他才是菜狗。开黑结果这里不便透露,大家猜猜吧。
+
+打完之后我就来发文了,因为我对游戏的匹配机制有了一点思考。
+
+![](https://labuladong.github.io/algo/images/随机权重/images.png)
+
+**所谓「隐藏分」我不知道是不是真的,毕竟匹配机制是所有竞技类游戏的核心环节,想必非常复杂,不是简单几个指标就能搞定的**。
+
+但是如果把这个「隐藏分」机制简化,倒是一个值得思考的算法问题:系统如何以不同的随机概率进行匹配?
+
+或者简单点说,如何带权重地做随机选择?
+
+不要觉得这个很容易,如果给你一个长度为 `n` 的数组,让你从中等概率随机抽取一个元素,你肯定会做,random 一个 `[0, n-1]` 的数字出来作为索引就行了,每个元素被随机选到的概率都是 `1/n`。
+
+但假设每个元素都有不同的权重,权重地大小代表随机选到这个元素的概率大小,你如何写算法去随机获取元素呢?
+
+力扣第 528 题「按权重随机选择」就是这样一个问题:
+
+![](https://labuladong.github.io/algo/images/随机权重/title.png)
+
+我们就来思考一下这个问题,解决按照权重随机选择元素的问题。
+
+### 解法思路
+
+首先回顾一下我们和随机算法有关的历史文章:
+
+前文 [设计随机删除元素的数据结构](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=二分查找详解) 能够解决带权重的随机选择算法**。
+
+这个随机算法和前缀和技巧和二分搜索技巧能扯上啥关系?且听我慢慢道来。
+
+假设给你输入的权重数组是 `w = [1,3,2,1]`,我们想让概率符合权重,那么可以抽象一下,根据权重画出这么一条彩色的线段:
+
+![](https://labuladong.github.io/algo/images/随机权重/1.jpeg)
+
+如果我在线段上面随机丢一个石子,石子落在哪个颜色上,我就选择该颜色对应的权重索引,那么每个索引被选中的概率是不是就是和权重相关联了?
+
+**所以,你再仔细看看这条彩色的线段像什么?这不就是 [前缀和数组](https://labuladong.github.io/article/fname.html?fname=前缀和技巧) 嘛**:
+
+![](https://labuladong.github.io/algo/images/随机权重/2.jpeg)
+
+那么接下来,如何模拟在线段上扔石子?
+
+当然是随机数,比如上述前缀和数组 `preSum`,取值范围是 `[1, 7]`,那么我生成一个在这个区间的随机数 `target = 5`,就好像在这条线段中随机扔了一颗石子:
+
+![](https://labuladong.github.io/algo/images/随机权重/3.jpeg)
+
+还有个问题,`preSum` 中并没有 5 这个元素,我们应该选择比 5 大的最小元素,也就是 6,即 `preSum` 数组的索引 3:
+
+![](https://labuladong.github.io/algo/images/随机权重/4.jpeg)
+
+**如何快速寻找数组中大于等于目标值的最小元素?[二分搜索算法](https://labuladong.github.io/article/fname.html?fname=二分查找详解) 就是我们想要的**。
+
+到这里,这道题的核心思路就说完了,主要分几步:
+
+1、根据权重数组 `w` 生成前缀和数组 `preSum`。
+
+2、生成一个取值在 `preSum` 之内的随机数,用二分搜索算法寻找大于等于这个随机数的最小元素索引。
+
+3、最后对这个索引减一(因为前缀和数组有一位索引偏移),就可以作为权重数组的索引,即最终答案:
+
+![](https://labuladong.github.io/algo/images/随机权重/5.jpeg)
+
+### 解法代码
+
+上述思路应该不难理解,但是写代码的时候坑可就多了。
+
+要知道涉及开闭区间、索引偏移和二分搜索的题目,需要你对算法的细节把控非常精确,否则会出各种难以排查的 bug。
+
+下面来抠细节,继续前面的例子:
+
+![](https://labuladong.github.io/algo/images/随机权重/3.jpeg)
+
+就比如这个 `preSum` 数组,你觉得随机数 `target` 应该在什么范围取值?闭区间 `[0, 7]` 还是左闭右开 `[0, 7)`?
+
+都不是,应该在闭区间 `[1, 7]` 中选择,**因为前缀和数组中 0 本质上是个占位符**,仔细体会一下:
+
+![](https://labuladong.github.io/algo/images/随机权重/6.jpeg)
+
+所以要这样写代码:
+
+```java
+int n = preSum.length;
+// target 取值范围是闭区间 [1, preSum[n - 1]]
+int target = rand.nextInt(preSum[n - 1]) + 1;
+```
+
+接下来,在 `preSum` 中寻找大于等于 `target` 的最小元素索引,应该用什么品种的二分搜索?搜索左侧边界的还是搜索右侧边界的?
+
+实际上应该使用搜索左侧边界的二分搜索:
+
+```java
+// 搜索左侧边界的二分搜索
+int left_bound(int[] nums, int target) {
+ if (nums.length == 0) return -1;
+ int left = 0, right = nums.length;
+ while (left < right) {
+ int mid = left + (right - left) / 2;
+ if (nums[mid] == target) {
+ right = mid;
+ } else if (nums[mid] < target) {
+ left = mid + 1;
+ } else if (nums[mid] > target) {
+ right = mid;
+ }
+ }
+ return left;
+}
+```
+
+前文 [二分搜索详解](https://labuladong.github.io/article/fname.html?fname=二分查找详解) 着重讲了数组中存在目标元素重复的情况,没仔细讲目标元素不存在的情况,这里补充一下。
+
+**当目标元素 `target` 不存在数组 `nums` 中时,搜索左侧边界的二分搜索的返回值可以做以下几种解读**:
+
+1、返回的这个值是 `nums` 中大于等于 `target` 的最小元素索引。
+
+2、返回的这个值是 `target` 应该插入在 `nums` 中的索引位置。
+
+3、返回的这个值是 `nums` 中小于 `target` 的元素个数。
+
+比如在有序数组 `nums = [2,3,5,7]` 中搜索 `target = 4`,搜索左边界的二分算法会返回 2,你带入上面的说法,都是对的。
+
+所以以上三种解读都是等价的,可以根据具体题目场景灵活运用,显然这里我们需要的是第一种。
+
+综上,我们可以写出最终解法代码:
+
+```java
+class Solution {
+ // 前缀和数组
+ private int[] preSum;
+ private Random rand = new Random();
+
+ public Solution(int[] w) {
+ int n = w.length;
+ // 构建前缀和数组,偏移一位留给 preSum[0]
+ preSum = new int[n + 1];
+ preSum[0] = 0;
+ // preSum[i] = sum(w[0..i-1])
+ for (int i = 1; i <= n; i++) {
+ preSum[i] = preSum[i - 1] + w[i - 1];
+ }
+ }
+
+ public int pickIndex() {
+ int n = preSum.length;
+ // 在闭区间 [1, preSum[n - 1]] 中随机选择一个数字
+ int target = rand.nextInt(preSum[n - 1]) + 1;
+ // 获取 target 在前缀和数组 preSum 中的索引
+ // 别忘了前缀和数组 preSum 和原始数组 w 有一位索引偏移
+ return left_bound(preSum, target) - 1;
+ }
+
+ // 搜索左侧边界的二分搜索
+ private int left_bound(int[] nums, int target) {
+ // 见上文
+ }
+}
+```
+
+有了之前的铺垫,相信你能够完全理解上述代码,这道随机权重的题目就解决了。
+
+经常有读者留言调侃,每次都是看我的文章「云刷题」,看完就会了,也不用亲自动手刷了。
+
+但我想说的是,很多题目思路一说就懂,但是深入一些的话很多细节都可能有坑,本文讲的这道题就是一个例子,所以还是建议多实践,多总结。
+
+后续我准备在核心读者群开展每天刷题打卡的活动,帮大家走出第一步,培养每天刷题 + 总结的习惯,有兴趣的读者在公众号后台回复关键词「核心群」加我微信。
+
+> 最后打个广告,我亲自制作了一门 [数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO),以视频课为主,手把手带你实现常用的数据结构及相关算法,旨在帮助算法基础较为薄弱的读者深入理解常用数据结构的底层原理,在算法学习中少走弯路。
+
+
+
+
+
+引用本文的文章
+
+ - [如何在无限序列中随机抽取元素](https://labuladong.github.io/article/fname.html?fname=水塘抽样)
+
+
+
+
+
+
+
+**_____________**
+
+**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**:
+
+![](https://labuladong.github.io/algo/images/souyisou2.png)