提交 4d64cc98 编写于 作者: L labuladong

feat: 更新所有内容

上级 993fe821
# 关于 Linux shell 你必须知道的技巧
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号-@labuladong-000000.svg?style=flat-square&logo=WeChat"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>
![](../pictures/souyisou.png)
![](https://labuladong.github.io/algo/images/souyisou1.png)
**通知:[数据结构精品课 V1.8](https://aep.h5.xeknow.com/s/1XJHEO) 持续更新中;[第十期刷题打卡挑战](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 最后一天报名。**
**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
**-----------**
......@@ -53,7 +58,7 @@ $ rm $(where connect.sh)
**标准输入就是编程语言中诸如`scanf`或者`readline`这种命令;而参数是指程序的`main`函数传入的`args`字符数组**
前文「Linux文件描述符」说过,管道符和重定向符是将数据作为程序的标准输入,而`$(cmd)`是读取`cmd`命令输出的数据作为参数。
前文 [Linux文件描述符](https://labuladong.github.io/article/fname.html?fname=linux进程) 说过,管道符和重定向符是将数据作为程序的标准输入,而`$(cmd)`是读取`cmd`命令输出的数据作为参数。
用刚才的例子说,`rm`命令源代码中肯定不接受标准输入,而是接收命令行参数,删除相应的文件。作为对比,`cat`命令是既接受标准输入,又接受命令行参数:
......@@ -104,7 +109,7 @@ $ logout
$ nohup some_cmd &
```
`nohup`命令也是类似的原理,不过通过我的测试,还是`(cmd &)`这种形式更加稳定。
`nohub`命令也是类似的原理,不过通过我的测试,还是`(cmd &)`这种形式更加稳定。
### 三、单引号和双引号的区别
......@@ -112,7 +117,7 @@ $ nohup some_cmd &
shell 的行为可以测试,使用`set -x`命令,会开启 shell 的命令回显,你可以通过回显观察 shell 到底在执行什么命令:
![](../pictures/linuxshell/1.png)
![](https://labuladong.github.io/algo/images/linuxshell/1.png)
可见 `echo $(cmd)``echo "$(cmd)"`,结果差不多,但是仍然有区别。注意观察,双引号转义完成的结果会自动增加单引号,而前者不会。
......@@ -147,12 +152,9 @@ $ sudo /home/fdl/bin/connect.sh
**_____________**
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**
![](https://labuladong.github.io/algo/images/souyisou2.png)
<p align='center'>
<img src="../pictures/qrcode.jpg" width=200 >
</p>
======其他语言代码======
......@@ -200,5 +200,4 @@ HTTPS 协议中的 SSL/TLS 安全层会组合使用以上几种加密方式,**
<img src="../pictures/qrcode.jpg" width=200 >
</p>
======其他语言代码======
[test ad](https://labuladong.gitee.io/algo/)
\ No newline at end of file
======其他语言代码======
\ No newline at end of file
# 二叉堆详解实现优先级队列
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号-@labuladong-000000.svg?style=flat-square&logo=WeChat"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>
![](../pictures/souyisou.png)
![](https://labuladong.github.io/algo/images/souyisou1.png)
**通知:[数据结构精品课 V1.8](https://aep.h5.xeknow.com/s/1XJHEO) 持续更新中;[第十期刷题打卡挑战](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 最后一天报名。**
**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
**-----------**
二叉堆(Binary Heap)没什么神秘,性质比二叉搜索树 BST 还简单。其主要操作就两个,`sink`(下沉)和 `swim`(上浮),用以维护二叉堆的性质。其主要应用有两个,首先是一种排序方法「堆排序」,第二是一种很有用的数据结构「优先级队列」。
本文就以实现优先级队列(Priority Queue)为例,通过图片和人类的语言来描述一下二叉堆怎么运作的。
本文参考《算法 4》的代码,以实现优先级队列(Priority Queue)为例,来讲讲一下二叉堆怎么运作的。
### 一、二叉堆概览
首先,二叉堆和二叉树有啥关系呢,为什么人们总把二叉堆画成一棵二叉树?
首先,二叉堆和二叉树有啥关系呢,为什么人们总把二叉堆画成一棵二叉树?
因为,二叉堆其实就是一种特殊的二叉树(完全二叉树),只不过存储在数组里。一般的链表二叉树,我们操作节点的指针,而在数组里,我们把数组索引作为指针:
因为,二叉堆在逻辑上其实是一种特殊的二叉树(完全二叉树),只不过存储在数组里。一般的链表二叉树,我们操作节点的指针,而在数组里,我们把数组索引作为指针:
```java
// 父节点的索引
......@@ -39,29 +44,29 @@ int right(int root) {
}
```
画个图你立即就能理解了,注意数组的第一个索引 0 空着不用,
画个图你立即就能理解了,比如 `arr` 是一个字符数组,注意数组的第一个索引 0 空着不用:
![1](../pictures/heap/1.png)
![](https://labuladong.github.io/algo/images/heap/1.png)
PS:因为数组索引是数组,为了方便区分,将字符作为数组元素
你看到了,因为这棵二叉树是「完全二叉树」,所以把 `arr[1]` 作为整棵树的根的话,每个节点的父节点和左右孩子的索引都可以通过简单的运算得到,这就是二叉堆设计的一个巧妙之处
你看到了,把 arr[1] 作为整棵树的根的话,每个节点的父节点和左右孩子的索引都可以通过简单的运算得到,这就是二叉堆设计的一个巧妙之处。为了方便讲解,下面都会画的图都是二叉树结构,相信你能把树和数组对应起来。
为了方便讲解,下面都会画的图都是二叉树结构,相信你能把树和数组对应起来。
二叉堆还分为最大堆和最小堆。**最大堆的性质是:每个节点都大于等于它的两个子节点。**类似的,最小堆的性质是:每个节点都小于等于它的子节点。
二叉堆还分为最大堆和最小堆。**最大堆的性质是:每个节点都大于等于它的两个子节点**类似的,最小堆的性质是:每个节点都小于等于它的子节点。
两种堆核心思路都是一样的,本文以最大堆为例讲解。
对于一个最大堆,根据其性质,显然堆顶,也就是 arr[1] 一定是所有元素中最大的元素。
对于一个最大堆,根据其性质,显然堆顶,也就是 `arr[1]` 一定是所有元素中最大的元素。
### 二、优先级队列概览
优先级队列这种数据结构有一个很有用的功能,你插入或者删除元素的时候,元素会自动排序,这底层的原理就是二叉堆的操作。
数据结构的功能无非增删查,优先级队列有两个主要 API,分别是 `insert` 插入一个元素和 `delMax` 删除最大元素(如果底层用最小堆,那么就是 `delMin`)。
数据结构的功能无非增删查,优先级队列有两个主要 API,分别是 `insert` 插入一个元素和 `delMax` 删除最大元素(如果底层用最小堆,那么就是 `delMin`)。
下面我们实现一个简化的优先级队列,先看下代码框架:
PS:为了清晰起见,这里用到 Java 的泛型,`Key` 可以是任何一种可比较大小的数据类型,你可以认为它是 int、char 等
> PS:这里用到 Java 的泛型,`Key` 可以是任何一种可比较大小的数据类型,比如 Integer 等类型
```java
public class MaxPQ
......@@ -69,7 +74,7 @@ public class MaxPQ
// 存储元素的数组
private Key[] pq;
// 当前 Priority Queue 中的元素个数
private int N = 0;
private int size = 0;
public MaxPQ(int cap) {
// 索引 0 不用,所以多分配一个空间
......@@ -87,14 +92,14 @@ public class MaxPQ
/* 删除并返回当前队列中最大元素 */
public Key delMax() {...}
/* 上浮第 k 个元素,以维护最大堆性质 */
private void swim(int k) {...}
/* 上浮第 x 个元素,以维护最大堆性质 */
private void swim(int x) {...}
/* 下沉第 k 个元素,以维护最大堆性质 */
private void sink(int k) {...}
/* 下沉第 x 个元素,以维护最大堆性质 */
private void sink(int x) {...}
/* 交换数组的两个元素 */
private void exch(int i, int j) {
private void swap(int i, int j) {
Key temp = pq[i];
pq[i] = pq[j];
pq[j] = temp;
......@@ -113,15 +118,15 @@ public class MaxPQ
### 三、实现 swim 和 sink
为什么要有上浮 swim 和下沉 sink 的操作呢?为了维护堆结构。
为什么要有上浮 `swim` 和下沉 `sink` 的操作呢?为了维护堆结构。
我们要讲的是最大堆,每个节点都比它的两个子节点大,但是在插入元素和删除元素时,难免破坏堆的性质,这就需要通过这两个操作来恢复堆的性质了。
对于最大堆,会破坏堆性质的有两种情况:
对于最大堆,会破坏堆性质的有两种情况:
1. 如果某个节点 A 比它的子节点(中的一个)小,那么 A 就不配做父节点,应该下去,下面那个更大的节点上来做父节点,这就是对 A 进行**下沉**
1如果某个节点 A 比它的子节点(中的一个)小,那么 A 就不配做父节点,应该下去,下面那个更大的节点上来做父节点,这就是对 A 进行**下沉**
2. 如果某个节点 A 比它的父节点大,那么 A 不应该做子节点,应该把父节点换下来,自己去做父节点,这就是对 A 的**上浮**
2如果某个节点 A 比它的父节点大,那么 A 不应该做子节点,应该把父节点换下来,自己去做父节点,这就是对 A 的**上浮**
当然,错位的节点 A 可能要上浮(或下沉)很多次,才能到达正确的位置,恢复堆的性质。所以代码中肯定有一个 `while` 循环。
......@@ -132,89 +137,87 @@ public class MaxPQ
**上浮的代码实现:**
```java
private void swim(int k) {
private void swim(int x) {
// 如果浮到堆顶,就不能再上浮了
while (k > 1 && less(parent(k), k)) {
// 如果第 k 个元素比上层大
// 将 k 换上去
exch(parent(k), k);
k = parent(k);
while (x > 1 && less(parent(x), x)) {
// 如果第 x 个元素比上层大
// 将 x 换上去
swap(parent(x), x);
x = parent(x);
}
}
```
画个 GIF 看一眼就明白了:
![2](../pictures/heap/swim.gif)
![](https://labuladong.github.io/algo/images/heap/swim.gif)
**下沉的代码实现:**
下沉比上浮略微复杂一点,因为上浮某个节点 A,只需要 A 和其父节点比较大小即可;但是下沉某个节点 A,需要 A 和其**两个子节点**比较大小,如果 A 不是最大的就需要调整位置,要把较大的那个子节点和 A 交换。
```java
private void sink(int k) {
private void sink(int x) {
// 如果沉到堆底,就沉不下去了
while (left(k) <= N) {
while (left(x) <= size) {
// 先假设左边节点较大
int older = left(k);
int max = left(x);
// 如果右边节点存在,比一下大小
if (right(k) <= N && less(older, right(k)))
older = right(k);
// 结点 k 比俩孩子都大,就不必下沉了
if (less(older, k)) break;
// 否则,不符合最大堆的结构,下沉 k 结点
exch(k, older);
k = older;
if (right(x) <= size && less(max, right(x)))
max = right(x);
// 结点 x 比俩孩子都大,就不必下沉了
if (less(max, x)) break;
// 否则,不符合最大堆的结构,下沉 x 结点
swap(x, max);
x = max;
}
}
```
画个 GIF 看下就明白了:
![3](../pictures/heap/sink.gif)
![](https://labuladong.github.io/algo/images/heap/sink.gif)
至此,二叉堆的主要操作就讲完了,一点都不难吧,代码加起来也就十行。明白了 `sink``swim` 的行为,下面就可以实现优先级队列了。
### 四、实现 delMax 和 insert
这两个方法就是建立在 `swim``sink` 上的。
**`insert` 方法先把要插入的元素添加到堆底的最后,然后让其上浮到正确位置。**
**`insert` 方法先把要插入的元素添加到堆底的最后,然后让其上浮到正确位置**
![4](../pictures/heap/insert.gif)
![](https://labuladong.github.io/algo/images/heap/insert.gif)
```java
public void insert(Key e) {
N++;
size++;
// 先把新元素加到最后
pq[N] = e;
pq[size] = e;
// 然后让它上浮到正确的位置
swim(N);
swim(size);
}
```
**`delMax` 方法先把堆顶元素 A 和堆底最后的元素 B 对调,然后删除 A,最后让 B 下沉到正确位置。**
**`delMax` 方法先把堆顶元素 `A` 和堆底最后的元素 `B` 对调,然后删除 `A`,最后让 `B` 下沉到正确位置**
```java
public Key delMax() {
// 最大堆的堆顶就是最大元素
Key max = pq[1];
// 把这个最大元素换到最后,删除之
exch(1, N);
pq[N] = null;
N--;
swap(1, size);
pq[size] = null;
size--;
// 让 pq[1] 下沉到正确位置
sink(1);
return max;
}
```
![5](../pictures/heap/delete.gif)
![](https://labuladong.github.io/algo/images/heap/delete.gif)
至此,一个优先级队列就实现了,插入和删除元素的时间复杂度为 `O(logK)``K` 为当前二叉堆(优先级队列)中的元素总数。因为我们时间复杂度主要花费在 `sink` 或者 `swim` 上,而不管上浮还是下沉,最多也就树(堆)的高度,也就是 log 级别。
### 五、最后总结
二叉堆就是一种完全二叉树,所以适合存储在数组中,而且二叉堆拥有一些特殊性质。
......@@ -225,17 +228,15 @@ public Key delMax() {
也许这就是数据结构的威力,简单的操作就能实现巧妙的功能,真心佩服发明二叉堆算法的人!
> 最后打个广告,我亲自制作了一门 [数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO),以视频课为主,手把手带你实现常用的数据结构及相关算法,旨在帮助算法基础较为薄弱的读者深入理解常用数据结构的底层原理,在算法学习中少走弯路。
**_____________**
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**
![](https://labuladong.github.io/algo/images/souyisou2.png)
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**
<p align='center'>
<img src="../pictures/qrcode.jpg" width=200 >
</p>
======其他语言代码======
### javascript
......
# 如何使用单调栈解题
# 特殊数据结构:单调栈
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号-@labuladong-000000.svg?style=flat-square&logo=WeChat"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>
![](../pictures/souyisou.png)
![](https://labuladong.github.io/algo/images/souyisou1.png)
相关推荐:
* [回溯算法解题套路框架](https://labuladong.gitee.io/algo/)
* [动态规划解题套路框架](https://labuladong.gitee.io/algo/)
**通知:[数据结构精品课 V1.8](https://aep.h5.xeknow.com/s/1XJHEO) 持续更新中;[第十期刷题打卡挑战](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 最后一天报名。**
读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
[496.下一个更大元素I](https://leetcode-cn.com/problems/next-greater-element-i)
[503.下一个更大元素II](https://leetcode-cn.com/problems/next-greater-element-ii)
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
[739.每日温度](https://leetcode-cn.com/problems/daily-temperatures/)
| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [496. Next Greater Element I](https://leetcode.com/problems/next-greater-element-i/) | [496. 下一个更大元素 I](https://leetcode.cn/problems/next-greater-element-i/) | 🟢
| [503. Next Greater Element II](https://leetcode.com/problems/next-greater-element-ii/) | [503. 下一个更大元素 II](https://leetcode.cn/problems/next-greater-element-ii/) | 🟠
| [739. Daily Temperatures](https://leetcode.com/problems/daily-temperatures/) | [739. 每日温度](https://leetcode.cn/problems/daily-temperatures/) | 🟠
**-----------**
栈(stack)是很简单的一种数据结构,先进后出的逻辑顺序,符合某些问题的特点,比如说函数调用栈。
栈(stack)是很简单的一种数据结构,先进后出的逻辑顺序,符合某些问题的特点,比如说函数调用栈。单调栈实际上就是栈,只是利用了一些巧妙的逻辑,使得每次新元素入栈后,栈内的元素都保持有序(单调递增或单调递减)。
单调栈实际上就是栈,只是利用了一些巧妙的逻辑,使得每次新元素入栈后,栈内的元素都保持有序(单调递增或单调递减)。
听起来有点像堆(heap)?不是的,单调栈用途不太广泛,只处理一种典型的问题,叫做 Next Greater Element。本文用讲解单调队列的算法模版解决这类问题,并且探讨处理「循环数组」的策略。
听起来有点像堆(heap)?不是的,单调栈用途不太广泛,只处理一类典型的问题,比如「下一个更大元素」,「上一个更小元素」等。本文用讲解单调队列的算法模版解决「下一个更大元素」相关问题,并且探讨处理「循环数组」的策略。至于其他的变体和经典例题,我会在 [数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 中讲解。
### 单调栈模板
首先,看一下 Next Greater Number 的原始问题,这是力扣第 496 题「下一个更大元素 I」:
给你一个数组,返回一个等长的数组,对应索引存储着下一个更大元素,如果没有更大的元素,就存 -1。
现在给你出这么一道题:输入一个数组 `nums`,请你返回一个等长的结果数组,结果数组中对应索引存储着下一个更大元素,如果没有更大的元素,就存 -1。函数签名如下:
函数签名如下:
```cpp
vector<int> nextGreaterElement(vector<int>& nums);
```java
int[] nextGreaterElement(int[] nums);
```
比如说,输入一个数组 `nums = [2,1,2,4,3]`,你返回数组 `[4,2,4,-1,-1]`
解释:第一个 2 后面比 2 大的数是 4; 1 后面比 1 大的数是 2;第二个 2 后面比 2 大的数是 4; 4 后面没有比 4 大的数,填 -1;3 后面没有比 3 大的数,填 -1。
比如说,输入一个数组 `nums = [2,1,2,4,3]`,你返回数组 `[4,2,4,-1,-1]`。因为第一个 2 后面比 2 大的数是 4; 1 后面比 1 大的数是 2;第二个 2 后面比 2 大的数是 4; 4 后面没有比 4 大的数,填 -1;3 后面没有比 3 大的数,填 -1。
这道题的暴力解法很好想到,就是对每个元素后面都进行扫描,找到第一个更大的元素就行了。但是暴力解法的时间复杂度是 `O(n^2)`
这个问题可以这样抽象思考:把数组的元素想象成并列站立的人,元素大小想象成人的身高。这些人面对你站成一列,如何求元素「2」的 Next Greater Number 呢?很简单,如果能够看到元素「2」,那么他后面可见的第一个人就是「2」的 Next Greater Number,因为比「2」小的元素身高不够,都被「2」挡住了,第一个露出来的就是答案。
这个问题可以这样抽象思考:把数组的元素想象成并列站立的人,元素大小想象成人的身高。这些人面对你站成一列,如何求元素「2」的下一个更大元素呢?很简单,如果能够看到元素「2」,那么他后面可见的第一个人就是「2」的下一个更大元素,因为比「2」小的元素身高不够,都被「2」挡住了,第一个露出来的就是答案。
![](../pictures/%E5%8D%95%E8%B0%83%E6%A0%88/1.jpeg)
![](https://labuladong.github.io/algo/images/单调栈/1.jpeg)
这个情景很好理解吧?带着这个抽象的情景,先来看下代码。
```cpp
vector<int> nextGreaterElement(vector<int>& nums) {
vector<int> res(nums.size()); // 存放答案的数组
stack<int> s;
```java
int[] nextGreaterElement(int[] nums) {
int n = nums.length;
// 存放答案的数组
int[] res = new int[n];
Stack<Integer> s = new Stack<>();
// 倒着往栈里放
for (int i = nums.size() - 1; i >= 0; i--) {
for (int i = n - 1; i >= 0; i--) {
// 判定个子高矮
while (!s.empty() && s.top() <= nums[i]) {
while (!s.isEmpty() && s.peek() <= nums[i]) {
// 矮个起开,反正也被挡着了。。。
s.pop();
}
// nums[i] 身后的 next great number
res[i] = s.empty() ? -1 : s.top();
//
// nums[i] 身后的更大元素
res[i] = s.isEmpty() ? -1 : s.peek();
s.push(nums[i]);
}
return res;
}
```
这就是单调队列解决问题的模板。for 循环要从后往前扫描元素,因为我们借助的是栈的结构,倒着入栈,其实是正着出栈。while 循环是把两个「个子高」元素之间的元素排除,因为他们的存在没有意义,前面挡着个「更高」的元素,所以他们不可能被作为后续进来的元素的 Next Great Number 了。
这就是单调队列解决问题的模板。for 循环要从后往前扫描元素,因为我们借助的是栈的结构,倒着入栈,其实是正着出栈。while 循环是把两个「个子高」元素之间的元素排除,因为他们的存在没有意义,前面挡着个「更高」的元素,所以他们不可能被作为后续进来的元素的下一个更大元素了。
这个算法的时间复杂度不是那么直观,如果你看到 for 循环嵌套 while 循环,可能认为这个算法的复杂度也是 `O(n^2)`,但是实际上这个算法的复杂度只有 `O(n)`
......@@ -82,36 +80,68 @@ vector<int> nextGreaterElement(vector<int>& nums) {
### 问题变形
单调栈的使用技巧差不多了,来一个简单的变形,力扣第 739 题「每日温度」:
单调栈的使用技巧差不多了,首先来一个简单的变形,力扣第 496 题「下一个更大元素 I」:
![](https://labuladong.github.io/algo/images/单调栈/title.jpg)
给你一个数组 `T`,这个数组存放的是近几天的天气气温,你返回一个等长的数组,计算:**对于每一天,你还要至少等多少天才能等到一个更暖和的气温;如果等不到那一天,填 0**
这道题给你输入两个数组 `nums1``nums2`,让你求 `nums1` 中的元素在 `nums2` 中的下一个更大元素,函数签名如下:
函数签名如下:
```java
int[] nextGreaterElement(int[] nums1, int[] nums2)
```
其实和把我们刚才的代码改一改就可以解决这道题了,因为题目说 `nums1``nums2` 的子集,那么我们先把 `nums2` 中每个元素的下一个更大元素算出来存到一个映射里,然后再让 `nums1` 中的元素去查表即可:
```java
int[] nextGreaterElement(int[] nums1, int[] nums2) {
// 记录 nums2 中每个元素的下一个更大元素
int[] greater = nextGreaterElement(nums2);
// 转化成映射:元素 x -> x 的下一个最大元素
HashMap<Integer, Integer> greaterMap = new HashMap<>();
for (int i = 0; i < nums2.length; i++) {
greaterMap.put(nums2[i], greater[i]);
}
// nums1 是 nums2 的子集,所以根据 greaterMap 可以得到结果
int[] res = new int[nums1.length];
for (int i = 0; i < nums1.length; i++) {
res[i] = greaterMap.get(nums1[i]);
}
return res;
}
```cpp
vector<int> dailyTemperatures(vector<int>& T);
int[] nextGreaterElement(int[] nums) {
// 见上文
}
```
比如说给你输入 `T = [73,74,75,71,69,76]`,你返回 `[1,1,3,2,1,0]`
解释:第一天 73 华氏度,第二天 74 华氏度,比 73 大,所以对于第一天,只要等一天就能等到一个更暖和的气温,后面的同理。
再看看力扣第 739 题「每日温度」:
给你一个数组 `temperatures`,这个数组存放的是近几天的天气气温,你返回一个等长的数组,计算:对于每一天,你还要至少等多少天才能等到一个更暖和的气温;如果等不到那一天,填 0。函数签名如下:
```java
int[] dailyTemperatures(int[] temperatures);
```
比如说给你输入 `temperatures = [73,74,75,71,69,76]`,你返回 `[1,1,3,2,1,0]`。因为第一天 73 华氏度,第二天 74 华氏度,比 73 大,所以对于第一天,只要等一天就能等到一个更暖和的气温,后面的同理。
这个问题本质上也是找 Next Greater Number,只不过现在不是问你 Next Greater Number 是多少,而是问你当前距离 Next Greater Number 的距离而已。
这个问题本质上也是找下一个更大元素,只不过现在不是问你下一个更大元素的值是多少,而是问你当前元素距离下一个更大元素的索引距离而已。
相同的思路,直接调用单调栈的算法模板,稍作改动就可以,直接上代码吧:
```cpp
vector<int> dailyTemperatures(vector<int>& T) {
vector<int> res(T.size());
```java
int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] res = new int[n];
// 这里放元素索引,而不是元素
stack<int> s;
Stack<Integer> s = new Stack<>();
/* 单调栈模板 */
for (int i = T.size() - 1; i >= 0; i--) {
while (!s.empty() && T[s.top()] <= T[i]) {
for (int i = n - 1; i >= 0; i--) {
while (!s.isEmpty() && temperatures[s.peek()] <= temperatures[i]) {
s.pop();
}
// 得到索引间距
res[i] = s.empty() ? 0 : (s.top() - i);
res[i] = s.isEmpty() ? 0 : (s.peek() - i);
// 将索引入栈,而不是元素
s.push(i);
}
......@@ -123,44 +153,44 @@ vector<int> dailyTemperatures(vector<int>& T) {
### 如何处理环形数组
同样是 Next Greater Number,现在假设给你的数组是个环形的,如何处理?力扣第 503 题「下一个更大元素 II」就是这个问题:
同样是求下一个更大元素,现在假设给你的数组是个环形的,如何处理?力扣第 503 题「下一个更大元素 II」就是这个问题:输入一个「环形数组」,请你计算其中每个元素的下一个更大元素。
比如输入一个数组 `[2,1,2,4,3]`,你返回数组 `[4,2,4,-1,4]`拥有了环形属性,**最后一个元素 3 绕了一圈后找到了比自己大的元素 4**
比如输入 `[2,1,2,4,3]`,你应该返回 `[4,2,4,-1,4]`,因为拥有了环形属性,**最后一个元素 3 绕了一圈后找到了比自己大的元素 4**
一般是通过 % 运算符求模(余数),来获得环形特效:
我们一般是通过 % 运算符求模(余数),来模拟环形特效:
```java
int[] arr = {1,2,3,4,5};
int n = arr.length, index = 0;
while (true) {
// 在环形数组中转圈
print(arr[index % n]);
index++;
}
```
这个问题肯定还是要用单调栈的解题模板,但难点在于,比如输入是 `[2,1,2,4,3]`,对于最后一个元素 3,如何找到元素 4 作为 Next Greater Number
这个问题肯定还是要用单调栈的解题模板,但难点在于,比如输入是 `[2,1,2,4,3]`,对于最后一个元素 3,如何找到元素 4 作为下一个更大元素
**对于这种需求,常用套路就是将数组长度翻倍**
![](../pictures/%E5%8D%95%E8%B0%83%E6%A0%88/2.jpeg)
这样,元素 3 就可以找到元素 4 作为 Next Greater Number 了,而且其他的元素都可以被正确地计算。
![](https://labuladong.github.io/algo/images/单调栈/2.jpeg)
有了思路,最简单的实现方式当然可以把这个双倍长度的数组构造出来,然后套用算法模板。但是,**我们可以不用构造新数组,而是利用循环数组的技巧来模拟数组长度翻倍的效果**
这样,元素 3 就可以找到元素 4 作为下一个更大元素了,而且其他的元素都可以被正确地计算
直接看代码吧:
有了思路,最简单的实现方式当然可以把这个双倍长度的数组构造出来,然后套用算法模板。但是,**我们可以不用构造新数组,而是利用循环数组的技巧来模拟数组长度翻倍的效果**直接看代码吧:
```cpp
vector<int> nextGreaterElements(vector<int>& nums) {
int n = nums.size();
vector<int> res(n);
stack<int> s;
// 假装这个数组长度翻倍了
```java
int[] nextGreaterElements(int[] nums) {
int n = nums.length;
int[] res = new int[n];
Stack<Integer> s = new Stack<>();
// 数组长度加倍模拟环形数组
for (int i = 2 * n - 1; i >= 0; i--) {
// 索引要求模,其他的和模板一样
while (!s.empty() && s.top() <= nums[i % n])
// 索引 i 要求模,其他的和模板一样
while (!s.isEmpty() && s.peek() <= nums[i % n]) {
s.pop();
res[i % n] = s.empty() ? -1 : s.top();
}
res[i % n] = s.isEmpty() ? -1 : s.peek();
s.push(nums[i % n]);
}
return res;
......@@ -169,17 +199,16 @@ vector<int> nextGreaterElements(vector<int>& nums) {
这样,就可以巧妙解决环形数组的问题,时间复杂度 `O(N)`
如果本文对你有帮助,请三连,这次一定。
最后提出一些问题吧,本文提供的单调栈模板是 `nextGreaterElement` 函数,可以计算每个元素的下一个更大元素,但如果题目让你计算上一个更大元素,或者计算上一个更大或相等的元素,应该如何修改对应的模板呢?而且在实际应用中,题目不会直接让你计算下一个(上一个)更大(小)的元素,你如何把问题转化成单调栈相关的问题呢?
我会在 [单调栈的几种变体](https://appktavsiei5995.pc.xiaoe-tech.com/detail/i_628dc1ace4b09dda126cf793/1) 对比单调栈的几种其他形式,并在 [单调栈的运用](https://appktavsiei5995.pc.xiaoe-tech.com/detail/i_628dc2d7e4b0cedf38b67734/1) 中给出单调栈的经典例题。
**_____________**
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**
![](https://labuladong.github.io/algo/images/souyisou2.png)
<p align='center'>
<img src="../pictures/qrcode.jpg" width=200 >
</p>
======其他语言代码======
[496.下一个更大元素I](https://leetcode-cn.com/problems/next-greater-element-i)
......
# 特殊数据结构:单调队列
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号-@labuladong-000000.svg?style=flat-square&logo=WeChat"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>
![](../pictures/souyisou.png)
![](https://labuladong.github.io/algo/images/souyisou1.png)
**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
**通知:[数据结构精品课 V1.8](https://aep.h5.xeknow.com/s/1XJHEO) 持续更新中;[第十期刷题打卡挑战](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 最后一天报名。**
读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
[239.滑动窗口最大值](https://leetcode-cn.com/problems/sliding-window-maximum)
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [239. Sliding Window Maximum](https://leetcode.com/problems/sliding-window-maximum/) | [239. 滑动窗口最大值](https://leetcode.cn/problems/sliding-window-maximum/) | 🔴
| - | [剑指 Offer 59 - I. 滑动窗口的最大值](https://leetcode.cn/problems/hua-dong-chuang-kou-de-zui-da-zhi-lcof/) | 🔴
| - | [剑指 Offer 59 - II. 队列的最大值](https://leetcode.cn/problems/dui-lie-de-zui-da-zhi-lcof/) | 🟠
**-----------**
前文讲了一种特殊的数据结构「单调栈」monotonic stack,解决了一类问题「Next Greater Number」,本文写一个类似的数据结构「单调队列」。
前文[单调栈解决三道算法问题](https://labuladong.github.io/article/fname.html?fname=单调栈) 介绍了单调栈这种特殊数据结构,本文写一个类似的数据结构「单调队列」。
也许这种数据结构的名字你没听过,其实没啥难的,就是一个「队列」,只是使用了一点巧妙的方法,使得队列中的元素单调递增(或递减)。这个数据结构有什么用?可以解决滑动窗口的一系列问题
也许这种数据结构的名字你没听过,其实没啥难的,就是一个「队列」,只是使用了一点巧妙的方法,使得队列中的元素全都是单调递增(或递减)的
看一道 LeetCode 题目,难度 hard
为啥要发明「单调队列」这种结构呢,主要是为了解决下面这个场景
![](../pictures/单调队列/title.png)
**给你一个数组 `window`,已知其最值为 `A`,如果给 `window` 中添加一个数 `B`,那么比较一下 `A` 和 `B` 就可以立即算出新的最值;但如果要从 `window` 数组中减少一个数,就不能直接得到最值了,因为如果减少的这个数恰好是 `A`,就需要遍历 `window` 中的所有元素重新寻找新的最值**
### 一、搭建解题框架
这个场景很常见,但不用单调队列似乎也可以,比如优先级队列也是一种特殊的队列,专门用来动态寻找最值的,我创建一个大(小)顶堆,不就可以很快拿到最大(小)值了吗?
这道题不复杂,难点在于如何在 O(1) 时间算出每个「窗口」中的最大值,使得整个算法在线性时间完成。在之前我们探讨过类似的场景,得到一个结论:
如果单纯地维护最值的话,优先级队列很专业,队头元素就是最值。但优先级队列无法满足标准队列结构「先进先出」的**时间顺序**,因为优先级队列底层利用二叉堆对元素进行动态排序,元素的出队顺序是元素的大小顺序,和入队的先后顺序完全没有关系。
在一堆数字中,已知最值,如果给这堆数添加一个数,那么比较一下就可以很快算出最值;但如果减少一个数,就不一定能很快得到最值了,而要遍历所有数重新找最值
所以,现在需要一种新的队列结构,既能够维护队列元素「先进先出」的时间顺序,又能够正确维护队列中所有元素的最值,这就是「单调队列」结构
回到这道题的场景,每个窗口前进的时候,要添加一个数同时减少一个数,所以想在 O(1) 的时间得出新的最值,就需要「单调队列」这种特殊的数据结构来辅助了
「单调队列」这个数据结构主要用来辅助解决滑动窗口相关的问题,前文 [滑动窗口核心框架](https://labuladong.github.io/article/fname.html?fname=滑动窗口技巧进阶) 把滑动窗口算法作为双指针技巧的一部分进行了讲解,但有些稍微复杂的滑动窗口问题不能只靠两个指针来解决,需要上更先进的数据结构
一个普通的队列一定有这两个操作:
比方说,你注意看前文 [滑动窗口核心框架](https://labuladong.github.io/article/fname.html?fname=滑动窗口技巧进阶) 讲的几道题目,每当窗口扩大(`right++`)和窗口缩小(`left++`)时,你单凭移出和移入窗口的元素即可决定是否更新答案。
但就本文开头说的那个判断一个窗口中最值的例子,你就无法单凭移出窗口的那个元素更新窗口的最值,除非重新遍历所有元素,但这样的话时间复杂度就上来了,这是我们不希望看到的。
我们来看看力扣第 239 题「滑动窗口最大值」,就是一道标准的滑动窗口问题:
给你输入一个数组 `nums` 和一个正整数 `k`,有一个大小为 `k` 的窗口在 `nums` 上从左至右滑动,请你输出每次窗口中 `k` 个元素的最大值。
函数签名如下:
```java
int[] maxSlidingWindow(int[] nums, int k);
```
比如说力扣给出的一个示例:
![](https://labuladong.github.io/algo/images/单调队列/window.png)
接下来,我们就借助单调队列结构,用 `O(1)` 时间算出每个滑动窗口中的最大值,使得整个算法在线性时间完成。
### 一、搭建解题框架
在介绍「单调队列」这种数据结构的 API 之前,先来看看一个普通的队列的标准 API:
```java
class Queue {
// enqueue 操作,在队尾加入元素 n
void push(int n);
// 或 enqueue,在队尾加入元素 n
// dequeue 操作,删除队头元素
void pop();
// 或 dequeue,删除队头元素
}
```
一个「单调队列」的操作也差不多:
我们要实现的「单调队列」的 API 也差不多:
```java
class MonotonicQueue {
......@@ -60,153 +93,190 @@ class MonotonicQueue {
当然,这几个 API 的实现方法肯定跟一般的 Queue 不一样,不过我们暂且不管,而且认为这几个操作的时间复杂度都是 O(1),先把这道「滑动窗口」问题的解答框架搭出来:
```cpp
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MonotonicQueue window;
vector<int> res;
for (int i = 0; i < nums.size(); i++) {
if (i < k - 1) { //先把窗口的前 k - 1 填满
```java
int[] maxSlidingWindow(int[] nums, int k) {
MonotonicQueue window = new MonotonicQueue();
List<Integer> res = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
if (i < k - 1) {
//先把窗口的前 k - 1 填满
window.push(nums[i]);
} else { // 窗口开始向前滑动
} else {
// 窗口开始向前滑动
// 移入新元素
window.push(nums[i]);
res.push_back(window.max());
// 将当前窗口中的最大元素记入结果
res.add(window.max());
// 移出最后的元素
window.pop(nums[i - k + 1]);
// nums[i - k + 1] 就是窗口最后的元素
}
}
return res;
// 将 List 类型转化成 int[] 数组作为返回值
int[] arr = new int[res.size()];
for (int i = 0; i < res.size(); i++) {
arr[i] = res.get(i);
}
return arr;
}
```
![图示](../pictures/单调队列/1.png)
![](https://labuladong.github.io/algo/images/单调队列/1.png)
这个思路很简单,能理解吧?下面我们开始重头戏,单调队列的实现。
### 二、实现单调队列数据结构
首先我们要认识另一种数据结构:deque,即双端队列。很简单:
```java
class deque {
// 在队头插入元素 n
void push_front(int n);
// 在队尾插入元素 n
void push_back(int n);
// 在队头删除元素
void pop_front();
// 在队尾删除元素
void pop_back();
// 返回队头元素
int front();
// 返回队尾元素
int back();
}
```
而且,这些操作的复杂度都是 O(1)。这其实不是啥稀奇的数据结构,用链表作为底层结构的话,很容易实现这些功能。
观察滑动窗口的过程就能发现,实现「单调队列」必须使用一种数据结构支持在头部和尾部进行插入和删除,很明显双链表是满足这个条件的。
「单调队列」的核心思路和「单调栈」类似。单调队列的 push 方法依然在队尾添加元素,但是要把前面比新元素小的元素都删掉:
「单调队列」的核心思路和「单调栈」类似`push` 方法依然在队尾添加元素,但是要把前面比自己小的元素都删掉:
```cpp
```java
class MonotonicQueue {
private:
deque<int> data;
public:
void push(int n) {
while (!data.empty() && data.back() < n)
data.pop_back();
data.push_back(n);
// 双链表,支持头部和尾部增删元素
// 维护其中的元素自尾部到头部单调递增
private LinkedList<Integer> maxq = new LinkedList<>();
// 在尾部添加一个元素 n,维护 maxq 的单调性质
public void push(int n) {
// 将前面小于自己的元素都删除
while (!maxq.isEmpty() && maxq.getLast() < n) {
maxq.pollLast();
}
};
maxq.addLast(n);
}
```
你可以想象,加入数字的大小代表人的体重,把前面体重不足的都压扁了,直到遇到更大的量级才停住。
![](../pictures/单调队列/2.png)
![](https://labuladong.github.io/algo/images/单调队列/3.png)
如果每个元素被加入时都这样操作,最终单调队列中的元素大小就会保持一个单调递减的顺序,因此我们的 max() API 可以可以这样写:
如果每个元素被加入时都这样操作,最终单调队列中的元素大小就会保持一个**单调递减**的顺序,因此我们的 `max` 方法可以可以这样写:
```cpp
int max() {
return data.front();
```java
public int max() {
// 队头的元素肯定是最大的
return maxq.getFirst();
}
```
pop() API 在队头删除元素 n,也很好写:
`pop` 方法在队头删除元素 `n`,也很好写:
```cpp
void pop(int n) {
if (!data.empty() && data.front() == n)
data.pop_front();
```java
public void pop(int n) {
if (n == maxq.getFirst()) {
maxq.pollFirst();
}
}
```
之所以要判断 `data.front() == n`,是因为我们想删除的队头元素 n 可能已经被「压扁」了,这时候就不用删除了:
之所以要判断 `data.getFirst() == n`,是因为我们想删除的队头元素 `n` 可能已经被「压扁」了,可能已经不存在了,所以这时候就不用删除了:
![](../pictures/单调队列/3.png)
![](https://labuladong.github.io/algo/images/单调队列/2.png)
至此,单调队列设计完毕,看下完整的解题代码:
```cpp
```java
/* 单调队列的实现 */
class MonotonicQueue {
private:
deque<int> data;
public:
void push(int n) {
while (!data.empty() && data.back() < n)
data.pop_back();
data.push_back(n);
LinkedList<Integer> maxq = new LinkedList<>();
public void push(int n) {
// 将小于 n 的元素全部删除
while (!maxq.isEmpty() && maxq.getLast() < n) {
maxq.pollLast();
}
// 然后将 n 加入尾部
maxq.addLast(n);
}
int max() { return data.front(); }
public int max() {
return maxq.getFirst();
}
void pop(int n) {
if (!data.empty() && data.front() == n)
data.pop_front();
public void pop(int n) {
if (n == maxq.getFirst()) {
maxq.pollFirst();
}
}
};
}
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MonotonicQueue window;
vector<int> res;
for (int i = 0; i < nums.size(); i++) {
if (i < k - 1) { //先填满窗口的前 k - 1
/* 解题函数的实现 */
int[] maxSlidingWindow(int[] nums, int k) {
MonotonicQueue window = new MonotonicQueue();
List<Integer> res = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
if (i < k - 1) {
//先填满窗口的前 k - 1
window.push(nums[i]);
} else { // 窗口向前滑动
} else {
// 窗口向前滑动,加入新数字
window.push(nums[i]);
res.push_back(window.max());
// 记录当前窗口的最大值
res.add(window.max());
// 移出旧数字
window.pop(nums[i - k + 1]);
}
}
return res;
// 需要转成 int[] 数组再返回
int[] arr = new int[res.size()];
for (int i = 0; i < res.size(); i++) {
arr[i] = res.get(i);
}
return arr;
}
```
**三、算法复杂度分析**
有一点细节问题不要忽略,在实现 `MonotonicQueue` 时,我们使用了 Java 的 `LinkedList`,因为链表结构支持在头部和尾部快速增删元素;而在解法代码中的 `res` 则使用的 `ArrayList` 结构,因为后续会按照索引取元素,所以数组结构更合适。
关于单调队列 API 的时间复杂度,读者可能有疑惑:`push` 操作中含有 while 循环,时间复杂度应该不是 `O(1)` 呀,那么本算法的时间复杂度应该不是线性时间吧?
这里就用到了 [算法时空复杂度分析使用手册](https://labuladong.github.io/article/fname.html?fname=时间复杂度) 中讲到的摊还分析:
单独看 `push` 操作的复杂度确实不是 `O(1)`,但是算法整体的复杂度依然是 `O(N)` 线性时间。要这样想,`nums` 中的每个元素最多被 `push``pop` 一次,没有任何多余操作,所以整体的复杂度还是 `O(N)`。空间复杂度就很简单了,就是窗口的大小 `O(k)`
### 拓展延伸
最后,我提出几个问题请大家思考:
1、本文给出的 `MonotonicQueue` 类只实现了 `max` 方法,你是否能够再额外添加一个 `min` 方法,在 `O(1)` 的时间返回队列中所有元素的最小值?
2、本文给出的 `MonotonicQueue` 类的 `pop` 方法还需要接收一个参数,这显然有悖于标准队列的做法,请你修复这个缺陷。
读者可能疑惑,push 操作中含有 while 循环,时间复杂度不是 O(1) 呀,那么本算法的时间复杂度应该不是线性时间吧?
3、请你实现 `MonotonicQueue` 类的 `size` 方法,返回单调队列中元素的个数(注意,由于每次 `push` 方法都可能从底层的 `q` 列表中删除元素,所以 `q` 中的元素个数并不是单调队列的元素个数)。
单独看 push 操作的复杂度确实不是 O(1),但是算法整体的复杂度依然是 O(N) 线性时间。要这样想,nums 中的每个元素最多被 push_back 和 pop_back 一次,没有任何多余操作,所以整体的复杂度还是 O(N)。
也就是说,你是否能够实现单调队列的通用实现:
空间复杂度就很简单了,就是窗口的大小 O(k)。
```java
/* 单调队列的通用实现,可以高效维护最大值和最小值 */
class MonotonicQueue<E extends Comparable<E>> {
// 标准队列 API,向队尾加入元素
public void push(E elem);
**四、最后总结**
// 标准队列 API,从队头弹出元素,符合先进先出的顺序
public E pop();
有的读者可能觉得「单调队列」和「优先级队列」比较像,实际上差别很大的。
// 标准队列 API,返回队列中的元素个数
public int size();
单调队列在添加元素的时候靠删除元素保持队列的单调性,相当于抽取出某个函数中单调递增(或递减)的部分;而优先级队列(二叉堆)相当于自动排序,差别大了去了。
// 单调队列特有 API,O(1) 时间计算队列中元素的最大值
public E max();
// 单调队列特有 API,O(1) 时间计算队列中元素的最小值
public E min();
}
```
赶紧去拿下 LeetCode 第 239 道题吧~
我将在 [单调队列通用实现及应用](https://appktavsiei5995.pc.xiaoe-tech.com/detail/i_62a692efe4b01a48520b9b9b/1) 中给出单调队列的通用实现和经典习题。
**_____________**
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**
![](https://labuladong.github.io/algo/images/souyisou2.png)
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**
<p align='center'>
<img src="../pictures/qrcode.jpg" width=200 >
</p>
======其他语言代码======
[239.滑动窗口最大值](https://leetcode-cn.com/problems/sliding-window-maximum)
......
......@@ -2,30 +2,34 @@
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号-@labuladong-000000.svg?style=flat-square&logo=WeChat"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>
![](../pictures/souyisou.png)
![](https://labuladong.github.io/algo/images/souyisou1.png)
**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
**通知:[数据结构精品课 V1.8](https://aep.h5.xeknow.com/s/1XJHEO) 持续更新中;[第十期刷题打卡挑战](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 最后一天报名。**
读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
[224.基本计算器](https://leetcode-cn.com/problems/basic-calculator)
[227.基本计算器II](https://leetcode-cn.com/problems/basic-calculator-ii)
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
[772.基本计算器III](https://leetcode-cn.com/problems/basic-calculator-iii)
| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [224. Basic Calculator](https://leetcode.com/problems/basic-calculator/) | [224. 基本计算器](https://leetcode.cn/problems/basic-calculator/) | 🔴
| [227. Basic Calculator II](https://leetcode.com/problems/basic-calculator-ii/) | [227. 基本计算器 II](https://leetcode.cn/problems/basic-calculator-ii/) | 🟠
| [772. Basic Calculator III](https://leetcode.com/problems/basic-calculator-iii/)🔒 | [772. 基本计算器 III](https://leetcode.cn/problems/basic-calculator-iii/)🔒 | 🔴
**-----------**
我们最终要实现的计算器功能如下:
1、输入一个字符串,可以包含`+ - * /`、数字、括号以及空格,你的算法返回运算结果。
1、输入一个字符串,可以包含 `+ - * /`、数字、括号以及空格,你的算法返回运算结果。
2、要符合运算法则,括号的优先级最高,先乘除后加减。
......@@ -35,7 +39,12 @@
比如输入如下字符串,算法会返回 9:
`3 * (2-6 /(3 -7))`
```java
3 * (2 - 6 / (3 - 7))
= 3 * (2 - 6 / (-4))
= 3 * (2 - (-1))
= 9
```
可以看到,这就已经非常接近我们实际生活中使用的计算器了,虽然我们以前肯定都用过计算器,但是如果简单思考一下其算法实现,就会大惊失色:
......@@ -47,7 +56,7 @@
我记得很多大学数据结构的教材上,在讲栈这种数据结构的时候,应该都会用计算器举例,但是有一说一,讲的真的垃圾,不知道多少未来的计算机科学家就被这种简单的数据结构劝退了。
那么本文就来聊聊怎么实现上述一个功能完备的计算器功能,**关键在于层层拆解问题,化整为零,逐个击破**,相信这种思维方式能帮大家解决各种复杂问题。
那么本文就来聊聊怎么实现上述一个功能完备的计算器功能,**关键在于层层拆解问题,化整为零,逐个击破**几条简单的算法规则就可以处理极其复杂的运算,相信这种思维方式能帮大家解决各种复杂问题。
下面就来拆解,从最简单的一个问题开始。
......@@ -66,17 +75,17 @@ for (int i = 0; i < s.size(); i++) {
// n 现在就等于 458
```
这个还是很简单的吧,老套路了。但是即便这么简单,依然有坑:**`(c - '0')`的这个括号不能省略,否则可能造成整型溢出**
这个还是很简单的吧,老套路了。但是即便这么简单,依然有坑:**`(c - '0')` 的这个括号不能省略,否则可能造成整型溢出**
因为变量`c`是一个 ASCII 码,如果不加括号就会先加后减,想象一下`s`如果接近 INT_MAX,就会溢出。所以用括号保证先减后加才行。
因为变量 `c` 是一个 ASCII 码,如果不加括号就会先加后减,想象一下 `s` 如果接近 INT_MAX,就会溢出。所以用括号保证先减后加才行。
### 二、处理加减法
现在进一步,**如果输入的这个算式只包含加减法,而且不存在空格**,你怎么计算结果?我们拿字符串算式`1-12+3`为例,来说一个很简单的思路:
现在进一步,**如果输入的这个算式只包含加减法,而且不存在空格**,你怎么计算结果?我们拿字符串算式 `1-12+3` 为例,来说一个很简单的思路:
1、先给第一个数字加一个默认符号`+`,变成`+1-12+3`
1、先给第一个数字加一个默认符号 `+`,变成 `+1-12+3`
2、把一个运算符和数字组合成一对儿,也就是三对儿`+1``-12``+3`,把它们转化成数字,然后放到一个栈中。
2、把一个运算符和数字组合成一对儿,也就是三对儿 `+1``-12``+3`,把它们转化成数字,然后放到一个栈中。
3、将栈中所有的数字求和,就是原算式的结果。
......@@ -118,23 +127,23 @@ int calculate(string s) {
}
```
我估计就是中间带`switch`语句的部分有点不好理解吧,`i`就是从左到右扫描,`sign``num`跟在它身后。当`s[i]`遇到一个运算符时,情况是这样的:
我估计就是中间带 `switch` 语句的部分有点不好理解吧,`i` 就是从左到右扫描,`sign``num` 跟在它身后。当 `s[i]` 遇到一个运算符时,情况是这样的:
![](../pictures/calculator/1.jpg)
![](https://labuladong.github.io/algo/images/calculator/1.jpg)
所以说,此时要根据`sign`的 case 不同选择`nums`的正负号,存入栈中,然后更新`sign`并清零`nums`记录下一对儿符合和数字的组合。
所以说,此时要根据 `sign` 的 case 不同选择 `nums` 的正负号,存入栈中,然后更新 `sign` 并清零 `nums` 记录下一对儿符合和数字的组合。
另外注意,不只是遇到新的符号会触发入栈,当`i`走到了算式的尽头(`i == s.size() - 1`),也应该将前面的数字入栈,方便后续计算最终结果。
另外注意,不只是遇到新的符号会触发入栈,当 `i` 走到了算式的尽头(`i == s.size() - 1` ),也应该将前面的数字入栈,方便后续计算最终结果。
![](../pictures/calculator/2.jpg)
![](https://labuladong.github.io/algo/images/calculator/2.jpg)
至此,仅处理紧凑加减法字符串的算法就完成了,请确保理解以上内容,后续的内容就基于这个框架修修改改就完事儿了。
### 三、处理乘除法
其实思路跟仅处理加减法没啥区别,拿字符串`2-3*4+5`举例,核心思路依然是把字符串分解成符号和数字的组合。
其实思路跟仅处理加减法没啥区别,拿字符串 `2-3*4+5` 举例,核心思路依然是把字符串分解成符号和数字的组合。
比如上述例子就可以分解为`+2``-3``*4``+5`几对儿,我们刚才不是没有处理乘除号吗,很简单,**其他部分都不用变**,在`switch`部分加上对应的 case 就行了:
比如上述例子就可以分解为 `+2``-3``*4``+5` 几对儿,我们刚才不是没有处理乘除号吗,很简单,**其他部分都不用变**,在 `switch` 部分加上对应的 case 就行了:
```cpp
for (int i = 0; i < s.size(); i++) {
......@@ -168,7 +177,7 @@ for (int i = 0; i < s.size(); i++) {
}
```
![](../pictures/calculator/3.jpg)
![](https://labuladong.github.io/algo/images/calculator/3.jpg)
**乘除法优先于加减法体现在,乘除法可以和栈顶的数结合,而加减法只能把自己放入栈**
......@@ -183,7 +192,7 @@ if (!isdigit(c) || i == s.size() - 1) {
}
```
显然空格会进入这个 if 语句,但是我们并不想让空格的情况进入这个 if,因为这里会更新`sign`并清零`nums`,空格根本就不是运算符,应该被忽略。
显然空格会进入这个 if 语句,但是我们并不想让空格的情况进入这个 if,因为这里会更新 `sign` 并清零 `nums`,空格根本就不是运算符,应该被忽略。
那么只要多加一个条件即可:
......@@ -228,22 +237,24 @@ def calculate(s: str) -> int:
sign = c
return sum(stack)
# 需要把字符串转成列表方便操作
return helper(list(s))
# 需要把字符串转成双端队列方便操作
return helper(collections.deque(s))
```
这段代码跟刚才 C++ 代码完全相同,唯一的区别是,不是从左到右遍历字符串,而是不断从左边`pop`出字符,本质还是一样的。
这段代码跟刚才 C++ 代码完全相同,唯一的区别是,不是从左到右遍历字符串,而是不断从左边 `pop` 出字符,本质还是一样的。
那么,为什么说处理括号没有看起来那么难呢,**因为括号具有递归性质**。我们拿字符串`3*(4-5/2)-6`举例:
那么,为什么说处理括号没有看起来那么难呢,**因为括号具有递归性质**。我们拿字符串 `3*(4-5/2)-6` 举例:
calculate(`3*(4-5/2)-6`)
= 3 * calculate(`4-5/2`) - 6
```java
calculate(3 * (4 - 5/2) - 6)
= 3 * calculate(4 - 5/2) - 6
= 3 * 2 - 6
= 0
```
可以脑补一下,无论多少层括号嵌套,通过 calculate 函数递归调用自己,都可以将括号中的算式化简成一个数字。**换句话说,括号包含的算式,我们直接视为一个数字就行了**
现在的问题是,递归的开始条件和结束条件是什么?**遇到`(`开始递归,遇到`)`结束递归**
现在的问题是,递归的开始条件和结束条件是什么?**遇到 `(` 开始递归,遇到 `)` 结束递归**
```python
def calculate(s: str) -> int:
......@@ -254,7 +265,7 @@ def calculate(s: str) -> int:
num = 0
while len(s) > 0:
c = s.pop(0)
c = s.popleft()
if c.isdigit():
num = 10 * num + int(c)
# 遇到左括号开始递归计算 num
......@@ -262,24 +273,29 @@ def calculate(s: str) -> int:
num = helper(s)
if (not c.isdigit() and c != ' ') or len(s) == 0:
if sign == '+': ...
elif sign == '-': ...
elif sign == '*': ...
elif sign == '/': ...
if sign == '+':
stack.append(num)
elif sign == '-':
stack.append(-num)
elif sign == '*':
stack[-1] = stack[-1] * num
elif sign == '/':
# python 除法向 0 取整的写法
stack[-1] = int(stack[-1] / float(num))
num = 0
sign = c
# 遇到右括号返回递归结果
if c == ')': break
return sum(stack)
return helper(list(s))
return helper(collections.deque(s))
```
![](../pictures/calculator/4.jpg)
![](https://labuladong.github.io/algo/images/calculator/4.jpg)
![](../pictures/calculator/5.jpg)
![](https://labuladong.github.io/algo/images/calculator/5.jpg)
![](../pictures/calculator/6.jpg)
![](https://labuladong.github.io/algo/images/calculator/6.jpg)
你看,加了两三行代码,就可以处理括号了,这就是递归的魅力。至此,计算器的全部功能就实现了,通过对问题的层层拆解化整为零,再回头看,这个问题似乎也没那么复杂嘛。
......@@ -293,16 +309,12 @@ def calculate(s: str) -> int:
**退而求其次是一种很聪明策略**。你想想啊,假设这是一道考试题,你不会实现这个计算器,但是你写了字符串转整数的算法并指出了容易溢出的陷阱,那起码可以得 20 分吧;如果你能够处理加减法,那可以得 40 分吧;如果你能处理加减乘除四则运算,那起码够 70 分了;再加上处理空格字符,80 有了吧。我就是不会处理括号,那就算了,80 已经很 OK 了好不好。
**_____________**
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**
![](https://labuladong.github.io/algo/images/souyisou2.png)
<p align='center'>
<img src="../pictures/qrcode.jpg" width=200 >
</p>
======其他语言代码======
......
# 设计Twitter
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号-@labuladong-000000.svg?style=flat-square&logo=WeChat"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>
![](../pictures/souyisou.png)
![](https://labuladong.github.io/algo/images/souyisou1.png)
**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
**通知:[数据结构精品课 V1.8](https://aep.h5.xeknow.com/s/1XJHEO) 持续更新中;[第十期刷题打卡挑战](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 最后一天报名。**
读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
[355.设计推特](https://leetcode-cn.com/problems/design-twitter)
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [355. Design Twitter](https://leetcode.com/problems/design-twitter/) | [355. 设计推特](https://leetcode.cn/problems/design-twitter/) | 🟠
**-----------**
「design Twitter」是 LeetCode 上第 355 道题目,不仅题目本身很有意思,而且把合并多个有序链表的算法和面向对象设计(OO design)结合起来了,很有实际意义,本文就带大家来看看这道题。
力扣第 355 「设计推特」不仅题目本身很有意思,而且把合并多个有序链表的算法和面向对象设计(OO design)结合起来了,很有实际意义,本文就带大家来看看这道题。
至于 Twitter 的什么功能跟算法有关系,等我们描述一下题目要求就知道了。
......@@ -120,13 +127,13 @@ class Tweet {
}
```
![tweet](../pictures/设计Twitter/tweet.jpg)
![](https://labuladong.github.io/algo/images/设计Twitter/tweet.jpg)
**2、User 类的实现**
我们根据实际场景想一想,一个用户需要存储的信息有 userId,关注列表,以及该用户发过的推文列表。其中关注列表应该用集合(Hash Set)这种数据结构来存,因为不能重复,而且需要快速查找;推文列表应该由链表这种数据结构储存,以便于进行有序合并的操作。画个图理解一下:
![User](../pictures/设计Twitter/user.jpg)
![](https://labuladong.github.io/algo/images/设计Twitter/user.jpg)
除此之外,根据面向对象的设计原则,「关注」「取关」和「发文」应该是 User 的行为,况且关注列表和推文列表也存储在 User 类中,所以我们也应该给 User 添加 follow,unfollow 和 post 这几个方法:
......@@ -269,11 +276,10 @@ public List<Integer> getNewsFeed(int userId) {
这个过程是这样的,下面是我制作的一个 GIF 图描述合并链表的过程。假设有三个 Tweet 链表按 time 属性降序排列,我们把他们降序合并添加到 res 中。注意图中链表节点中的数字是 time 属性,不是 id 属性:
![gif](../pictures/设计Twitter/merge.gif)
![](https://labuladong.github.io/algo/images/设计Twitter/merge.gif)
至此,这道一个极其简化的 Twitter 时间线功能就设计完毕了。
### 四、最后总结
本文运用简单的面向对象技巧和合并 k 个有序链表的算法设计了一套简化的时间线功能,这个功能其实广泛地运用在许多社交应用中。
......@@ -282,23 +288,19 @@ public List<Integer> getNewsFeed(int userId) {
当然,实际应用中的社交 App 数据量是巨大的,考虑到数据库的读写性能,我们的设计可能承受不住流量压力,还是有些太简化了。而且实际的应用都是一个极其庞大的工程,比如下图,是 Twitter 这样的社交网站大致的系统结构:
![design](../pictures/设计Twitter/design.png)
![](https://labuladong.github.io/algo/images/设计Twitter/design.png)
我们解决的问题应该只能算 Timeline Service 模块的一小部分,功能越多,系统的复杂性可能是指数级增长的。所以说合理的顶层设计十分重要,其作用是远超某一个算法的。
最后,Github 上有一个优秀的开源项目,专门收集了很多大型系统设计的案例和解析,而且有中文版本,上面这个图也出自该项目。对系统设计感兴趣的读者可以点击 [这里](https://github.com/donnemartin/system-design-primer) 查看。
PS:本文前两张图片和 GIF 是我第一次尝试用平板的绘图软件制作的,花了很多时间,尤其是 GIF 图,需要一帧一帧制作。如果本文内容对你有帮助,点个赞分个享,鼓励一下我呗!
**_____________**
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**
![](https://labuladong.github.io/algo/images/souyisou2.png)
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**
<p align='center'>
<img src="../pictures/qrcode.jpg" width=200 >
</p>
======其他语言代码======
[355.设计推特](https://leetcode-cn.com/problems/design-twitter)
......
# 递归反转链表的一部分
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号-@labuladong-000000.svg?style=flat-square&logo=WeChat"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>
![](../pictures/souyisou.png)
![](https://labuladong.github.io/algo/images/souyisou1.png)
**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
**通知:[数据结构精品课 V1.8](https://aep.h5.xeknow.com/s/1XJHEO) 持续更新中;[第十期刷题打卡挑战](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 最后一天报名。**
读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
[92.反转链表II](https://leetcode-cn.com/problems/reverse-linked-list-ii/)
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [206. Reverse Linked List](https://leetcode.com/problems/reverse-linked-list/) | [206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/) | 🟢
| [92. Reverse Linked List II](https://leetcode.com/problems/reverse-linked-list-ii/) | [92. 反转链表 II](https://leetcode.cn/problems/reverse-linked-list-ii/) | 🟠
| - | [剑指 Offer 24. 反转链表](https://leetcode.cn/problems/fan-zhuan-lian-biao-lcof/) | 🟢
| - | [剑指 Offer II 024. 反转链表](https://leetcode.cn/problems/UHnkqh/) | 🟢
**-----------**
......@@ -31,9 +41,11 @@ public class ListNode {
}
```
什么叫反转单链表的一部分呢,就是给你一个索引区间,让你把单链表中这部分元素反转,其他部分不变:
什么叫反转单链表的一部分呢,就是给你一个索引区间,让你把单链表中这部分元素反转,其他部分不变。
看下力扣第 92 题「反转链表 II」:
![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/title.png)
![](https://labuladong.github.io/algo/images/反转链表/title.png)
**注意这里的索引是从 1 开始的**。迭代的思路大概是:先用一个 for 循环找到第 `m` 个位置,然后再用一个 for 循环将 `m``n` 之间的元素反转。但是我们的递归解法不用一个 for 循环,纯递归实现反转。
......@@ -41,11 +53,14 @@ public class ListNode {
### 一、递归反转整个链表
个算法可能很多读者都听说过,这里详细介绍一下,先直接看实现代码
也是力扣第 206 题「反转链表」,递归反转单链表的算法可能很多读者都听说过,这里详细介绍一下,直接看代码实现
```java
// 定义:输入一个单链表头结点,将该链表反转,返回新的头结点
ListNode reverse(ListNode head) {
if (head.next == null) return head;
if (head == null || head.next == null) {
return head;
}
ListNode last = reverse(head.next);
head.next.next = head;
head.next = null;
......@@ -59,9 +74,9 @@ ListNode reverse(ListNode head) {
**输入一个节点 `head`,将「以 `head` 为起点」的链表反转,并返回反转之后的头结点**
明白了函数的定义,来看这个问题。比如说我们想反转这个链表:
明白了函数的定义,来看这个问题。比如说我们想反转这个链表:
![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/1.jpg)
![](https://labuladong.github.io/algo/images/反转链表/1.jpg)
那么输入 `reverse(head)` 后,会在这里进行递归:
......@@ -71,11 +86,11 @@ ListNode last = reverse(head.next);
不要跳进递归(你的脑袋能压几个栈呀?),而是要根据刚才的函数定义,来弄清楚这段代码会产生什么结果:
![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/2.jpg)
![](https://labuladong.github.io/algo/images/反转链表/2.jpg)
这个 `reverse(head.next)` 执行完成后,整个链表就成了这样:
![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/3.jpg)
![](https://labuladong.github.io/algo/images/反转链表/3.jpg)
并且根据函数定义,`reverse` 函数会返回反转之后的头结点,我们用变量 `last` 接收了。
......@@ -85,7 +100,7 @@ ListNode last = reverse(head.next);
head.next.next = head;
```
![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/4.jpg)
![](https://labuladong.github.io/algo/images/反转链表/4.jpg)
接下来:
......@@ -94,17 +109,19 @@ head.next = null;
return last;
```
![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/5.jpg)
![](https://labuladong.github.io/algo/images/反转链表/5.jpg)
神不神奇,这样整个链表就反转过来了!递归代码就是这么简洁优雅,不过其中有两个地方需要注意:
1、递归函数要有 base case,也就是这句:
```java
if (head.next == null) return head;
if (head == null || head.next == null) {
return head;
}
```
意思是如果链表只有一个节点的时候反转也是它自己,直接返回即可。
意思是如果链表为空或者只有一个节点的时候,反转结果就是它自己,直接返回即可。
2、当链表递归反转之后,新的头结点是 `last`,而之前的 `head` 变成了最后一个节点,别忘了链表的末尾要指向 null:
......@@ -125,7 +142,7 @@ ListNode reverseN(ListNode head, int n)
比如说对于下图链表,执行 `reverseN(head, 3)`
![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/6.jpg)
![](https://labuladong.github.io/algo/images/反转链表/6.jpg)
解决思路和反转整个链表差不多,只要稍加修改即可:
......@@ -134,7 +151,7 @@ ListNode successor = null; // 后驱节点
// 反转以 head 为起点的 n 个节点,返回新的头结点
ListNode reverseN(ListNode head, int n) {
if (n == 1) {
if (n == 1) {
// 记录第 n + 1 个节点
successor = head.next;
return head;
......@@ -146,22 +163,22 @@ ListNode reverseN(ListNode head, int n) {
// 让反转之后的 head 节点和后面的节点连起来
head.next = successor;
return last;
}
}
```
具体的区别:
1、base case 变为 `n == 1`,反转一个元素,就是它本身,同时**要记录后驱节点**
2、刚才我们直接把 `head.next` 设置为 null,因为整个链表反转后原来的 `head` 变成了整个链表的最后一个节点。但现在 `head` 节点在递归反转之后不一定是最后一个节点了,所以要记录后驱 `successor`(第 n + 1 个节点),反转之后将 `head` 连接上。
2、刚才我们直接把 `head.next` 设置为 null,因为整个链表反转后原来的 `head` 变成了整个链表的最后一个节点。但现在 `head` 节点在递归反转之后不一定是最后一个节点了,所以要记录后驱 `successor`(第 `n + 1` 个节点),反转之后将 `head` 连接上。
![](../pictures/%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/7.jpg)
![](https://labuladong.github.io/algo/images/反转链表/7.jpg)
OK,如果这个函数你也能看懂,就离实现「反转一部分链表」不远了。
### 三、反转链表的一部分
现在解决我们最开始提出的问题,给一个索引区间 `[m,n]`(索引从 1 开始),仅仅反转区间中的链表元素。
现在解决我们最开始提出的问题,给一个索引区间 `[m, n]`(索引从 1 开始),仅仅反转区间中的链表元素。
```java
ListNode reverseBetween(ListNode head, int m, int n)
......@@ -206,15 +223,15 @@ ListNode reverseBetween(ListNode head, int m, int n) {
值得一提的是,递归操作链表并不高效。和迭代解法相比,虽然时间复杂度都是 O(N),但是迭代解法的空间复杂度是 O(1),而递归解法需要堆栈,空间复杂度是 O(N)。所以递归操作链表可以作为对递归算法的练习或者拿去和小伙伴装逼,但是考虑效率的话还是使用迭代算法更好。
> 最后打个广告,我亲自制作了一门 [数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO),以视频课为主,手把手带你实现常用的数据结构及相关算法,旨在帮助算法基础较为薄弱的读者深入理解常用数据结构的底层原理,在算法学习中少走弯路。
**_____________**
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**
![](https://labuladong.github.io/algo/images/souyisou2.png)
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**
<p align='center'>
<img src="../pictures/qrcode.jpg" width=200 >
</p>
======其他语言代码======
[92.反转链表II](https://leetcode-cn.com/problems/reverse-linked-list-ii/)
......
# 队列实现栈|栈实现队列
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号-@labuladong-000000.svg?style=flat-square&logo=WeChat"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>
![](../pictures/souyisou.png)
![](https://labuladong.github.io/algo/images/souyisou1.png)
**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
**通知:[数据结构精品课 V1.8](https://aep.h5.xeknow.com/s/1XJHEO) 持续更新中;[第十期刷题打卡挑战](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 最后一天报名。**
读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
[232.用栈实现队列](https://leetcode-cn.com/problems/implement-queue-using-stacks)
[225.用队列实现栈](https://leetcode-cn.com/problems/implement-stack-using-queues)
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [225. Implement Stack using Queues](https://leetcode.com/problems/implement-stack-using-queues/) | [225. 用队列实现栈](https://leetcode.cn/problems/implement-stack-using-queues/) | 🟢
| [232. Implement Queue using Stacks](https://leetcode.com/problems/implement-queue-using-stacks/) | [232. 用栈实现队列](https://leetcode.cn/problems/implement-queue-using-stacks/) | 🟢
| - | [剑指 Offer 09. 用两个栈实现队列](https://leetcode.cn/problems/yong-liang-ge-zhan-shi-xian-dui-lie-lcof/) | 🟢
**-----------**
队列是一种先进先出的数据结构,栈是一种先进后出的数据结构,形象一点就是这样:
![](../pictures/%E6%A0%88%E9%98%9F%E5%88%97/1.jpg)
![](https://labuladong.github.io/algo/images/栈队列/1.jpg)
这两种数据结构底层其实都是数组或者链表实现的,只是 API 限定了它们的特性,那么今天就来看看如何使用「栈」的特性来实现一个「队列」,如何用「队列」实现一个「栈」。
### 一、用栈实现队列
首先,队列的 API 如下:
力扣第 232 题「用栈实现队列」让我们实现的 API 如下:
```java
class MyQueue {
......@@ -49,7 +56,7 @@ class MyQueue {
我们使用两个栈 `s1, s2` 就能实现一个队列的功能(这样放置栈可能更容易理解):
![](../pictures/%E6%A0%88%E9%98%9F%E5%88%97/2.jpg)
![](https://labuladong.github.io/algo/images/栈队列/2.jpg)
```java
class MyQueue {
......@@ -65,7 +72,7 @@ class MyQueue {
当调用 `push` 让元素入队时,只要把元素压入 `s1` 即可,比如说 `push` 进 3 个元素分别是 1,2,3,那么底层结构就是这样:
![](../pictures/%E6%A0%88%E9%98%9F%E5%88%97/3.jpg)
![](https://labuladong.github.io/algo/images/栈队列/3.jpg)
```java
/** 添加元素到队尾 */
......@@ -76,7 +83,7 @@ public void push(int x) {
那么如果这时候使用 `peek` 查看队头的元素怎么办呢?按道理队头元素应该是 1,但是在 `s1` 中 1 被压在栈底,现在就要轮到 `s2` 起到一个中转的作用了:当 `s2` 为空时,可以把 `s1` 的所有元素取出再添加进 `s2`**这时候 `s2` 中元素就是先进先出顺序了**
![](../pictures/%E6%A0%88%E9%98%9F%E5%88%97/4.jpg)
![](https://labuladong.github.io/algo/images/栈队列/4.jpg)
```java
/** 返回队头元素 */
......@@ -119,7 +126,9 @@ public boolean empty() {
### 二、用队列实现栈
如果说双栈实现队列比较巧妙,那么用队列实现栈就比较简单粗暴了,只需要一个队列作为底层数据结构。首先看下栈的 API:
如果说双栈实现队列比较巧妙,那么用队列实现栈就比较简单粗暴了,只需要一个队列作为底层数据结构。
力扣第 225 题「用队列实现栈」让我们实现如下 API:
```java
class MyStack {
......@@ -159,13 +168,13 @@ class MyStack {
}
```
我们的底层数据结构是先进先出的队列,每次 `pop` 只能从队头取元素;但是栈是后进先出,也就是说 `pop` API 要从队尾取元素
我们的底层数据结构是先进先出的队列,每次 `pop` 只能从队头取元素;但是栈是后进先出,也就是说 `pop` API 要从队尾取元素
![](../pictures/%E6%A0%88%E9%98%9F%E5%88%97/5.jpg)
![](https://labuladong.github.io/algo/images/栈队列/5.jpg)
解决方法简单粗暴,把队列前面的都取出来再加入队尾,让之前的队尾元素排到队头,这样就可以取出了:
![](../pictures/%E6%A0%88%E9%98%9F%E5%88%97/6.jpg)
![](https://labuladong.github.io/algo/images/栈队列/6.jpg)
```java
/** 删除栈顶的元素并返回 */
......@@ -212,7 +221,7 @@ public boolean empty() {
个人认为,用队列实现栈是没啥亮点的问题,但是**用双栈实现队列是值得学习的**
![](../pictures/%E6%A0%88%E9%98%9F%E5%88%97/4.jpg)
![](https://labuladong.github.io/algo/images/栈队列/4.jpg)
从栈 `s1` 搬运元素到 `s2` 之后,元素在 `s2` 中就变成了队列的先进先出顺序,这个特性有点类似「负负得正」,确实不太容易想到。
......@@ -220,13 +229,10 @@ public boolean empty() {
**_____________**
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**
![](https://labuladong.github.io/algo/images/souyisou2.png)
<p align='center'>
<img src="../pictures/qrcode.jpg" width=200 >
</p>
======其他语言代码======
......
# FloodFill算法详解及应用
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号-@labuladong-000000.svg?style=flat-square&logo=WeChat"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>
![](../pictures/souyisou.png)
![](https://labuladong.github.io/algo/images/souyisou1.png)
**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
**通知:[数据结构精品课 V1.8](https://aep.h5.xeknow.com/s/1XJHEO) 持续更新中;[第十期刷题打卡挑战](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 最后一天报名。**
读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
[733.图像渲染](https://leetcode-cn.com/problems/flood-fill)
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [733. Flood Fill](https://leetcode.com/problems/flood-fill/) | [733. 图像渲染](https://leetcode.cn/problems/flood-fill/) | 🟢
**-----------**
啥是 FloodFill 算法呢,最直接的一个应用就是「颜色填充」,就是 Windows 绘画本中那个小油漆桶的标志,可以把一块被圈起来的区域全部染色。
![floodfill](../pictures/floodfill/floodfill.gif)
![](https://labuladong.github.io/algo/images/floodfill/floodfill.gif)
这种算法思想还在许多其他地方有应用。比如说扫雷游戏,有时候你点一个方格,会一下子展开一片区域,这个展开过程,就是 FloodFill 算法实现的。
![扫雷](../pictures/floodfill/扫雷.png)
![](https://labuladong.github.io/algo/images/floodfill/扫雷.png)
类似的,像消消乐这类游戏,相同方块积累到一定数量,就全部消除,也是 FloodFill 算法的功劳。
![xiaoxiaole](../pictures/floodfill/xiaoxiaole.jpg)
![](https://labuladong.github.io/algo/images/floodfill/xiaoxiaole.jpg)
通过以上的几个例子,你应该对 FloodFill 算法有个概念了,现在我们要抽象问题,提取共同点。
......@@ -52,7 +57,7 @@ void fill(int x, int y) {
下面看一道 LeetCode 题目,其实就是让我们来实现一个「颜色填充」功能。
![title](../pictures/floodfill/leetcode.png)
![](https://labuladong.github.io/algo/images/floodfill/leetcode.png)
根据上篇文章,我们讲了「树」算法设计的一个总路线,今天就可以用到:
......@@ -89,11 +94,11 @@ boolean inArea(int[][] image, int x, int y) {
### 二、研究细节
为什么会陷入无限递归呢,很好理解,因为每个坐标都要搜索上下左右,那么对于一个坐标,一定会被上下左右的坐标搜索。**被重复搜索时,必须保证递归函数能够能正确地退出,否则就会陷入死循环。**
为什么会陷入无限递归呢,很好理解,因为每个坐标都要搜索上下左右,那么对于一个坐标,一定会被上下左右的坐标搜索。**被重复搜索时,必须保证递归函数能够能正确地退出,否则就会陷入死循环**
为什么 newColor 和 origColor 不同时可以正常退出呢?把算法流程画个图理解一下:
![ppt1](../pictures/floodfill/ppt1.PNG)
![](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 语句被怼回去,正确退出了。
......@@ -101,11 +106,11 @@ boolean inArea(int[][] image, int x, int y) {
// 碰壁:遇到其他颜色,超出 origColor 区域
if (image[x][y] != origColor) return;
```
![ppt2](../pictures/floodfill/ppt2.PNG)
![](https://labuladong.github.io/algo/images/floodfill/ppt2.PNG)
但是,如果说 origColor 和 newColor 一样,这个 if 语句就无法让 fill(1, 1)* 正确退出,而是开启了下面的重复递归,形成了死循环。
![ppt3](../pictures/floodfill/ppt3.PNG)
![](https://labuladong.github.io/algo/images/floodfill/ppt3.PNG)
### 三、处理细节
......@@ -124,7 +129,7 @@ image[x][y] = newColor;
完全 OK,这也是处理「图」的一种常用手段。不过对于此题,不用开数组,我们有一种更好的方法,那就是回溯算法。
前文 [回溯算法框架套路](https://labuladong.gitee.io/algo/)讲过,这里不再赘述,直接套回溯算法框架:
前文 [回溯算法框架套路](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版)讲过,这里不再赘述,直接套回溯算法框架:
```java
void fill(int[][] image, int x, int y,
......@@ -149,12 +154,11 @@ void fill(int[][] image, int x, int y,
这种解决方法是最常用的,相当于使用一个特殊值 -1 代替 visited 数组的作用,达到不走回头路的效果。为什么是 -1,因为题目中说了颜色取值在 0 - 65535 之间,所以 -1 足够特殊,能和颜色区分开。
### 四、拓展延伸:自动魔棒工具和扫雷
大部分图片编辑软件一定有「自动魔棒工具」这个功能:点击一个地方,帮你自动选中相近颜色的部分。如下图,我想选中老鹰,可以先用自动魔棒选中蓝天背景,然后反向选择,就选中了老鹰。我们来分析一下自动魔棒工具的原理。
![抠图](../pictures/floodfill/抠图.jpg)
![](https://labuladong.github.io/algo/images/floodfill/抠图.jpg)
显然,这个算法肯定是基于 FloodFill 算法的,但有两点不同:首先,背景色是蓝色,但不能保证都是相同的蓝色,毕竟是像素点,可能存在肉眼无法分辨的深浅差异,而我们希望能够忽略这种细微差异。第二,FloodFill 算法是「区域填充」,这里更像「边界填充」。
......@@ -167,7 +171,7 @@ if (Math.abs(image[x][y] - origColor) > threshold)
对于第二个问题,我们首先明确问题:不要把区域内所有 origColor 的都染色,而是只给区域最外圈染色。然后,我们分析,如何才能仅给外围染色,即如何才能找到最外围坐标,最外围坐标有什么特点?
![ppt4](../pictures/floodfill/ppt4.PNG)
![](https://labuladong.github.io/algo/images/floodfill/ppt4.PNG)
可以发现,区域边界上的坐标,至少有一个方向不是 origColor,而区域内部的坐标,四面都是 origColor,这就是解决问题的关键。保持框架不变,使用 visited 数组记录已搜索坐标,主要代码如下:
......@@ -223,19 +227,16 @@ int fill(int[][] image, int x, int y,
同理,思考扫雷游戏,应用 FloodFill 算法展开空白区域的同时,也需要计算并显示边界上雷的个数,如何实现的?其实也是相同的思路,遇到雷就返回 true,这样 surround 变量存储的就是雷的个数。当然,扫雷的 FloodFill 算法不能只检查上下左右,还得加上四个斜向。
![](../pictures/floodfill/ppt5.PNG)
![](https://labuladong.github.io/algo/images/floodfill/ppt5.PNG)
以上详细讲解了 FloodFill 算法的框架设计,**二维矩阵中的搜索问题,都逃不出这个算法框架**
**_____________**
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**
![](https://labuladong.github.io/algo/images/souyisou2.png)
<p align='center'>
<img src="../pictures/qrcode.jpg" width=200 >
</p>
======其他语言代码======
......
# 前缀和技巧
# 经典数组技巧:前缀和数组
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号-@labuladong-000000.svg?style=flat-square&logo=WeChat"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>
![](../pictures/souyisou.png)
![](https://labuladong.github.io/algo/images/souyisou1.png)
**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
**通知:[数据结构精品课 V1.8](https://aep.h5.xeknow.com/s/1XJHEO) 持续更新中;[第十期刷题打卡挑战](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 最后一天报名。**
读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
[560.和为K的子数组](https://leetcode-cn.com/problems/subarray-sum-equals-k)
**-----------**
今天来聊一道简单却十分巧妙的算法问题:算出一共有几个和为 `k` 的子数组。
![](../pictures/%E5%89%8D%E7%BC%80%E5%92%8C/title.png)
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
那我把所有子数组都穷举出来,算它们的和,看看谁的和等于 `k` 不就行了。
| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [303. Range Sum Query - Immutable](https://leetcode.com/problems/range-sum-query-immutable/) | [303. 区域和检索 - 数组不可变](https://leetcode.cn/problems/range-sum-query-immutable/) | 🟢
| [304. Range Sum Query 2D - Immutable](https://leetcode.com/problems/range-sum-query-2d-immutable/) | [304. 二维区域和检索 - 矩阵不可变](https://leetcode.cn/problems/range-sum-query-2d-immutable/) | 🟠
| - | [剑指 Offer II 013. 二维子矩阵的和](https://leetcode.cn/problems/O4NDxx/) | 🟠
关键是,**如何快速得到某个子数组的和呢**,比如说给你一个数组 `nums`,让你实现一个接口 `sum(i, j)`,这个接口要返回 `nums[i..j]` 的和,而且会被多次调用,你怎么实现这个接口呢?
因为接口要被多次调用,显然不能每次都去遍历 `nums[i..j]`,有没有一种快速的方法在 O(1) 时间内算出 `nums[i..j]` 呢?这就需要**前缀和**技巧了。
**-----------**
### 一、什么是前缀和
> 本文有视频版:[前缀和/差分数组技巧精讲](https://www.bilibili.com/video/BV1NY4y1J7xQ/)
前缀和的思路是这样的,对于一个给定的数组 `nums`,我们额外开辟一个前缀和数组进行预处理:
前缀和技巧适用于快速、频繁地计算一个索引区间内的元素之和。
```java
int n = nums.length;
// 前缀和数组
int[] preSum = new int[n + 1];
preSum[0] = 0;
for (int i = 0; i < n; i++)
preSum[i + 1] = preSum[i] + nums[i];
```
### 一维数组中的前缀和
![](../pictures/%E5%89%8D%E7%BC%80%E5%92%8C/1.jpg)
先看一道例题,力扣第 303 题「区域和检索 - 数组不可变」,让你计算数组区间内元素的和,这是一道标准的前缀和问题:
这个前缀和数组 `preSum` 的含义也很好理解,`preSum[i]` 就是 `nums[0..i-1]` 的和。那么如果我们想求 `nums[i..j]` 的和,只需要一步操作 `preSum[j+1]-preSum[i]` 即可,而不需要重新去遍历数组了。
![](https://labuladong.github.io/algo/images/前缀和/title1.png)
回到这个子数组问题,我们想求有多少个子数组的和为 k,借助前缀和技巧很容易写出一个解法
题目要求你实现这样一个类
```java
int subarraySum(int[] nums, int k) {
int n = nums.length;
// 构造前缀和
int[] sum = new int[n + 1];
sum[0] = 0;
for (int i = 0; i < n; i++)
sum[i + 1] = sum[i] + nums[i];
int ans = 0;
// 穷举所有子数组
for (int i = 1; i <= n; i++)
for (int j = 0; j < i; j++)
// sum of nums[j..i-1]
if (sum[i] - sum[j] == k)
ans++;
class NumArray {
return ans;
public NumArray(int[] nums) {}
/* 查询闭区间 [left, right] 的累加和 */
public int sumRange(int left, int right) {}
}
```
这个解法的时间复杂度 `O(N^2)` 空间复杂度 `O(N)`,并不是最优的解法。不过通过这个解法理解了前缀和数组的工作原理之后,可以使用一些巧妙的办法把时间复杂度进一步降低。
`sumRange` 函数需要计算并返回一个索引区间之内的元素和,没学过前缀和的人可能写出如下代码:
### 二、优化解法
```java
class NumArray {
前面的解法有嵌套的 for 循环:
private int[] nums;
```java
for (int i = 1; i <= n; i++)
for (int j = 0; j < i; j++)
if (sum[i] - sum[j] == k)
ans++;
public NumArray(int[] nums) {
this.nums = nums;
}
public int sumRange(int left, int right) {
int res = 0;
for (int i = left; i <= right; i++) {
res += nums[i];
}
return res;
}
}
```
第二层 for 循环在干嘛呢?翻译一下就是,**在计算,有几个 `j` 能够使得 `sum[i]` 和 `sum[j]` 的差为 k。**毎找到一个这样的 `j`,就把结果加一
这样,可以达到效果,但是效率很差,因为 `sumRange` 方法会被频繁调用,而它的时间复杂度是 `O(N)`,其中 `N` 代表 `nums` 数组的长度
我们可以把 if 语句里的条件判断移项,这样写:
```java
if (sum[j] == sum[i] - k)
ans++;
```
这道题的最优解法是使用前缀和技巧,将 `sumRange` 函数的时间复杂度降为 `O(1)`,说白了就是不要在 `sumRange` 里面用 for 循环,咋整?
优化的思路是:**我直接记录下有几个 `sum[j]` 和 `sum[i] - k` 相等,直接更新结果,就避免了内层的 for 循环**。我们可以用哈希表,在记录前缀和的同时记录该前缀和出现的次数。
直接看代码实现:
```java
int subarraySum(int[] nums, int k) {
int n = nums.length;
// map:前缀和 -> 该前缀和出现的次数
HashMap<Integer, Integer>
preSum = new HashMap<>();
// base case
preSum.put(0, 1);
int ans = 0, sum0_i = 0;
for (int i = 0; i < n; i++) {
sum0_i += nums[i];
// 这是我们想找的前缀和 nums[0..j]
int sum0_j = sum0_i - k;
// 如果前面有这个前缀和,则直接更新答案
if (preSum.containsKey(sum0_j))
ans += preSum.get(sum0_j);
// 把前缀和 nums[0..i] 加入并记录出现次数
preSum.put(sum0_i,
preSum.getOrDefault(sum0_i, 0) + 1);
class NumArray {
// 前缀和数组
private int[] preSum;
/* 输入一个数组,构造前缀和 */
public NumArray(int[] nums) {
// preSum[0] = 0,便于计算累加和
preSum = new int[nums.length + 1];
// 计算 nums 的累加和
for (int i = 1; i < preSum.length; i++) {
preSum[i] = preSum[i - 1] + nums[i - 1];
}
}
/* 查询闭区间 [left, right] 的累加和 */
public int sumRange(int left, int right) {
return preSum[right + 1] - preSum[left];
}
return ans;
}
```
比如说下面这个情况,需要前缀和 8 就能找到和为 k 的子数组了,之前的暴力解法需要遍历数组去数有几个 8,而优化解法借助哈希表可以直接得知有几个前缀和为 8。
核心思路是我们 new 一个新的数组 `preSum` 出来,`preSum[i]` 记录 `nums[0..i-1]` 的累加和,看图 10 = 3 + 5 + 2:
![](../pictures/%E5%89%8D%E7%BC%80%E5%92%8C/2.jpg)
![](https://labuladong.github.io/algo/images/差分数组/1.jpeg)
这样,就把时间复杂度降到了 `O(N)`,是最优解法了
看这个 `preSum` 数组,如果我想求索引区间 `[1, 4]` 内的所有元素之和,就可以通过 `preSum[5] - preSum[1]` 得出
### 三、总结
这样,`sumRange` 函数仅仅需要做一次减法运算,避免了每次进行 for 循环调用,最坏时间复杂度为常数 `O(1)`
前缀和不难,却很有用,主要用于处理数组区间的问题
这个技巧在生活中运用也挺广泛的,比方说,你们班上有若干同学,每个同学有一个期末考试的成绩(满分 100 分),那么请你实现一个 API,输入任意一个分数段,返回有多少同学的成绩在这个分数段内
比如说,让你统计班上同学考试成绩在不同分数段的百分比,也可以利用前缀和技巧
那么,你可以先通过计数排序的方式计算每个分数具体有多少个同学,然后利用前缀和技巧来实现分数段查询的 API
```java
int[] scores; // 存储着所有同学的分数
// 试卷满分 150 分
int[] count = new int[150 + 1]
// 试卷满分 100 分
int[] count = new int[100 + 1]
// 记录每个分数有几个同学
for (int score : scores)
count[score]++
// 构造前缀和
for (int i = 1; i < count.length; i++)
count[i] = count[i] + count[i-1];
// 利用 count 这个前缀和数组进行分数段查询
```
接下来,我们看一看前缀和思路在二维数组中如何运用。
### 二维矩阵中的前缀和
这是力扣第 304 题「二维区域和检索 - 矩阵不可变」,其实和上一题类似,上一题是让你计算子数组的元素之和,这道题让你计算二维矩阵中子矩阵的元素之和:
![](https://labuladong.github.io/algo/images/前缀和/title2.png)
比如说输入的 `matrix` 如下图:
![](https://labuladong.github.io/algo/images/前缀和/4.png)
按照题目要求,矩阵左上角为坐标原点 `(0, 0)`,那么 `sumRegion([2,1,4,3])` 就是图中红色的子矩阵,你需要返回该子矩阵的元素和 8。
当然,你可以用一个嵌套 for 循环去遍历这个矩阵,但这样的话 `sumRegion` 函数的时间复杂度就高了,你算法的格局就低了。
注意任意子矩阵的元素和可以转化成它周边几个大矩阵的元素和的运算:
![](https://labuladong.github.io/algo/images/前缀和/5.jpeg)
而这四个大矩阵有一个共同的特点,就是左上角都是 `(0, 0)` 原点。
那么做这道题更好的思路和一维数组中的前缀和是非常类似的,我们可以维护一个二维 `preSum` 数组,专门记录以原点为顶点的矩阵的元素之和,就可以用几次加减运算算出任何一个子矩阵的元素和:
```java
class NumMatrix {
// 定义:preSum[i][j] 记录 matrix 中子矩阵 [0, 0, i-1, j-1] 的元素和
private int[][] preSum;
public NumMatrix(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
if (m == 0 || n == 0) return;
// 构造前缀和矩阵
preSum = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// 计算每个矩阵 [0, 0, i, j] 的元素和
preSum[i][j] = preSum[i-1][j] + preSum[i][j-1] + matrix[i - 1][j - 1] - preSum[i-1][j-1];
}
}
}
// 计算子矩阵 [x1, y1, x2, y2] 的元素和
public int sumRegion(int x1, int y1, int x2, int y2) {
// 目标矩阵之和由四个相邻矩阵运算获得
return preSum[x2+1][y2+1] - preSum[x1][y2+1] - preSum[x2+1][y1] + preSum[x1][y1];
}
}
```
这样,给你任何一个分数段,你都能通过前缀和相减快速计算出这个分数段的人数,百分比也就很容易计算了。
这样,`sumRegion` 函数的时间复杂度也用前缀和技巧优化到了 O(1),这是典型的「空间换时间」思路。
前缀和技巧就讲到这里,应该说这个算法技巧是会者不难难者不会,实际运用中还是要多培养自己的思维灵活性,做到一眼看出题目是一个前缀和问题。
但是,稍微复杂一些的算法问题,不止考察简单的前缀和技巧。比如本文探讨的这道题目,就需要借助前缀和的思路做进一步的优化,借助哈希表去除不必要的嵌套循环。可见对题目的理解和细节的分析能力对于算法的优化是至关重要的
除了本文举例的基本用法,前缀和数组经常和其他数据结构或算法技巧相结合,我会在 [前缀和技巧高频习题](https://appktavsiei5995.pc.xiaoe-tech.com/detail/i_627cd61de4b0cedf38b0f3a0/1) 中举例讲解
希望本文对你有帮助。
**_____________**
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**
![](https://labuladong.github.io/algo/images/souyisou2.png)
<p align='center'>
<img src="../pictures/qrcode.jpg" width=200 >
</p>
======其他语言代码======
......
# 回溯算法详解
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号-@labuladong-000000.svg?style=flat-square&logo=WeChat"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>
![](../pictures/souyisou.png)
![](https://labuladong.github.io/algo/images/souyisou1.png)
**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
**通知:[数据结构精品课 V1.8](https://aep.h5.xeknow.com/s/1XJHEO) 持续更新中;[第十期刷题打卡挑战](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 最后一天报名。**
读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
[46.全排列](https://leetcode-cn.com/problems/permutations)
[51.N皇后](https://leetcode-cn.com/problems/n-queens)
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [46. Permutations](https://leetcode.com/problems/permutations/) | [46. 全排列](https://leetcode.cn/problems/permutations/) | 🟠
| [51. N-Queens](https://leetcode.com/problems/n-queens/) | [51. N 皇后](https://leetcode.cn/problems/n-queens/) | 🔴
| - | [剑指 Offer II 083. 没有重复元素集合的全排列](https://leetcode.cn/problems/VvJkup/) | 🟠
**-----------**
本文有视频版:[回溯算法框架套路详解](https://www.bilibili.com/video/BV1P5411N7Xc)
> 本文有视频版:[回溯算法框架套路详解](https://www.bilibili.com/video/BV1P5411N7Xc/)
这篇文章是很久之前的一篇 [回溯算法详解](https://mp.weixin.qq.com/s/trILKSiN9EoS58pXmvUtUQ) 的进阶版,之前那篇不够清楚,就不必看了,看这篇就行。把框架给你讲清楚,你会发现回溯算法问题都是一个套路。
废话不多说,直接上回溯算法框架。**解决一个回溯问题,实际上就是一个决策树的遍历过程**。你只需要思考 3 个问题:
本文解决几个问题:
回溯算法是什么?解决回溯算法相关的问题有什么技巧?如何学习回溯算法?回溯算法代码是否有规律可循?
其实回溯算法和我们常说的 DFS 算法非常类似,本质上就是一种暴力穷举算法。回溯算法和 DFS 算法的细微差别是:**回溯算法是在遍历「树枝」,DFS 算法是在遍历「节点」**,本文就是简单提一下,等你看到后文 [图论算法基础](https://labuladong.github.io/article/fname.html?fname=图) 时就能深刻理解这句话的含义了。
废话不多说,直接上回溯算法框架,解决一个回溯问题,实际上就是一个决策树的遍历过程,站在回溯树的一个节点上,你只需要思考 3 个问题:
1、路径:也就是已经做出的选择。
......@@ -55,54 +68,61 @@ def backtrack(路径, 选择列表):
### 一、全排列问题
我们在高中的时候就做过排列组合的数学题,我们也知道 `n` 个不重复的数,全排列共有 n! 个。
力扣第 46 题「全排列」就是给你输入一个数组 `nums`,让你返回这些数字的全排列。
> **PS:我们这次讨论的全排列问题不包含重复的数字,包含重复数字的扩展场景我在后文 [回溯算法秒杀排列组合子集的九种题型](https://labuladong.github.io/article/fname.html?fname=子集排列组合) 中讲解**。
PS:**为了简单清晰起见,我们这次讨论的全排列问题不包含重复的数字**
我们在高中的时候就做过排列组合的数学题,我们也知道 `n` 个不重复的数,全排列共有 `n!` 个。那么我们当时是怎么穷举全排列的呢?
那么我们当时是怎么穷举全排列的呢?比方说给三个数 `[1,2,3]`,你肯定不会无规律地乱穷举,一般是这样:
比方说给三个数 `[1,2,3]`,你肯定不会无规律地乱穷举,一般是这样:
先固定第一位为 1,然后第二位可以是 2,那么第三位只能是 3;然后可以把第二位变成 3,第三位就只能是 2 了;然后就只能变化第一位,变成 2,然后再穷举后两位……
其实这就是回溯算法,我们高中无师自通就会用,或者有的同学直接画出如下这棵回溯树:
![](../pictures/backtracking/1.jpg)
![](https://labuladong.github.io/algo/images/backtracking/1.jpg)
只要从根遍历这棵树,记录路径上的数字,其实就是所有的全排列。**我们不妨把这棵树称为回溯算法的「决策树」**
**为啥说这是决策树呢,因为你在每个节点上其实都在做决策**。比如说你站在下图的红色节点上:
![](../pictures/backtracking/2.jpg)
![](https://labuladong.github.io/algo/images/backtracking/2.jpg)
你现在就在做决策,可以选择 1 那条树枝,也可以选择 3 那条树枝。为啥只能在 1 和 3 之中选择呢?因为 2 这个树枝在你身后,这个选择你之前做过了,而全排列是不允许重复使用数字的。
**现在可以解答开头的几个名词:`[2]` 就是「路径」,记录你已经做过的选择;`[1,3]` 就是「选择列表」,表示你当前可以做出的选择;「结束条件」就是遍历到树的底层,在这里就是选择列表为空的时候**
**现在可以解答开头的几个名词:`[2]` 就是「路径」,记录你已经做过的选择;`[1,3]` 就是「选择列表」,表示你当前可以做出的选择;「结束条件」就是遍历到树的底层叶子节点,这里也就是选择列表为空的时候**
如果明白了这几个名词,**可以把「路径」和「选择」列表作为决策树上每个节点的属性**,比如下图列出了几个节点的属性:
如果明白了这几个名词,可以把「路径」和「选择」列表作为决策树上每个节点的属性,比如下图列出了几个蓝色节点的属性:
![](../pictures/backtracking/3.jpg)
![](https://labuladong.github.io/algo/images/backtracking/3.jpg)
**我们定义的 `backtrack` 函数其实就像一个指针,在这棵树上游走,同时要正确维护每个节点的属性,每当走到树的底层,其「路径」就是一个全排列**
**我们定义的 `backtrack` 函数其实就像一个指针,在这棵树上游走,同时要正确维护每个节点的属性,每当走到树的底层叶子节点,其「路径」就是一个全排列**
再进一步,如何遍历一棵树?这个应该不难吧。回忆一下之前「学习数据结构的框架思维」写过,各种搜索问题其实都是树的遍历问题,而多叉树的遍历框架就是这样:
再进一步,如何遍历一棵树?这个应该不难吧。回忆一下之前 [学习数据结构的框架思维](https://labuladong.github.io/article/fname.html?fname=学习数据结构和算法的高效方法) 写过,各种搜索问题其实都是树的遍历问题,而多叉树的遍历框架就是这样:
```java
void traverse(TreeNode root) {
for (TreeNode child : root.children)
// 前序遍历需要的操作
for (TreeNode child : root.childern) {
// 前序位置需要的操作
traverse(child);
// 后序遍历需要的操作
// 后序位置需要的操作
}
}
```
> PS:细心的读者肯定会疑问:多叉树 DFS 遍历框架的前序位置和后序位置应该在 for 循环外面,并不应该是在 for 循环里面呀?为什么在回溯算法中跑到 for 循环里面了?
>
> 是的,DFS 算法的前序和后序位置应该在 for 循环外面,不过回溯算法和 DFS 算法略有不同,后文 [图论算法基础](https://labuladong.github.io/article/fname.html?fname=图) 会详细对比,这里可以暂且忽略这个问题。
而所谓的前序遍历和后序遍历,他们只是两个很有用的时间点,我给你画张图你就明白了:
![](../pictures/backtracking/4.jpg)
![](https://labuladong.github.io/algo/images/backtracking/4.jpg)
**前序遍历的代码在进入某一个节点之前的那个时间点执行,后序遍历代码在离开某个节点之后的那个时间点执行**
回想我们刚才说的,「路径」和「选择」是每个节点的属性,函数在树上游走要正确维护节点的属性,那么就要在这两个特殊时间点搞点动作:
![](../pictures/backtracking/5.jpg)
![](https://labuladong.github.io/algo/images/backtracking/5.jpg)
现在,你是否理解了回溯算法的这段核心框架?
......@@ -128,14 +148,17 @@ List<List<Integer>> res = new LinkedList<>();
List<List<Integer>> permute(int[] nums) {
// 记录「路径」
LinkedList<Integer> track = new LinkedList<>();
backtrack(nums, track);
// 「路径」中的元素会被标记为 true,避免重复使用
boolean[] used = new boolean[nums.length];
backtrack(nums, track, used);
return res;
}
// 路径:记录在 track 中
// 选择列表:nums 中不存在于 track 的那些元素
// 选择列表:nums 中不存在于 track 的那些元素(used[i] 为 false)
// 结束条件:nums 中的元素全都在 track 中出现
void backtrack(int[] nums, LinkedList<Integer> track) {
void backtrack(int[] nums, LinkedList<Integer> track, boolean[] used) {
// 触发结束条件
if (track.size() == nums.length) {
res.add(new LinkedList(track));
......@@ -144,23 +167,27 @@ void backtrack(int[] nums, LinkedList<Integer> track) {
for (int i = 0; i < nums.length; i++) {
// 排除不合法的选择
if (track.contains(nums[i]))
if (used[i]) {
// nums[i] 已经在 track 中,跳过
continue;
}
// 做选择
track.add(nums[i]);
used[i] = true;
// 进入下一层决策树
backtrack(nums, track);
backtrack(nums, track, used);
// 取消选择
track.removeLast();
used[i] = false;
}
}
```
我们这里稍微做了些变通,没有显式记录「选择列表」,而是通过 `nums``track` 推导出当前的选择列表:
我们这里稍微做了些变通,没有显式记录「选择列表」,而是通过 `used` 数组排除已经存在 `track` 中的元素,从而推导出当前的选择列表:
![](../pictures/backtracking/6.jpg)
![](https://labuladong.github.io/algo/images/backtracking/6.jpg)
至此,我们就通过全排列问题详解了回溯算法的底层原理。当然,这个算法解决全排列不是很高效,应为对链表使用 `contains` 方法需要 O(N) 的时间复杂度。有更好的方法通过交换元素达到目的,但是难理解一些,这里就不写了,有兴趣可以自行搜索一下。
至此,我们就通过全排列问题详解了回溯算法的底层原理。当然,这个算法解决全排列不是最高效的,你可能看到有的解法连 `used` 数组都不使用,通过交换元素达到目的。但是那种解法稍微难理解一些,这里就不写了,有兴趣可以自行搜索一下。
但是必须说明的是,不管怎么优化,都符合回溯框架,而且时间复杂度都不可能低于 O(N!),因为穷举整棵决策树是无法避免的。**这也是回溯算法的一个特点,不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高**
......@@ -168,20 +195,21 @@ void backtrack(int[] nums, LinkedList<Integer> track) {
### 二、N 皇后问题
这个问题很经典了,简单解释一下:给你一个 N×N 的棋盘,让你放置 N 个皇后,使得它们不能互相攻击。
力扣第 51 题「N 皇后」就是这个经典问题,简单解释一下:给你一个 `N×N` 的棋盘,让你放置 `N` 个皇后,使得它们不能互相攻击。
PS:皇后可以攻击同一行、同一列、左上左下右上右下四个方向的任意单位。
> PS:皇后可以攻击同一行、同一列、左上左下右上右下四个方向的任意单位。
这个问题本质上跟全排列问题差不多,决策树的每一层表示棋盘上的每一行;每个节点可以做出的选择是,在该行的任意一列放置一个皇后。
直接套用框架:
因为 C++ 代码对字符串的操作方便一些,所以这道题我用 C++ 来写解法,直接套用回溯算法框架:
```cpp
vector<vector<string>> res;
/* 输入棋盘边长 n,返回所有合法的放置 */
vector<vector<string>> solveNQueens(int n) {
// '.' 表示空,'Q' 表示皇后,初始化空棋盘。
// vector<string> 代表一个棋盘
// '.' 表示空,'Q' 表示皇后,初始化空棋盘
vector<string> board(n, string(n, '.'));
backtrack(board, 0);
return res;
......@@ -200,8 +228,9 @@ void backtrack(vector<string>& board, int row) {
int n = board[row].size();
for (int col = 0; col < n; col++) {
// 排除不合法选择
if (!isValid(board, row, col))
if (!isValid(board, row, col)) {
continue;
}
// 做选择
board[row][col] = 'Q';
// 进入下一行决策
......@@ -219,7 +248,7 @@ void backtrack(vector<string>& board, int row) {
bool isValid(vector<string>& board, int row, int col) {
int n = board.size();
// 检查列是否有皇后互相冲突
for (int i = 0; i < n; i++) {
for (int i = 0; i <= row; i++) {
if (board[i][col] == 'Q')
return false;
}
......@@ -239,17 +268,25 @@ bool isValid(vector<string>& board, int row, int col) {
}
```
> PS:肯定有读者问,按照 N 皇后问题的描述,我们为什么不检查左下角,右下角和下方的格子,只检查了左上角,右上角和上方的格子呢?
>
> 因为皇后是一行一行从上往下放的,所以左下方,右下方和正下方不用检查(还没放皇后);因为一行只会放一个皇后,所以每行不用检查。也就是最后只用检查上面,左上,右上三个方向。
函数 `backtrack` 依然像个在决策树上游走的指针,通过 `row``col` 就可以表示函数遍历到的位置,通过 `isValid` 函数可以将不符合条件的情况剪枝:
![](../pictures/backtracking/7.jpg)
![](https://labuladong.github.io/algo/images/backtracking/7.jpg)
如果直接给你这么一大段解法代码,可能是懵逼的。但是现在明白了回溯算法的框架套路,还有啥难理解的呢?无非是改改做选择的方式,排除不合法选择的方式而已,只要框架存于心,你面对的只剩下小问题了。
`N = 8` 时,就是八皇后问题,数学大佬高斯穷尽一生都没有数清楚八皇后问题到底有几种可能的放置方法,但是我们的算法只需要一秒就可以算出来所有可能的结果。
不过真的不怪高斯。这个问题的复杂度确实非常高,看看我们的决策树,虽然有 `isValid` 函数剪枝,但是最坏时间复杂度仍然是 O(N^(N+1)),而且无法优化。如果 `N = 10` 的时候,计算就已经很耗时了。
不过真的不怪高斯,这个问题的复杂度确实非常高,粗略估算一下:
**有的时候,我们并不想得到所有合法的答案,只想要一个答案,怎么办呢**?比如解数独的算法,找所有解法复杂度太高,只要找到一种解法就可以。
`N` 行棋盘中,第一行有 `N` 个位置可能可以放皇后,第二行有 `N - 1` 个位置,第三行有 `N - 2` 个位置,以此类推,再叠加每次放皇后之前 `isValid` 函数所需的 O(N) 复杂度,所以总的时间复杂度上界是 O(N! * N),而且没有什么明显的冗余计算可以优化效率。你可以试试 `N = 10` 的时候,计算就已经很耗时了。
当然,因为有 `isValid` 函数剪枝,并不会真的在每个位置都尝试放皇后,所以实际的执行效率会高一些。但正如前文 [算法时空复杂度分析实用指南](https://labuladong.github.io/article/fname.html?fname=时间复杂度) 所说,这个时间复杂度作为上界是没问题的。
**有的时候,如果我们并不想得到所有合法的答案,只想要一个答案,怎么办呢**?比如解数独的算法,找所有解法复杂度太高,只要找到一种解法就可以。
其实特别简单,只要稍微修改一下回溯算法的代码即可:
......@@ -294,19 +331,15 @@ def backtrack(...):
其实想想看,回溯算法和动态规划是不是有点像呢?我们在动态规划系列文章中多次强调,动态规划的三个需要明确的点就是「状态」「选择」和「base case」,是不是就对应着走过的「路径」,当前的「选择列表」和「结束条件」?
某种程度上说,动态规划的暴力求解阶段就是回溯算法。只是有的问题具有重叠子问题性质,可以用 dp table 或者备忘录优化,将递归树大幅剪枝,这就变成了动态规划。而今天的两个问题,都没有重叠子问题,也就是回溯算法问题了,复杂度非常高是不可避免的。
动态规划和回溯算法底层都把问题抽象成了树的结构,但这两种算法在思路上是完全不同的。在 [东哥带你刷二叉树(纲领篇)](https://labuladong.github.io/article/fname.html?fname=二叉树总结) 你将看到动态规划和回溯算法更深层次的区别和联系。
**_____________**
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**
![](https://labuladong.github.io/algo/images/souyisou2.png)
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**
<p align='center'>
<img src="../pictures/qrcode.jpg" width=200 >
</p>
======其他语言代码======
[46.全排列](https://leetcode-cn.com/problems/permutations)
......
# 字符串乘法
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号-@labuladong-000000.svg?style=flat-square&logo=WeChat"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>
![](../pictures/souyisou.png)
![](https://labuladong.github.io/algo/images/souyisou1.png)
**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
**通知:[数据结构精品课 V1.8](https://aep.h5.xeknow.com/s/1XJHEO) 持续更新中;[第十期刷题打卡挑战](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 最后一天报名。**
读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
[43.字符串相乘](https://leetcode-cn.com/problems/multiply-strings)
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [43. Multiply Strings](https://leetcode.com/problems/multiply-strings/) | [43. 字符串相乘](https://leetcode.cn/problems/multiply-strings/) | 🟠
**-----------**
对于比较小的数字,做运算可以直接使用编程语言提供的运算符,但是如果相乘的两个因数非常大,语言提供的数据类型可能就会溢出。一种替代方案就是,运算数以字符串的形式输入,然后模仿我们小学学习的乘法算术过程计算出结果,并且也用字符串表示。
![](../pictures/%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B9%98%E6%B3%95/title.png)
看下力扣第 43 题「字符串相乘」:
![](https://labuladong.github.io/algo/images/字符串乘法/title.png)
需要注意的是,`num1``num2` 可以非常长,所以不可以把他们直接转成整型然后运算,唯一的思路就是模仿我们手算乘法。
比如说我们手算 `123 × 45`,应该会这样计算:
![](../pictures/%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B9%98%E6%B3%95/1.jpg)
![](https://labuladong.github.io/algo/images/字符串乘法/1.jpg)
计算 `123 × 5`,再计算 `123 × 4`,最后错一位相加。这个流程恐怕小学生都可以熟练完成,但是你是否能**把这个运算过程进一步机械化**,写成一套算法指令让没有任何智商的计算机来执行呢?
......@@ -34,21 +43,21 @@
首先,我们这种手算方式还是太「高级」了,我们要再「低级」一点,`123 × 5``123 × 4` 的过程还可以进一步分解,最后再相加:
![](../pictures/%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B9%98%E6%B3%95/2.jpg)
![](https://labuladong.github.io/algo/images/字符串乘法/2.jpg)
现在 `123` 并不大,如果是个很大的数字的话,是无法直接计算乘积的。我们可以用一个数组在底下接收相加结果:
![](../pictures/%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B9%98%E6%B3%95/3.jpg)
![](https://labuladong.github.io/algo/images/字符串乘法/3.jpg)
整个计算过程大概是这样,**有两个指针 `i,j` 在 `num1` 和 `num2` 上游走,计算乘积,同时将乘积叠加到 `res` 的正确位置**
整个计算过程大概是这样,**有两个指针 `i,j` 在 `num1` 和 `num2` 上游走,计算乘积,同时将乘积叠加到 `res` 的正确位置**,如下 GIF 图所示
![](../pictures/%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B9%98%E6%B3%95/4.gif)
![](https://labuladong.github.io/algo/images/字符串乘法/4.gif)
现在还有一个关键问题,如何将乘积叠加到 `res` 的正确位置,或者说,如何通过 `i,j` 计算 `res` 的对应索引呢?
其实,细心观察之后就发现,**`num1[i]` 和 `num2[j]` 的乘积对应的就是 `res[i+j]` 和 `res[i+j+1]` 这两个位置**
![](../pictures/%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B9%98%E6%B3%95/6.jpg)
![](https://labuladong.github.io/algo/images/字符串乘法/6.jpg)
明白了这一点,就可以用代码模仿出这个计算过程了:
......@@ -91,13 +100,10 @@ string multiply(string num1, string num2) {
**_____________**
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**
![](https://labuladong.github.io/algo/images/souyisou2.png)
<p align='center'>
<img src="../pictures/qrcode.jpg" width=200 >
</p>
======其他语言代码======
[43.字符串相乘](https://leetcode-cn.com/problems/multiply-strings)
......
# 常用的位操作
# 常用的位运算技巧
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号-@labuladong-000000.svg?style=flat-square&logo=WeChat"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>
![](../pictures/souyisou.png)
![](https://labuladong.github.io/algo/images/souyisou1.png)
**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
**通知:[数据结构精品课 V1.8](https://aep.h5.xeknow.com/s/1XJHEO) 持续更新中;[第十期刷题打卡挑战](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 最后一天报名。**
读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
[191.位1的个数](https://leetcode-cn.com/problems/number-of-1-bits)
[231.2的幂](https://leetcode-cn.com/problems/power-of-two/)
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [136. Single Number](https://leetcode.com/problems/single-number/) | [136. 只出现一次的数字](https://leetcode.cn/problems/single-number/) | 🟢
| [191. Number of 1 Bits](https://leetcode.com/problems/number-of-1-bits/) | [191. 位1的个数](https://leetcode.cn/problems/number-of-1-bits/) | 🟢
| [231. Power of Two](https://leetcode.com/problems/power-of-two/) | [231. 2 的幂](https://leetcode.cn/problems/power-of-two/) | 🟢
| [268. Missing Number](https://leetcode.com/problems/missing-number/) | [268. 丢失的数字](https://leetcode.cn/problems/missing-number/) | 🟢
| - | [剑指 Offer 15. 二进制中1的个数](https://leetcode.cn/problems/er-jin-zhi-zhong-1de-ge-shu-lcof/) | 🟢
**-----------**
本文分两部分,第一部分列举几个有趣的位操作,第二部分讲解算法中常用的 `n & (n - 1)` 操作,顺便把用到这个技巧的算法题列出来讲解一下。因为位操作很简单,所以假设读者已经了解与、或、异或这三种基本操作。
本文分两部分,第一部分列举几个有趣的位操作,第二部分讲解算法题中常用的位运算操作。因为位操作很简单,所以假设读者已经了解与、或、异或这三种基本操作。
位操作(Bit Manipulation)可以玩出很多奇技淫巧,但是这些技巧大部分都过于晦涩,没必要深究,读者只要记住一些有用的操作即可。
......@@ -28,42 +37,42 @@
1. **利用或操作 `|` 和空格将英文字符转换为小写**
```c
```java
('a' | ' ') = 'a'
('A' | ' ') = 'a'
```
2. **利用与操作 `&` 和下划线将英文字符转换为大写**
```c
```java
('b' & '_') = 'B'
('B' & '_') = 'B'
```
3. **利用异或操作 `^` 和空格进行英文字符大小写互换**
```c
```java
('d' ^ ' ') = 'D'
('D' ^ ' ') = 'd'
```
以上操作能够产生奇特效果的原因在于 ASCII 编码。字符其实就是数字,恰巧这些字符对应的数字通过位运算就能得到正确的结果,有兴趣的读者可以查 ASCII 码表自己算算,本文就不展开讲了。
以上操作能够产生奇特效果的原因在于 ASCII 编码。ASCII 字符其实就是数字,恰巧这些字符对应的数字通过位运算就能得到正确的结果,有兴趣的读者可以查 ASCII 码表自己算算,本文就不展开讲了。
4. **判断两个数是否异号**
```cpp
```java
int x = -1, y = 2;
bool f = ((x ^ y) < 0); // true
boolean f = ((x ^ y) < 0); // true
int x = 3, y = 2;
bool f = ((x ^ y) < 0); // false
boolean f = ((x ^ y) < 0); // false
```
这个技巧还是很实用的,利用的是补码编码的符号位。如果不用位运算来判断是否异号,需要使用 if else 分支,还挺麻烦的。读者可能想利用乘积或者商来判断两个数是否异号,但是这种处理方式可能造成溢出,从而出现错误。
5. **不用临时变量交换两个数**
```c
```java
int a = 1, b = 2;
a ^= b;
b ^= a;
......@@ -73,7 +82,7 @@ a ^= b;
6. **加一**
```c
```java
int n = 1;
n = -~n;
// 现在 n = 2
......@@ -81,32 +90,34 @@ n = -~n;
7. **减一**
```c
```java
int n = 2;
n = ~-n;
// 现在 n = 1
```
PS:上面这三个操作就纯属装逼用的,没啥实际用处,大家了解了解乐呵一下就行。
> PS:上面这三个操作就纯属装逼用的,没啥实际用处,大家了解了解乐呵一下就行。
### 二、算法常用操作
### 二、`n & (n-1)` 的运用
`n&(n-1)` 这个操作是算法中常见的,作用是消除数字 `n` 的二进制表示中的最后一个 1
**`n & (n-1)` 这个操作是算法中常见的,作用是消除数字 `n` 的二进制表示中的最后一个 1**
看个图就很容易理解了:
![](../pictures/%E4%BD%8D%E6%93%8D%E4%BD%9C/1.png)
![](https://labuladong.github.io/algo/images/位操作/1.png)
其核心逻辑就是,`n - 1` 一定可以消除最后一个 1,同时把其后的 0 都变成 1,这样再和 `n` 做一次 `&` 运算,就可以仅仅把最后一个 1 变成 0 了。
1. **计算汉明权重(Hamming Weight)**
**1、计算汉明权重(Hamming Weight)**
![](../pictures/%E4%BD%8D%E6%93%8D%E4%BD%9C/title.png)
这是力扣第 191 题「位 1 的个数」:
就是让你返回 n 的二进制表示中有几个 1。因为 n & (n - 1) 可以消除最后一个 1,所以可以用一个循环不停地消除 1 同时计数,直到 n 变成 0 为止。
![](https://labuladong.github.io/algo/images/位操作/title.png)
```cpp
int hammingWeight(uint32_t n) {
就是让你返回 `n` 的二进制表示中有几个 1。因为 `n & (n - 1)` 可以消除最后一个 1,所以可以用一个循环不停地消除 1 同时计数,直到 `n` 变成 0 为止。
```java
int hammingWeight(int n) {
int res = 0;
while (n != 0) {
n = n & (n - 1);
......@@ -116,37 +127,43 @@ int hammingWeight(uint32_t n) {
}
```
2. **判断一个数是不是 2 的指数**
**2、判断一个数是不是 2 的指数**
力扣第 231 题「2 的幂」就是这个问题。
一个数如果是 2 的指数,那么它的二进制表示一定只含有一个 1:
```cpp
```java
2^0 = 1 = 0b0001
2^1 = 2 = 0b0010
2^2 = 4 = 0b0100
```
如果使用 `n&(n-1)` 的技巧就很简单了(注意运算符优先级,括号不可以省略):
如果使用 `n & (n-1)` 的技巧就很简单了(注意运算符优先级,括号不可以省略):
```cpp
bool isPowerOfTwo(int n) {
```java
boolean isPowerOfTwo(int n) {
if (n <= 0) return false;
return (n & (n - 1)) == 0;
}
```
**3、查找只出现一次的元素**
![](../pictures/位操作/title1.png)
### 三、`a ^ a = 0` 的运用
这里就可以运用异或运算的性质
异或运算的性质是需要我们牢记的
一个数和它本身做异或运算结果为 0,即 `a ^ a = 0`;一个数和 0 做异或运算的结果为它本身,即 `a ^ 0 = a`
**1、查找只出现一次的元素**
这是力扣第 136 题「只出现一次的数字」:
![](https://labuladong.github.io/algo/images/位操作/title1.png)
对于这道题目,我们只要把所有数字进行异或,成对儿的数字就会变成 0,落单的数字和 0 做异或还是它本身,所以最后异或的结果就是只出现一次的元素:
```cpp
int singleNumber(vector<int>& nums) {
```java
int singleNumber(int[] nums) {
int res = 0;
for (int n : nums) {
res ^= n;
......@@ -155,6 +172,76 @@ int singleNumber(vector<int>& nums) {
}
```
**2、寻找缺失的元素**
这是力扣第 268 题「丢失的数字」:
![](https://labuladong.github.io/algo/images/缺失元素/title.png)
给一个长度为 `n` 的数组,其索引应该在 `[0,n)`,但是现在你要装进去 `n + 1` 个元素 `[0,n]`,那么肯定有一个元素装不下嘛,请你找出这个缺失的元素。
这道题不难的,我们应该很容易想到,把这个数组排个序,然后遍历一遍,不就很容易找到缺失的那个元素了吗?
或者说,借助数据结构的特性,用一个 HashSet 把数组里出现的数字都储存下来,再遍历 `[0,n]` 之间的数字,去 HashSet 中查询,也可以很容易查出那个缺失的元素。
排序解法的时间复杂度是 O(NlogN),HashSet 的解法时间复杂度是 O(N),但是还需要 O(N) 的空间复杂度存储 HashSet。
这个问题其实还有一个特别简单的解法:等差数列求和公式。
题目的意思可以这样理解:现在有个等差数列 `0, 1, 2,..., n`,其中少了某一个数字,请你把它找出来。那这个数字不就是 `sum(0,1,..n) - sum(nums)` 嘛?
```java
int missingNumber(int[] nums) {
int n = nums.length;
// 虽然题目给的数据范围不大,但严谨起见,用 long 类型防止整型溢出
// 求和公式:(首项 + 末项) * 项数 / 2
long expect = (0 + n) * (n + 1) / 2;
long sum = 0;
for (int x : nums) {
sum += x;
}
return (int)(expect - sum);
}
```
不过,本文的主题是位运算,我们来讲讲如何利用位运算技巧来解决这道题。
再回顾一下异或运算的性质:一个数和它本身做异或运算结果为 0,一个数和 0 做异或运算还是它本身。
而且异或运算满足交换律和结合律,也就是说:
```java
2 ^ 3 ^ 2 = 3 ^ (2 ^ 2) = 3 ^ 0 = 3
```
而这道题索就可以通过这些性质巧妙算出缺失的那个元素,比如说 `nums = [0,3,1,4]`
![](https://labuladong.github.io/algo/images/缺失元素/1.jpg)
为了容易理解,我们假设先把索引补一位,然后让每个元素和自己相等的索引相对应:
![](https://labuladong.github.io/algo/images/缺失元素/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;
}
```
![](https://labuladong.github.io/algo/images/缺失元素/3.jpg)
由于异或运算满足交换律和结合律,所以总是能把成对儿的数字消去,留下缺失的那个元素。
以上便是一些有趣/常用的位操作。其实位操作的技巧很多,有一个叫做 Bit Twiddling Hacks 的外国网站收集了几乎所有位操作的黑科技玩法,感兴趣的读者可以查看:
......@@ -162,13 +249,11 @@ http://graphics.stanford.edu/~seander/bithacks.html#ReverseParallel
**_____________**
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**
![](https://labuladong.github.io/algo/images/souyisou2.png)
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**
<p align='center'>
<img src="../pictures/qrcode.jpg" width=200 >
</p>
======其他语言代码======
[191.位1的个数](https://leetcode-cn.com/problems/number-of-1-bits)
......
# 洗牌算法
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号-@labuladong-000000.svg?style=flat-square&logo=WeChat"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>
![](../pictures/souyisou.png)
![](https://labuladong.github.io/algo/images/souyisou1.png)
**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
**通知:[数据结构精品课 V1.8](https://aep.h5.xeknow.com/s/1XJHEO) 持续更新中;[第十期刷题打卡挑战](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 最后一天报名。**
读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
[384.打乱数组](https://leetcode-cn.com/problems/shuffle-an-array)
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [384. Shuffle an Array](https://leetcode.com/problems/shuffle-an-array/) | [384. 打乱数组](https://leetcode.cn/problems/shuffle-an-array/) | 🟠
**-----------**
......@@ -62,7 +67,7 @@ void shuffle(int[] arr) {
```
**分析洗牌算法正确性的准则:产生的结果必须有 n! 种可能,否则就是错误的。**这个很好解释,因为一个长度为 n 的数组的全排列就有 n! 种,也就是说打乱结果总共有 n! 种。算法必须能够反映这个事实,才是正确的。
**分析洗牌算法正确性的准则:产生的结果必须有 n! 种可能,否则就是错误的**这个很好解释,因为一个长度为 n 的数组的全排列就有 n! 种,也就是说打乱结果总共有 n! 种。算法必须能够反映这个事实,才是正确的。
我们先用这个准则分析一下**第一种写法**的正确性:
......@@ -81,15 +86,15 @@ void shuffle(int[] arr) {
for 循环第一轮迭代时,`i = 0``rand` 的取值范围是 `[0, 4]`,有 5 个可能的取值。
![第一次](../pictures/%E6%B4%97%E7%89%8C%E7%AE%97%E6%B3%95/1.png)
![](https://labuladong.github.io/algo/images/洗牌算法/1.png)
for 循环第二轮迭代时,`i = 1``rand` 的取值范围是 `[1, 4]`,有 4 个可能的取值。
![第二次](../pictures/%E6%B4%97%E7%89%8C%E7%AE%97%E6%B3%95/2.png)
![](https://labuladong.github.io/algo/images/洗牌算法/2.png)
后面以此类推,直到最后一次迭代,`i = 4``rand` 的取值范围是 `[4, 4]`,只有 1 个可能的取值。
![最后一次](../pictures/%E6%B4%97%E7%89%8C%E7%AE%97%E6%B3%95/3.png)
![](https://labuladong.github.io/algo/images/洗牌算法/3.png)
可以看到,整个过程产生的所有可能结果有 `n! = 5! = 5*4*3*2*1` 种,所以这个算法是正确的。
......@@ -128,13 +133,13 @@ void shuffle(int[] arr) {
### 二、蒙特卡罗方法验证正确性
洗牌算法,或者说随机乱置算法的**正确性衡量标准是:对于每种可能的结果出现的概率必须相等,也就是说要足够随机。**
洗牌算法,或者说随机乱置算法的**正确性衡量标准是:对于每种可能的结果出现的概率必须相等,也就是说要足够随机**
如果不用数学严格证明概率相等,可以用蒙特卡罗方法近似地估计出概率是否相等,结果是否足够随机。
记得高中有道数学题:往一个正方形里面随机打点,这个正方形里紧贴着一个圆,告诉你打点的总数和落在圆里的点的数量,让你计算圆周率。
![正方形](../pictures/%E6%B4%97%E7%89%8C%E7%AE%97%E6%B3%95/4.png)
![](https://labuladong.github.io/algo/images/洗牌算法/4.png)
这其实就是利用了蒙特卡罗方法:当打的点足够多的时候,点的数量就可以近似代表图形的面积。通过面积公式,由正方形和圆的面积比值是可以很容易推出圆周率的。当然打的点越多,算出的圆周率越准确,充分体现了大力出奇迹的真理。
......@@ -142,7 +147,7 @@ void shuffle(int[] arr) {
**第一种思路**,我们把数组 arr 的所有排列组合都列举出来,做成一个直方图(假设 arr = {1,2,3}):
![直方图](../pictures/%E6%B4%97%E7%89%8C%E7%AE%97%E6%B3%95/5.jpg)
![](https://labuladong.github.io/algo/images/洗牌算法/5.jpg)
每次进行洗牌算法后,就把得到的打乱结果对应的频数加一,重复进行 100 万次,如果每种结果出现的总次数差不多,那就说明每种结果出现的概率应该是相等的。写一下这个思路的伪代码:
......@@ -187,7 +192,7 @@ for (int feq : count)
print(feq / N + " "); // 频率
```
![直方图](../pictures/%E6%B4%97%E7%89%8C%E7%AE%97%E6%B3%95/6.png)
![](https://labuladong.github.io/algo/images/洗牌算法/6.png)
这种思路也是可行的,而且避免了阶乘级的空间复杂度,但是多了嵌套 for 循环,时间复杂度高一点。不过由于我们的测试数据量不会有多大,这些问题都可以忽略。
......@@ -201,13 +206,11 @@ for (int feq : count)
**_____________**
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**
![](https://labuladong.github.io/algo/images/souyisou2.png)
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**
<p align='center'>
<img src="../pictures/qrcode.jpg" width=200 >
</p>
======其他语言代码======
[384.打乱数组](https://leetcode-cn.com/problems/shuffle-an-array)
......
# 烧饼排序
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号-@labuladong-000000.svg?style=flat-square&logo=WeChat"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>
![](../pictures/souyisou.png)
![](https://labuladong.github.io/algo/images/souyisou1.png)
**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
**通知:[数据结构精品课 V1.8](https://aep.h5.xeknow.com/s/1XJHEO) 持续更新中;[第十期刷题打卡挑战](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 最后一天报名。**
读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
[969.煎饼排序](https://leetcode-cn.com/problems/pancake-sorting)
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [969. Pancake Sorting](https://leetcode.com/problems/pancake-sorting/) | [969. 煎饼排序](https://leetcode.cn/problems/pancake-sorting/) | 🟠
**-----------**
烧饼排序是个很有意思的实际问题:假设盘子上有 `n`**面积大小不一**的烧饼,你如何用一把锅铲进行若干次翻转,让这些烧饼的大小有序(小的在上,大的在下)?
力扣第 969 题「煎饼排序」是个很有意思的实际问题:假设盘子上有 `n`**面积大小不一**的烧饼,你如何用一把锅铲进行若干次翻转,让这些烧饼的大小有序(小的在上,大的在下)?
![](../pictures/pancakeSort/1.jpg)
![](https://labuladong.github.io/algo/images/pancakeSort/1.jpg)
设想一下用锅铲翻转一堆烧饼的情景,其实是有一点限制的,我们每次只能将最上面的若干块饼子翻转:
![](../pictures/pancakeSort/2.png)
![](https://labuladong.github.io/algo/images/pancakeSort/2.png)
我们的问题是,**如何使用算法得到一个翻转序列,使得烧饼堆变得有序**
首先,需要把这个问题抽象,用数组来表示烧饼堆:
![](../pictures/pancakeSort/title.png)
![](https://labuladong.github.io/algo/images/pancakeSort/title.png)
如何解决这个问题呢?其实类似上篇文章 [递归反转链表的一部分](https://labuladong.gitee.io/algo/),这也是需要**递归思想**的。
如何解决这个问题呢?其实类似上篇文章 [递归反转链表的一部分](https://labuladong.github.io/article/fname.html?fname=递归反转链表的一部分),这也是需要**递归思想**的。
### 一、思路分析
......@@ -45,11 +52,11 @@ void sort(int[] cakes, int n);
如果我们找到了前 `n` 个烧饼中最大的那个,然后设法将这个饼子翻转到最底下:
![](../pictures/pancakeSort/3.jpg)
![](https://labuladong.github.io/algo/images/pancakeSort/3.jpg)
那么,原问题的规模就可以减小,递归调用 `pancakeSort(A, n-1)` 即可:
![](../pictures/pancakeSort/4.jpg)
![](https://labuladong.github.io/algo/images/pancakeSort/4.jpg)
接下来,对于上面的这 `n - 1` 块饼,如何排序呢?还是先从中找到最大的一块饼,然后把这块饼放到底下,再递归调用 `pancakeSort(A, n-1-1)`……
......@@ -128,10 +135,12 @@ void reverse(int[] arr, int i, int j) {
显然,这个结果不是最优的(最短的),比如说一堆煎饼 `[3,2,4,1]`,我们的算法得到的翻转序列是 `[3,4,2,3,1,2]`,但是最快捷的翻转方法应该是 `[2,3,4]`
```
初始状态 :[3,2,4,1]
翻前 2 个:[2,3,4,1]
翻前 3 个:[4,3,2,1]
翻前 4 个:[1,2,3,4]
```
如果要求你的算法计算排序烧饼的**最短**操作序列,你该如何计算呢?或者说,解决这种求最优解法的问题,核心思路什么,一定需要使用什么算法技巧呢?
......@@ -139,13 +148,11 @@ void reverse(int[] arr, int i, int j) {
**_____________**
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**
![](https://labuladong.github.io/algo/images/souyisou2.png)
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**
<p align='center'>
<img src="../pictures/qrcode.jpg" width=200 >
</p>
======其他语言代码======
[969.煎饼排序](https://leetcode-cn.com/problems/pancake-sorting)
......
# 层层拆解,带你手写 LRU 算法
# LRU 缓存淘汰算法设计
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号-@labuladong-000000.svg?style=flat-square&logo=WeChat"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>
![](../pictures/souyisou.png)
![](https://labuladong.github.io/algo/images/souyisou1.png)
**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
**通知:[数据结构精品课 V1.8](https://aep.h5.xeknow.com/s/1XJHEO) 持续更新中;[第十期刷题打卡挑战](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 最后一天报名。**
读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
[146.LRU缓存机制](https://leetcode-cn.com/problems/lru-cache/)
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [146. LRU Cache](https://leetcode.com/problems/lru-cache/) | [146. LRU 缓存](https://leetcode.cn/problems/lru-cache/) | 🟠
| - | [剑指 Offer II 031. 最近最少使用缓存](https://leetcode.cn/problems/OrIXps/) | 🟠
**-----------**
LRU 算法就是一种缓存淘汰策略,原理不难,但是面试中写出没有 bug 的算法比较有技巧,需要对数据结构进行层层抽象和拆解,本文 labuladong 就给你写一手漂亮的代码。
LRU 算法就是一种缓存淘汰策略,原理不难,但是面试中写出没有 bug 的算法比较有技巧,需要对数据结构进行层层抽象和拆解,本文就带你写一手漂亮的代码。
计算机的缓存容量有限,如果缓存满了就要删除一些内容,给新内容腾位置。但问题是,删除哪些内容呢?我们肯定希望删掉哪些没什么用的缓存,而把有用的数据继续留在缓存里,方便之后继续使用。那么,什么样的数据,我们判定为「有用的」的数据呢?
......@@ -26,17 +36,17 @@ LRU 缓存淘汰算法就是一种常用策略。LRU 的全称是 Least Recently
举个简单的例子,安卓手机都可以把软件放到后台运行,比如我先后打开了「设置」「手机管家」「日历」,那么现在他们在后台排列的顺序是这样的:
![](../pictures/LRU%E7%AE%97%E6%B3%95/1.jpg)
![](https://labuladong.github.io/algo/images/LRU算法/1.jpg)
但是这时候如果我访问了一下「设置」界面,那么「设置」就会被提前到第一个,变成这样:
![](../pictures/LRU%E7%AE%97%E6%B3%95/2.jpg)
![](https://labuladong.github.io/algo/images/LRU算法/2.jpg)
假设我的手机只允许我同时开 3 个应用程序,现在已经满了。那么如果我新开了一个应用「时钟」,就必须关闭一个应用为「时钟」腾出一个位置,关那个呢?
按照 LRU 的策略,就关最底下的「手机管家」,因为那是最久未使用的,然后把新开的应用放到最上面:
![](../pictures/LRU%E7%AE%97%E6%B3%95/3.jpg)
![](https://labuladong.github.io/algo/images/LRU算法/3.jpg)
现在你应该理解 LRU(Least Recently Used)策略了。当然还有其他缓存淘汰策略,比如不要按访问的时序来淘汰,而是按访问频率(LFU 策略)来淘汰等等,各有应用场景。本文讲解 LRU 算法策略。
......@@ -48,7 +58,7 @@ LRU 缓存淘汰算法就是一种常用策略。LRU 的全称是 Least Recently
注意哦,`get``put` 方法必须都是 `O(1)` 的时间复杂度,我们举个具体例子来看看 LRU 算法怎么工作。
```cpp
```java
/* 缓存容量为 2 */
LRUCache cache = new LRUCache(2);
// 你可以把 cache 理解成一个队列
......@@ -97,7 +107,7 @@ cache.put(1, 4);
LRU 缓存算法的核心数据结构就是哈希链表,双向链表和哈希表的结合体。这个数据结构长这样:
![HashLinkedList](../pictures/LRU%E7%AE%97%E6%B3%95/4.jpg)
![](https://labuladong.github.io/algo/images/LRU算法/4.jpg)
借助这个结构,我们来逐一分析上面的 3 个条件:
......@@ -180,7 +190,7 @@ class DoubleList {
到这里就能回答刚才「为什么必须要用双向链表」的问题了,因为我们需要删除操作。删除一个节点不光要得到该节点本身的指针,也需要操作其前驱节点的指针,而双向链表才能支持直接查找前驱,保证操作的时间复杂度 O(1)。
**注意我们实现的双链表 API 只能从尾部插入,也就是说靠尾部的数据是最近使用的,靠头部的数据是最久使用的**
**注意我们实现的双链表 API 只能从尾部插入,也就是说靠尾部的数据是最近使用的,靠头部的数据是最久使用的**
有了双向链表的实现,我们只需要在 LRU 算法中把它和哈希表结合起来即可,先搭出代码框架:
......@@ -263,7 +273,7 @@ public int get(int key) {
`put` 方法稍微复杂一些,我们先来画个图搞清楚它的逻辑:
![](../pictures/LRU算法/put.jpg)
![](https://labuladong.github.io/algo/images/LRU算法/put.jpg)
这样我们可以轻松写出 `put` 方法的代码:
......@@ -332,17 +342,19 @@ class LRUCache {
}
```
至此,LRU 算法就没有什么神秘的了,**敬请期待下文:LFU 算法拆解与实现**
至此,LRU 算法就没有什么神秘的了。
接下来可阅读:
* [手把手带你实现 LFU 算法](https://labuladong.github.io/article/fname.html?fname=LFU)
**_____________**
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**
![](https://labuladong.github.io/algo/images/souyisou2.png)
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**
<p align='center'>
<img src="../pictures/qrcode.jpg" width=200 >
</p>
======其他语言代码======
[146.LRU缓存机制](https://leetcode-cn.com/problems/lru-cache/)
......
# 如何k个一组反转链表
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号-@labuladong-000000.svg?style=flat-square&logo=WeChat"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>
![](../pictures/souyisou.png)
![](https://labuladong.github.io/algo/images/souyisou1.png)
**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
**通知:[数据结构精品课 V1.8](https://aep.h5.xeknow.com/s/1XJHEO) 持续更新中;[第十期刷题打卡挑战](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 最后一天报名。**
读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
[25.K个一组翻转链表](https://leetcode-cn.com/problems/reverse-nodes-in-k-group)
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [25. Reverse Nodes in k-Group](https://leetcode.com/problems/reverse-nodes-in-k-group/) | [25. K 个一组翻转链表](https://leetcode.cn/problems/reverse-nodes-in-k-group/) | 🔴
**-----------**
之前的文章「递归反转链表的一部分」讲了如何递归地反转一部分链表,有读者就问如何迭代地反转链表,这篇文章解决的问题也需要反转链表的函数,我们不妨就用迭代方式来解决。
本文要解决「K 个一组反转链表」,不难理解:
本文要解决力扣第 25 题「K 个一组翻转链表」,题目不难理解:
![](../pictures/kgroup/title.png)
![](https://labuladong.github.io/algo/images/kgroup/title.png)
这个问题经常在面经中看到,而且 LeetCode 上难度是 Hard,它真的有那么难吗?
这个问题经常在面经中看到,而且力扣上难度是 Hard,它真的有那么难吗?
对于基本数据结构的算法问题其实都不难,只要结合特点一点点拆解分析,一般都没啥难点。下面我们就来拆解一下这个问题。
### 一、分析问题
首先,前文[学习数据结构的框架思维](https://labuladong.gitee.io/algo/)提到过,链表是一种兼具递归和迭代性质的数据结构,认真思考一下可以发现**这个问题具有递归性质**
首先,前文 [学习数据结构的框架思维](https://labuladong.github.io/article/fname.html?fname=学习数据结构和算法的高效方法) 提到过,链表是一种兼具递归和迭代性质的数据结构,认真思考一下可以发现**这个问题具有递归性质**
什么叫递归性质?直接上图理解,比如说我们对这个链表调用 `reverseKGroup(head, 2)`,即以 2 个节点为一组反转链表:
![](../pictures/kgroup/1.jpg)
![](https://labuladong.github.io/algo/images/kgroup/1.jpg)
如果我设法把前 2 个节点反转,那么后面的那些节点怎么处理?后面的这些节点也是一条链表,而且规模(长度)比原来这条链表小,这就叫**子问题**
![](../pictures/kgroup/2.jpg)
![](https://labuladong.github.io/algo/images/kgroup/2.jpg)
我们可以直接递归调用 `reverseKGroup(cur, 2)`,因为子问题和原问题的结构完全相同,这就是所谓的递归性质。
我们可以把原先的 `head` 指针移动到后面这一段链表的开头,然后继续递归调用 `reverseKGroup(head, 2)`,因为子问题(后面这部分链表)和原问题(整条链表)的结构完全相同,这就是所谓的递归性质。
发现了递归性质,就可以得到大致的算法流程:
**1、先反转以 `head` 开头的 `k` 个元素**
![](../pictures/kgroup/3.jpg)
![](https://labuladong.github.io/algo/images/kgroup/3.jpg)
**2、将第 `k + 1` 个元素作为 `head` 递归调用 `reverseKGroup` 函数**
![](../pictures/kgroup/4.jpg)
![](https://labuladong.github.io/algo/images/kgroup/4.jpg)
**3、将上述两个过程的结果连接起来**
![](../pictures/kgroup/5.jpg)
![](https://labuladong.github.io/algo/images/kgroup/5.jpg)
整体思路就是这样了,最后一点值得注意的是,递归函数都有个 base case,对于这个问题是什么呢?
......@@ -82,7 +89,9 @@ ListNode reverse(ListNode a) {
}
```
![](../pictures/kgroup/8.gif)
算法执行的过程如下 GIF 所示::
![](https://labuladong.github.io/algo/images/kgroup/8.gif)
这次使用迭代思路来实现的,借助动画理解应该很容易。
......@@ -130,29 +139,28 @@ ListNode reverseKGroup(ListNode head, int k) {
解释一下 `for` 循环之后的几句代码,注意 `reverse` 函数是反转区间 `[a, b)`,所以情形是这样的:
![](../pictures/kgroup/6.jpg)
![](https://labuladong.github.io/algo/images/kgroup/6.jpg)
递归部分就不展开了,整个函数递归完成之后就是这个结果,完全符合题意:
![](../pictures/kgroup/7.jpg)
![](https://labuladong.github.io/algo/images/kgroup/7.jpg)
### 三、最后说两句
从阅读量上看,基本数据结构相关的算法文章看的人都不多,我想说这是要吃亏的。
大家喜欢看动态规划相关的问题,可能因为面试很常见,但就我个人理解,很多算法思想都是源于数据结构的。我们公众号的成名之作之一,「学习数据结构的框架思维」就提过,什么动规、回溯、分治算法,其实都是树的遍历,树这种结构它不就是个多叉链表吗?你能处理基本数据结构的问题,解决一般的算法问题应该也不会太费事。
大家喜欢看动态规划相关的问题,可能因为面试很常见,但就我个人理解,很多算法思想都是源于数据结构的。我们公众号的成名之作之一,[学习数据结构的框架思维](https://labuladong.github.io/article/fname.html?fname=学习数据结构和算法的高效方法) 就提过,什么动规、回溯、分治算法,其实都是树的遍历,树这种结构它不就是个多叉链表吗?你能处理基本数据结构的问题,解决一般的算法问题应该也不会太费事。
那么如何分解问题、发现递归性质呢?这个只能多练习,也许后续可以专门写一篇文章来探讨一下,本文就到此为止吧,希望对大家有帮助!
> 最后打个广告,我亲自制作了一门 [数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO),以视频课为主,手把手带你实现常用的数据结构及相关算法,旨在帮助算法基础较为薄弱的读者深入理解常用数据结构的底层原理,在算法学习中少走弯路。
**_____________**
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**
![](https://labuladong.github.io/algo/images/souyisou2.png)
<p align='center'>
<img src="../pictures/qrcode.jpg" width=200 >
</p>
======其他语言代码======
......
# 一行代码就能解决的算法题
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号-@labuladong-000000.svg?style=flat-square&logo=WeChat"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>
![](../pictures/souyisou.png)
![](https://labuladong.github.io/algo/images/souyisou1.png)
**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
**通知:[数据结构精品课 V1.8](https://aep.h5.xeknow.com/s/1XJHEO) 持续更新中;[第十期刷题打卡挑战](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 最后一天报名。**
读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
[292.Nim游戏](https://leetcode-cn.com/problems/nim-game)
[877.石子游戏](https://leetcode-cn.com/problems/stone-game)
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
[319.灯泡开关](https://leetcode-cn.com/problems/bulb-switcher)
| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [292. Nim Game](https://leetcode.com/problems/nim-game/) | [292. Nim 游戏](https://leetcode.cn/problems/nim-game/) | 🟢
| [319. Bulb Switcher](https://leetcode.com/problems/bulb-switcher/) | [319. 灯泡开关](https://leetcode.cn/problems/bulb-switcher/) | 🟠
| [877. Stone Game](https://leetcode.com/problems/stone-game/) | [877. 石子游戏](https://leetcode.cn/problems/stone-game/) | 🟠
**-----------**
下文是我在 LeetCode 刷题过程中总结的三道有趣的「脑筋急转弯」题目,可以使用算法编程解决,但只要稍加思考,就能找到规律,直接想出答案。
下文是我在刷题过程中总结的三道有趣的「脑筋急转弯」题目,可以使用算法编程解决,但只要稍加思考,就能找到规律,直接想出答案。
### 一、Nim 游戏
游戏规则是这样的:你和你的朋友面前有一堆石子,你们轮流拿,一次至少拿一颗,最多拿三颗,谁拿走最后一颗石子谁获胜。
力扣第 292 题「Nim 游戏」给了这样一个游戏规则:
你和你的朋友面前有一堆石子,你们轮流拿,一次至少拿一颗,最多拿三颗,谁拿走最后一颗石子谁获胜。
假设你们都很聪明,由你第一个开始拿,请你写一个算法,输入一个正整数 n,返回你是否能赢(true 或 false)。
假设你们都很聪明,由你第一个开始拿,请你写一个算法,输入一个正整数 `n`,返回你是否能赢(true 或 false)。
比如现在有 4 颗石子,算法应该返回 false。因为无论你拿 1 颗 2 颗还是 3 颗,对方都能一次性拿完,拿走最后一颗石子,所以你一定会输。
......@@ -46,20 +53,21 @@
这样一直循环下去,我们发现只要踩到 4 的倍数,就落入了圈套,永远逃不出 4 的倍数,而且一定会输。所以这道题的解法非常简单:
```cpp
bool canWinNim(int n) {
```java
boolean canWinNim(int n) {
// 如果上来就踩到 4 的倍数,那就认输吧
// 否则,可以把对方控制在 4 的倍数,必胜
return n % 4 != 0;
}
```
### 二、石头游戏
游戏规则是这样的:你和你的朋友面前有一排石头堆,用一个数组 piles 表示,piles[i] 表示第 i 堆石子有多少个。你们轮流拿石头,一次拿一堆,但是只能拿走最左边或者最右边的石头堆。所有石头被拿完后,谁拥有的石头多,谁获胜。
力扣第 877 题「石子游戏」的规则是这样的:
你和你的朋友面前有一排石头堆,用一个数组 `piles` 表示,`piles[i]` 表示第 `i` 堆石子有多少个。你们轮流拿石头,一次拿一堆,但是只能拿走最左边或者最右边的石头堆。所有石头被拿完后,谁拥有的石头多,谁获胜。
**假设你们都很聪明**,由你第一个开始拿,请你写一个算法,输入一个数组 piles,返回你是否能赢(true 或 false)。
**假设你们都很聪明**,由你第一个开始拿,请你写一个算法,输入一个数组 `piles`,返回你是否能赢(true 或 false)。
注意,石头的堆的数量为偶数,所以你们两人拿走的堆数一定是相同的。石头的总数为奇数,也就是你们最后不可能拥有相同多的石头,一定有胜负之分。
......@@ -97,7 +105,9 @@ boolean stoneGame(int[] piles) {
### 三、电灯开关问题
这个问题是这样描述的:有 n 盏电灯,最开始时都是关着的。现在要进行 n 轮操作:
力扣第 319 题「灯泡开关」的规则是这样的:
`n` 盏电灯,最开始时都是关着的。现在要进行 `n` 轮操作:
第 1 轮操作是把每一盏电灯的开关按一下(全部打开)。
......@@ -105,9 +115,9 @@ boolean stoneGame(int[] piles) {
第 3 轮操作是把每三盏灯的开关按一下(就是按第 3,6,9... 盏灯的开关,有的被关闭,比如 3,有的被打开,比如 6)...
如此往复,直到第 n 轮,即只按一下第 n 盏灯的开关。
如此往复,直到第 `n` 轮,即只按一下第 `n` 盏灯的开关。
现在给你输入一个正整数 n 代表电灯的个数,问你经过 n 轮操作后,这些电灯有多少盏是亮的?
现在给你输入一个正整数 `n` 代表电灯的个数,问你经过 `n` 轮操作后,这些电灯有多少盏是亮的?
我们当然可以用一个布尔数组表示这些灯的开关情况,然后模拟这些操作过程,最后去数一下就能出结果。但是这样显得没有灵性,最好的解法是这样的:
......@@ -125,7 +135,7 @@ int bulbSwitch(int n) {
为什么第 1、2、3、6 轮会被按呢?因为 `6=1*6=2*3`。一般情况下,因子都是成对出现的,也就是说开关被按的次数一般是偶数次。但是有特殊情况,比如说总共有 16 盏灯,那么第 16 盏灯会被按几次?
`16=1*16=2*8=4*4`
`16 = 1*16 = 2*8 = 4*4`
其中因子 4 重复出现,所以第 16 盏灯会被按 5 次,奇数次。现在你应该理解这个问题为什么和平方根有关了吧?
......@@ -133,19 +143,15 @@ int bulbSwitch(int n) {
就假设现在总共有 16 盏灯,我们求 16 的平方根,等于 4,这就说明最后会有 4 盏灯亮着,它们分别是第 `1*1=1` 盏、第 `2*2=4` 盏、第 `3*3=9` 盏和第 `4*4=16` 盏。
就算有的 n 平方根结果是小数,强转成 int 型,也相当于一个最大整数上界,比这个上界小的所有整数,平方后的索引都是最后亮着的灯的索引。所以说我们直接把平方根转成整数,就是这个问题的答案。
就算有的 `n` 平方根结果是小数,强转成 int 型,也相当于一个最大整数上界,比这个上界小的所有整数,平方后的索引都是最后亮着的灯的索引。所以说我们直接把平方根转成整数,就是这个问题的答案。
**_____________**
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**
![](https://labuladong.github.io/algo/images/souyisou2.png)
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**
<p align='center'>
<img src="../pictures/qrcode.jpg" width=200 >
</p>
======其他语言代码======
[292.Nim游戏](https://leetcode-cn.com/problems/nim-game)
......
# 二分查找高效判定子序列
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号-@labuladong-000000.svg?style=flat-square&logo=WeChat"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>
![](../pictures/souyisou.png)
![](https://labuladong.github.io/algo/images/souyisou1.png)
**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
**通知:[数据结构精品课 V1.8](https://aep.h5.xeknow.com/s/1XJHEO) 持续更新中;[第十期刷题打卡挑战](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 最后一天报名。**
读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
[392.判断子序列](https://leetcode-cn.com/problems/is-subsequence)
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [392. Is Subsequence](https://leetcode.com/problems/is-subsequence/) | [392. 判断子序列](https://leetcode.cn/problems/is-subsequence/) | 🟢
| [792. Number of Matching Subsequences](https://leetcode.com/problems/number-of-matching-subsequences/) | [792. 匹配子序列的单词数](https://leetcode.cn/problems/number-of-matching-subsequences/) | 🟠
**-----------**
二分查找本身不难理解,难在巧妙地运用二分查找技巧。对于一个问题,你可能都很难想到它跟二分查找有关,比如前文 [最长递增子序列](https://labuladong.gitee.io/algo/) 就借助一个纸牌游戏衍生出二分查找解法。
二分查找本身不难理解,难在巧妙地运用二分查找技巧。
今天再讲一道巧用二分查找的算法问题:如何判定字符串 `s` 是否是字符串 `t` 的子序列(可以假定 `s` 长度比较小,且 `t` 的长度非常大)。举两个例子:
对于一个问题,你可能都很难想到它跟二分查找有关,比如前文 [最长递增子序列](https://labuladong.github.io/article/fname.html?fname=动态规划设计:最长递增子序列) 就借助一个纸牌游戏衍生出二分查找解法。
今天再讲一道巧用二分查找的算法问题,力扣第 392 题「判断子序列」:
s = "abc", t = "**a**h**b**gd**c**", return true.
请你判定字符串 `s` 是否是字符串 `t` 的子序列(可以假定 `s` 长度比较小,且 `t` 的长度非常大)。
举两个例子:
s = "abc", t = "**a**h**b**gd**c**", return true.
s = "axc", t = "ahbgdc", return false.
......@@ -34,20 +46,22 @@ s = "axc", t = "ahbgdc", return false.
首先,一个很简单的解法是这样的:
```cpp
bool isSubsequence(string s, string t) {
```java
boolean isSubsequence(String s, String t) {
int i = 0, j = 0;
while (i < s.size() && j < t.size()) {
if (s[i] == t[j]) i++;
while (i < s.length() && j < t.length()) {
if (s.charAt(i) == t.charAt(j)) {
i++;
}
j++;
}
return i == s.size();
return i == s.length();
}
```
其思路也非常简单,利用双指针 `i, j` 分别指向 `s, t`,一边前进一边匹配子序列:
![gif](../pictures/%E5%AD%90%E5%BA%8F%E5%88%97/1.gif)
![](https://labuladong.github.io/algo/images/子序列/1.gif)
读者也许会问,这不就是最优解法了吗,时间复杂度只需 O(N),N 为 `t` 的长度。
......@@ -79,21 +93,21 @@ for (int i = 0; i < n; i++) {
}
```
![](../pictures/%E5%AD%90%E5%BA%8F%E5%88%97/2.jpg)
![](https://labuladong.github.io/algo/images/子序列/2.jpg)
比如对于这个情况,匹配了 "ab",应该匹配 "c" 了:
![](../pictures/%E5%AD%90%E5%BA%8F%E5%88%97/1.jpg)
![](https://labuladong.github.io/algo/images/子序列/1.jpg)
按照之前的解法,我们需要 `j` 线性前进扫描字符 "c",但借助 `index` 中记录的信息,**可以二分搜索 `index[c]` 中比 j 大的那个索引**,在上图的例子中,就是在 `[0,2,6]` 中搜索比 4 大的那个索引:
![](../pictures/%E5%AD%90%E5%BA%8F%E5%88%97/3.jpg)
![](https://labuladong.github.io/algo/images/子序列/3.jpg)
这样就可以直接得到下一个 "c" 的索引。现在的问题就是,如何用二分查找计算那个恰好比 4 大的索引呢?答案是,寻找左侧边界的二分搜索就可以做到。
### 三、再谈二分查找
在前文 [二分查找详解](https://labuladong.gitee.io/algo/) 中,详解了如何正确写出三种二分查找算法的细节。二分查找返回目标值 `val` 的索引,对于搜索**左侧边界**的二分查找,有一个特殊性质:
在前文 [二分查找详解](https://labuladong.github.io/article/fname.html?fname=二分查找详解) 中,详解了如何正确写出三种二分查找算法的细节。二分查找返回目标值 `val` 的索引,对于搜索**左侧边界**的二分查找,有一个特殊性质:
**当 `val` 不存在时,得到的索引恰好是比 `val` 大的最小元素索引**
......@@ -101,23 +115,24 @@ for (int i = 0; i < n; i++) {
```java
// 查找左侧边界的二分查找
int left_bound(ArrayList<Integer> arr, int tar) {
int lo = 0, hi = arr.size();
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (tar > arr.get(mid)) {
lo = mid + 1;
int left_bound(ArrayList<Integer> arr, int target) {
int left = 0, right = arr.size();
while (left < right) {
int mid = left + (right - left) / 2;
if (target > arr.get(mid)) {
left = mid + 1;
} else {
hi = mid;
right = mid;
}
}
return lo;
if (left == arr.size()) {
return -1;
}
return left;
}
```
以上就是搜索左侧边界的二分查找,等会儿会用到,其中的细节可以参见前文《二分查找详解》,这里不再赘述。
### 四、代码实现
以上就是搜索左侧边界的二分查找,等会儿会用到,其中的细节可以参见前文 [二分查找详解](https://labuladong.github.io/article/fname.html?fname=二分查找详解),这里不再赘述。
这里以单个字符串 `s` 为例,对于多个字符串 `s`,可以把预处理部分抽出来。
......@@ -142,7 +157,7 @@ boolean isSubsequence(String s, String t) {
if (index[c] == null) return false;
int pos = left_bound(index[c], j);
// 二分搜索区间中没有找到字符 c
if (pos == index[c].size()) return false;
if (pos == -1) return false;
// 向前移动指针 j
j = index[c].get(pos) + 1;
}
......@@ -152,19 +167,76 @@ boolean isSubsequence(String s, String t) {
算法执行的过程是这样的:
![](../pictures/%E5%AD%90%E5%BA%8F%E5%88%97/2.gif)
![](https://labuladong.github.io/algo/images/子序列/2.gif)
可见借助二分查找,算法的效率是可以大幅提升的。
明白了这个思路,我们可以直接拿下力扣第 792 题「匹配子序列的单词数」:给你输入一个字符串列表 `words` 和一个字符串 `s`,问你 `words` 中有多少字符串是 `s` 的子序列。
函数签名如下:
```java
int numMatchingSubseq(String s, String[] words)
```
我们直接把上一道题的代码稍微改改即可完成这道题:
可见借助二分查找,算法的效率是可以大幅提升的。
```java
int numMatchingSubseq(String s, String[] words) {
// 对 s 进行预处理
// char -> 该 char 的索引列表
ArrayList<Integer>[] index = new ArrayList[256];
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (index[c] == null) {
index[c] = new ArrayList<>();
}
index[c].add(i);
}
int res = 0;
for (String word : words) {
// 字符串 word 上的指针
int i = 0;
// 串 s 上的指针
int j = 0;
// 借助 index 查找 word 中每个字符的索引
for (; i < word.length(); i++) {
char c = word.charAt(i);
// 整个 s 压根儿没有字符 c
if (index[c] == null) {
break;
}
int pos = left_bound(index[c], j);
// 二分搜索区间中没有找到字符 c
if (pos == -1) {
break;
}
// 向前移动指针 j
j = index[c].get(pos) + 1;
}
// 如果 word 完成匹配,则是子序列
if (i == word.length()) {
res++;
}
}
return res;
}
// 查找左侧边界的二分查找
int left_bound(ArrayList<Integer> arr, int target) {
// 见上文
}
```
**_____________**
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**
![](https://labuladong.github.io/algo/images/souyisou2.png)
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**
<p align='center'>
<img src="../pictures/qrcode.jpg" width=200 >
</p>
======其他语言代码======
[392.判断子序列](https://leetcode-cn.com/problems/is-subsequence)
......
# 如何调度考生的座位
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号-@labuladong-000000.svg?style=flat-square&logo=WeChat"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>
![](../pictures/souyisou.png)
![](https://labuladong.github.io/algo/images/souyisou1.png)
**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
**通知:[数据结构精品课 V1.8](https://aep.h5.xeknow.com/s/1XJHEO) 持续更新中;[第十期刷题打卡挑战](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 最后一天报名。**
读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
[855.考场就座](https://leetcode-cn.com/problems/exam-room)
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [855. Exam Room](https://leetcode.com/problems/exam-room/) | [855. 考场就座](https://leetcode.cn/problems/exam-room/) | 🟠
**-----------**
这是 LeetCode 第 855 题,有趣且具有一定技巧性。这种题目并不像动态规划这类算法拼智商,而是看你对常用数据结构的理解和写代码的水平,个人认为值得重视和学习。
本文讲一讲力扣第 855 题「考场就座」,有趣且具有一定技巧性。这种题目并不像动态规划这类算法拼智商,而是看你对常用数据结构的理解和写代码的水平,个人认为值得重视和学习。
另外说句题外话,很多读者都问,算法框架是如何总结出来的,其实框架反而是慢慢从细节里抠出来的。希望大家看了我们的文章之后,最好能抽时间把相关的问题亲自做一做,纸上得来终觉浅,绝知此事要躬行嘛。
......@@ -128,7 +135,7 @@ private int distance(int[] intv) {
「虚拟线段」其实就是为了将所有座位表示为一个线段:
![](../pictures/座位调度/1.jpg)
![](https://labuladong.github.io/algo/images/座位调度/1.jpg)
有了上述铺垫,主要 API `seat``leave` 就可以写了:
......@@ -167,7 +174,7 @@ public void leave(int p) {
}
```
![三种情况](../pictures/座位调度/2.jpg)
![](https://labuladong.github.io/algo/images/座位调度/2.jpg)
至此,算法就基本实现了,代码虽多,但思路很简单:找最长的线段,从中间分隔成两段,中点就是 `seat()` 的返回值;找 `p` 的左右线段,合并成一个线段,这就是 `leave(p)` 的逻辑。
......@@ -175,11 +182,11 @@ public void leave(int p) {
但是,题目要求多个选择时选择索引最小的那个座位,我们刚才忽略了这个问题。比如下面这种情况会出错:
![](../pictures/座位调度/3.jpg)
![](https://labuladong.github.io/algo/images/座位调度/3.jpg)
现在有序集合里有线段 `[0,4]``[4,9]`,那么最长线段 `longest` 就是后者,按照 `seat` 的逻辑,就会分割 `[4,9]`,也就是返回座位 6。但正确答案应该是座位 2,因为 2 和 6 都满足最大化相邻考生距离的条件,二者应该取较小的。
![](../pictures/座位调度/4.jpg)
![](https://labuladong.github.io/algo/images/座位调度/4.jpg)
**遇到题目的这种要求,解决方式就是修改有序数据结构的排序方式**。具体到这个问题,就是修改 `TreeMap` 的比较函数逻辑:
......@@ -207,7 +214,7 @@ private int distance(int[] intv) {
}
```
![](../pictures/座位调度/5.jpg)
![](https://labuladong.github.io/algo/images/座位调度/5.jpg)
这样,`[0,4]``[4,9]``distance` 值就相等了,算法会比较二者的索引,取较小的线段进行分割。到这里,这道算法题目算是完全解决了。
......@@ -217,19 +224,17 @@ private int distance(int[] intv) {
处理动态问题一般都会用到有序数据结构,比如平衡二叉搜索树和二叉堆,二者的时间复杂度差不多,但前者支持的操作更多。
既然平衡二叉搜索树这么好用,还用二叉堆干嘛呢?因为二叉堆底层就是数组,实现简单啊,详见旧文「二叉堆详解」。你实现个红黑树试试?操作复杂,而且消耗的空间相对来说会多一些。具体问题,还是要选择恰当的数据结构来解决。
既然平衡二叉搜索树这么好用,还用二叉堆干嘛呢?因为二叉堆底层就是数组,实现简单啊,详见前文 [二叉堆详解](https://labuladong.github.io/article/fname.html?fname=二叉堆详解实现优先级队列)。你实现个红黑树试试?操作复杂,而且消耗的空间相对来说会多一些。具体问题,还是要选择恰当的数据结构来解决。
希望本文对大家有帮助。
**_____________**
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**
![](https://labuladong.github.io/algo/images/souyisou2.png)
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**
<p align='center'>
<img src="../pictures/qrcode.jpg" width=200 >
</p>
======其他语言代码======
[855.考场就座](https://leetcode-cn.com/problems/exam-room)
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册