7867 字
21 分钟
算法算法题
算法总结-栈与队列

总结汇总一下栈与队列题型。

栈与队列的核心理解#

什么是栈#

栈是一种 后进先出(LIFO, Last In First Out)的数据结构。最后放进去的元素,最先被拿出来。

最常见的几个动作是:

  • push:压栈
  • pop:弹栈
  • top / peek:查看栈顶

栈的直觉非常像一摞盘子。你只能从最上面继续放,也只能从最上面拿。

刷题里,栈最常用于解决下面几类问题:

  • 配对与匹配,比如括号匹配
  • 撤销、回退、消消乐
  • 表达式求值
  • 单调栈维护"下一个更大/更小元素"

什么是队列#

队列是一种 先进先出(FIFO, First In First Out)的数据结构。最先进入队列的元素,会最先离开。

常见动作是:

  • push / offer:入队
  • pop / poll:出队
  • front / peek:查看队头

队列非常像排队买饭。先来的人先处理,后来的只能排在后面。

刷题里,队列最常用于:

  • 按顺序处理元素
  • 层序遍历
  • BFS 最短步数问题
  • 滑动窗口
  • 单调队列维护窗口最值

栈和队列的区别#

最核心的区别其实只有一句话:

  • 栈:后进先出
  • 队列:先进先出

也正因为这个区别,它们擅长处理的问题也不同:

  • 栈更擅长"最近的、嵌套的、需要回退的"关系
  • 队列更擅长"按到达顺序逐个处理"的关系

所以很多题目不是在考你会不会写 appendpop,而是在考你:

这个问题需要的是最近进入的元素,还是最早进入的元素?

什么情况下想到栈#

看到下面这些特征,优先想栈:

  • 括号匹配、标签匹配、路径回退
  • 撤销、恢复、消消乐、相邻抵消
  • 表达式求值
  • 需要维护"最近一个比当前大/小"的元素
  • 需要在扫描过程中保存"还没处理完的历史信息"

一句话记忆:

只要题目和“最近的未完成状态”有关,就很容易想到栈。

什么情况下想到队列#

看到下面这些味道,优先想队列:

  • 层序遍历
  • 按轮次推进
  • 最少步数、最短操作次数
  • 数据流按顺序进入,旧元素会自然淘汰
  • 滑动窗口

一句话记忆:

只要题目强调“顺序推进、一层一层扩散、最先进入最先处理”,就很容易想到队列。

Python 中的栈、队列、双端队列、优先队列#

Python 里最常用的几个容器如下:

  • 普通栈:直接用 list
  • 普通队列 / 双端队列:用 collections.deque
  • 优先队列:用 heapq

其中:

  • list.append() + list.pop() 就可以当栈用
  • deque.append() / deque.popleft() 很适合写队列
  • heapq 默认是小根堆,如果想模拟大根堆,通常存相反数

刷题时其实不用纠结"我是不是在手写一个正式数据结构",更重要的是:

我现在需要维护哪种顺序?

基础栈题:后进先出#

括号匹配#

LC20 - 有效的括号#

这是最基础的栈应用题,我们只需要用一个栈来决定就行。首先用字典记录右括号对应的左括号,右括号遇到左括号必须弹出,不然就是无效的,最后栈非空也是无效。

Python3 点击展开代码
12 lines 展开代码

如果只有一种括号,那么有另外一种思路,用左括号数量去判断有效性。如果要让字符串有效,那么必须每个右括号 ) 的左边必须有一个左括号 ( 和它匹配。所以我们维持一个变量left,遇到左括号+,遇到右括号-,这样中间是否会右括号太多以及最后只要判断是不是正好抵消就行。(其实与栈思路一样,只不过只用了一个变量)。本题不行,因为有三种括号,所以要加大统计量。

LC921 - 使括号有效的最少添加#

只有满足下面几点之一,括号字符串才是有效的:

  • 它是一个空字符串,或者

  • 它可以被写成 AB (A 与 B 连接), 其中 A 和 B 都是有效字符串,或者

  • 它可以被写作 (A),其中 A 是有效字符串。 给定一个括号字符串 s ,在每一次操作中,你都可以在字符串的任何位置插入一个括号

  • 例如,如果 s = "()))" ,你可以插入一个开始括号为 "(()))" 或结束括号为 "())))" 。 返回 为使结果字符串 s 有效而必须添加的最少括号数。

这一题可以回到需求的解法了,我们维持插入次数res、右括号的需求量need。如果是左括号,need+1即可;如果是右括号,我们不仅要-1,还要判断-1是不是变成刚需左括号了,如果是这样必须立刻补左括号,res+1。这样,我们最终返回res+need就是需要插入的总次数。

总而言之,我们在插入过程中维持有效性。

Python3 点击展开代码
12 lines 展开代码

LC1541 - 平衡括号字符串的最少插入次数#

本题和上一题的区别在于,一个left要对应两个右括号了,简单改一下就能出答案。

Python3 点击展开代码
19 lines 展开代码

LC1249 - 移除无效的括号#

这题要求从一个字符串中删除最小数量的括号,让括号有效。字符串中有一些无关小写字母。按照LC20相同的代码,其实我们就可以直接得到需要删除的个数,但是本题要求的是删除后的字符串,需要更多信息。但是其实改动也不多。

Python3 点击展开代码
24 lines 展开代码

这一题,当然也可以用栈存待会要删除的下标(只存还没匹配的 ( 的下标),来之后构造答案,思路很自然:

Python3 点击展开代码
17 lines 展开代码

综上,结构匹配型我们就用栈来做,数量欠账类的用res/need,构造答案则是两者皆可,用栈来存储更优雅。

表达式与字符串处理#

LC150 - 逆波兰表达式求值#

逆波兰式式经典应用栈求数值的题目。

Python3 点击展开代码
20 lines 展开代码

LC71 - 简化路径#

我们通过curr_name来判断目前两个/直接夹的东西,并采用对应的栈操作。

Python3 点击展开代码
20 lines 展开代码

LC227 - 基本计算器 II#

这一题,其实是中缀表达式求值,上一题的逆波兰表达式则是后缀表达式。后缀表达式可以轻松用栈解决,中缀则是可以通过栈来转为后缀。思路如下:

  1. 遇到操作数直接加入后缀表达式
  2. 遇到左括号直接入栈,然后遇到右括号依次弹出栈内运算符并加入后缀表达式,直到弹出 ( 为止。
  3. 遇到运算符,则比较栈顶的运算符的优先级,如果比栈顶优先级高/栈顶为 ( /栈为空,则直接加入表达式;否则将栈顶弹出加入表达式,然后重新判断。
  4. 重复直到遍历完毕,将栈所有元素弹出,加入后缀表达式

不过对于本题来说,并不存在括号,我们可以这样来处理:遇到加减立刻将数字(取反)压栈,遇到乘除则立刻和栈顶计算:

Python3 点击展开代码
24 lines 展开代码

为什么可以这么做?这是因为,我们用栈调整了运算顺序。如果是优先级较低的+-,我们先记账,留到最后一起加减;如果是乘除,我们就立刻运算。

LC224 - 基本计算器#

这一题属于有括号,但只有+-。因此,也是有简化的方式:我们不用处理优先级,只需要看看这一层算到了多少、这一层整体前面带的符号是什么。

  • 用res记录当前层结果
  • sign记录当前数字前的符号
  • num组装多位数
  • 遇到 ( 将外层res和sign压栈,进入新的一层
  • 遇到 ) 先结算当前层,再和外层合并
Python3 点击展开代码
29 lines 展开代码

LC772 - 基本计算器 III#

这一题,不仅有括号,且有+-*/,没有什么比较好的方法遍历一次解决了。一般而言,我们可以用栈按照前面的技巧转为后缀表达式,然后用栈计算答案。但是,这一题更好的写法是"递归+栈",也就是每一层递归只处理一层的表达式,遇到 ( 就递归进去,拿子表达式的结果当成一个普通数字 num 来继续算。

所以其实这一题可以根据 基本计算器 II 加一个递归入口简单改编出来。

Python3 点击展开代码
27 lines 展开代码

辅助栈与模拟栈#

LC155 - 最小栈#

最小栈是一种支持push、pop、top操作,并能在常数时间内检索到最小元素的栈,一般的解决思路是用两个栈来实现,一个栈stack来保存元素,然后用另一个栈min_stack[i] 表示主栈 stack[0..i] 这一层时的最小值:

Python3 点击展开代码
27 lines 展开代码

LC232 - 用栈实现队列#

又是数据设计题,我们可以用两个栈来模拟队列,其中一个是常规栈,另一个用于出栈倒腾顺序:

Python3 点击展开代码
25 lines 展开代码

LC225 - 用队列实现栈#

用两个队列来实现栈,思路如下:

  • push(x):直接进主队列
  • pop():把前 n-1 个元素转移到辅助队列,最后剩下的那个就是栈顶,弹出它
  • 操作结束后交换 q1 和 q2,让 q1 继续做主队列
Python3 点击展开代码
31 lines 展开代码

基础队列题:先进先出#

普通队列#

LC933 - 最近的请求次数#

本题只记录和目前时间差3000以内的数目,所以我们可以直接用一个队列维护,如果队头和入队元素差距超过3000,直接弹出:

Python3 点击展开代码
9 lines 展开代码

LC649 - Dota2 参议院#

本题是一个轮流禁言题,也就是循环回合 + 先手淘汰。按照原始顺序流动,每个 R 可以禁言一个 D,每个 D 可以禁言一个 R,直到最后只剩同一阵容胜利。

这一题最好的解法是用两个队列存储还活着的议员下标,然后每次队头下标小的先行动,把对方队头ban掉,然后把自己的下标加上n放到队尾(等下一轮),被ban掉的出队就不会再回来了。

Python3 点击展开代码
23 lines 展开代码

双端队列#

LC641 - 设计循环双端队列#

这一题在 408 里也很常见。我们需要维持一个 front 和一个 rear,把数组想象成一个环,然后对这个环做入队、出队和取模移动。

Python3 点击展开代码
49 lines 展开代码

层序遍历与 BFS#

队列常被用来实现层序遍历,所以这一组题也很适合放在这里一起理解。

LC102 - 二叉树的层序遍历#

经典层序遍历,需要达到肌肉级别记忆。

Python3 点击展开代码
25 lines 展开代码

LC107 - 二叉树的层序遍历 II#

自底向上的层序遍历,和上一题相比只是 ans 改成从左边添加而已。

Python3 点击展开代码
21 lines 展开代码

LC103 - 二叉树的锯齿形层序遍历#

我们可以根据ans的长度得到奇数层偶数层,从而决定这个level是append还是appendleft。

Python3 点击展开代码
34 lines 展开代码

LC199 - 二叉树的右视图#

右视图其实就是每层的最后一个节点,所以在普通层序遍历的基础上,取每个 level[-1] 即可。这题和前面几题的差别不在数据结构,而在"每层要收集哪一个元素"。

图论中的队列#

LC994 - 腐烂的橘子#

本题用队列来存储腐烂橘子的坐标,弹出的时候让周围的新鲜橘子入队,每层扩散一次,时间 +1。当队列为空时遍历结束,这时候如果还有新鲜橘子就不可能全部腐烂,返回 -1,否则返回分钟数。

Python3 点击展开代码
32 lines 展开代码

LC752 - 打开转盘锁#

这一题的目标是最少步数,所以应该用BFS来做,因为BFS是天然的求"无权路最短图"的方法。如果用DFS,会一路走到底,然后要保证路径最短,还要将所有可能路径搜完,再取最小值,复杂度很差,且存在大量重复状态。

我们用bfs的次数就可以统计进行的步数,所以这一题只要把握好状态更新入队即可。

Python3 点击展开代码
36 lines 展开代码

LC773 - 滑动谜题#

依旧状态更新,依旧最短次数。我们直接一个广搜,用队列存储更新状态。可是,如果每次队列都存储一个二维数组,负担太大了,而且由于我们已经知道是2x3的板子了,可以先预处理好可以相邻下标。

Python3 点击展开代码
50 lines 展开代码

单调栈:维护下一个更大或更小#

单调栈的核心思想#

单调栈本质上还是栈,只不过我们在压栈时额外维护了一个"单调性":

  • 要么从栈底到栈顶单调递增
  • 要么从栈底到栈顶单调递减

它最擅长解决的问题是:

  • 下一个更大元素
  • 下一个更小元素
  • 上一个更大 / 更小元素
  • 左右第一个打破单调性的位置
  • 区间贡献问题

很多人第一次学单调栈,会觉得它像魔法。其实它做的事情非常朴素:

新元素一来,就把那些已经不可能成为答案的旧元素弹掉。

所以单调栈的关键不在"背模板",而在想清楚:

  • 栈里到底存的是值还是下标
  • 当前元素来了之后,谁已经失去资格了
  • 弹栈的那一刻,我能不能顺便结算答案

下一个更大元素#

LC496 - 下一个更大元素 I#

要找下一个更大元素,单调栈就是非常自然的解法。我们可以用哈希表记录每个元素下一个更大元素。

Python3 点击展开代码
10 lines 展开代码

LC503 - 下一个更大元素 II#

这一题要求 nums 中每个元素的下一个更大元素,但是本题数组是循环数组。这一题的解法实际上只要做两个处理:

  • 让循环范围扩大确保每个元素都判断一次是否是下一个元素
  • 然后让下标定位时取余即可
Python3 点击展开代码
12 lines 展开代码

LC739 - 每日温度#

一道很规整的单调栈题目,存下标,算距离存入。

Python3 点击展开代码
10 lines 展开代码

找左右边界#

LC84 - 柱状图中最大的矩形#

本质上,也是在寻找左右两侧第一个比当前矩形矮的,然后这时候就可以得出当前矩形能贡献出来的最大矩形。

Python3 点击展开代码
12 lines 展开代码

LC85 - 最大矩形#

这一题,实际上是枚举每一行作为底边,来做多个一维柱状图最大矩形问题。

Python3 点击展开代码
35 lines 展开代码

LC901 - 股票价格跨度#

设计一个算法收集某些股票的每日报价,并返回该股票当日价格的 跨度 。当日股票价格的 跨度 被定义为股票价格小于或等于今天价格的最大连续日数(从今天开始往回数,包括今天)。

这一题和普通"找下一个更大元素"的题有一点不一样。它不是一次性给完整数组,而是数据流在线输入,所以不能等全部输入完再统一求。

思路是维护一个单调递减栈,栈里存 (price, span)。如果当前价格比栈顶大,那么栈顶那一整段跨度都能被当前价格吞掉,于是我们把它们的跨度累加起来。这就是这一题看起来"多了一个累加量"的原因。

Python3 点击展开代码
13 lines 展开代码

单调栈变形#

LC853 - 车队#

这一题属于"排序 + 单调栈"的变形。把车按位置从左到右排序后,每辆车都有一个到终点的时间 time = (target - pos) / speed。如果后车到终点所需时间小于等于前车,那么它最终一定会追上前车,合并成同一个车队。

所以我们只需要维护一个单调的到达时间栈,能合并的就弹掉,最后栈里剩下的就是车队数量。

Python3 点击展开代码
9 lines 展开代码

区间贡献问题#

LC907 - 子数组的最小值之和#

这一题如果直接暴力二重循环弄出所有子数组,会超时。所以必须要转化思路,核心思想是算每个 arr[i] 作为最小值时,能贡献多少个子数组。

实际上,我们依旧要回到单调栈的原本思想,我们要看 arr[i] 左边第一个比它小的位置、右边第一个小于等于它的位置在哪。left[i] = 从 i 往左,最多能选多少个起点;right[i] = 从 i 往右,最多能选多少个终点;那么,arr[i] 的贡献 = arr[i] * left[i] * right[i]。

比如,[3,1,2,4],3左边没有更小的,所以左侧可选一个起点;右侧第一个小于等于它的是1,所以只能是[3]。而1左侧没有比它更小的,起点可以选3或者1,右侧是没有小于等于它的,可以选1、2、4,一共贡献 123 = 6。

Python3 点击展开代码
28 lines 展开代码

LC2104 - 子数组范围和#

返回所有连续子数组最小元素和最大元素差值,在上一题中,我们已经求过了最小值贡献,改一下不等于号方向就可以求最大值贡献了。然后,用最大值贡献-最小值贡献,就是本题的答案。

Python3 点击展开代码
42 lines 展开代码

本题其实可以细细品味,包含着单调栈的核心思想。

单调栈模板#

最常见的三个模板其实可以这样记:

Python3 点击展开代码
7 lines 展开代码
Python3 点击展开代码
7 lines 展开代码
Python3 点击展开代码
10 lines 展开代码

单调队列:维护滑动窗口最值#

单调队列的核心思想#

单调队列本质上是 deque + 维护单调性

和单调栈相比,它最大的不同在于:单调队列往往服务于滑动窗口。所以除了维护单调性,还要解决一个额外问题:

队头这个元素过期了吗?

于是单调队列的经典操作一般有两步:

  1. 新元素进来时,从队尾弹出所有"不如它"的元素
  2. 窗口左端移动时,判断队头是否已经滑出窗口

所以单调队列最擅长的就是:

  • 滑动窗口最大值 / 最小值
  • 窗口合法性判定
  • 前缀和 + 最优左端点筛选

滑动窗口最值#

LC239 - 滑动窗口最大值#

这是单调队列最经典的使用题。我们用deque,保持队头始终是当前窗口的最大值,然后每次验证队头是否过期即可。

Python3 点击展开代码
19 lines 展开代码

前缀和 + 单调队列#

LC862 - 和至少为 K 的最短子数组#

给你一个整数数组 nums 和一个整数 k ,找出 nums 中和至少为 k 的 最短非空子数组 ,并返回该子数组的长度。如果不存在这样的 子数组 ,返回 -1 。(子数组连续)

我们知道,要想求任意子数组的和,用前缀和就可以解决。这题不能使用滑动窗口,因为 LC862 里可能有负数。有负数时,窗口右扩后,和不一定变大;左缩后,和也不一定变小,所以普通双指针失效。

构造好前缀和之后,用单调队列维护一批最有希望成为左端点的前缀和下标。为了让和尽量大,我们让最小的前缀和始终保持在队顶,然后遍历的时候每次和当前 pre[i] 做差得到和,大于k之后,将ans更新为较小的坐标长度。

Python3 点击展开代码
16 lines 展开代码

这一题代码虽然很短,但思路转化并不直观,所以一定要把"前缀和 + 单调队列"的套路记住。

LC1438 - 绝对差不超过限制的最长连续子数组#

依旧是滑动窗口最值,这次既要最大又要最小,那么好,用两个队列就可以满足。

这一题包含比较通用的滑动窗口最值求解,请仔细阅读:

Python3 点击展开代码
26 lines 展开代码

单调队列模板#

单调队列最常见的模板其实就是下面这版:

Python3 点击展开代码
14 lines 展开代码

如果同时需要维护最大值和最小值,就开两个队列:

  • 一个单调递减,维护最大值
  • 一个单调递增,维护最小值

而如果题目是 LC862 这种"前缀和 + 最短子数组",那么队列里存的就不是原数组元素,而是前缀和下标。这一点要单独记。

优先队列:动态维护最值#

优先队列的核心思想#

优先队列的题,核心味道其实非常明显:

候选集会动态变化,但每一步只关心其中的一个最值。

这时就很容易想到堆。

常见信号包括:

  • 一边扫描,一边动态加入候选
  • 每次只需要当前最大 / 当前最小
  • Top K
  • 多路归并
  • 数据流维护第 k 大 / 中位数

在 Python 里,刷题时通常直接用 heapq

  • 默认是小根堆
  • 想要大根堆,就存相反数

Top K 问题#

LC215 - 数组中的第 K 个最大元素#

Top K问题,可以通过快速选择算法来解决。我们这里先复习一下优化重复元素性能的三路快选。

Python3 点击展开代码
39 lines 展开代码

然后,我们就进入这一题的优先队列写法。一般优先队列题优先用heapq即可,堆就是优先队列的常见实现方式。我们通过取反模拟大根堆,然后弹出k-1个之后得到第k个最大元素。

Python3 点击展开代码
10 lines 展开代码

另一种做法是固定大小的小根堆,这样会一直保留最小的k个,如果新元素比堆顶大,就换堆顶,到最后堆顶就会变成最大的k个中的最小的,也就是第k个最大元素。

Python3 点击展开代码
9 lines 展开代码

LC347 - 前 K 个高频元素#

这一题最符合直觉的做法是哈希表+小根堆,我们维持大小为k的小根堆,遇到大的就替换掉堆顶,最终就会剩余前k个高频元素。

Python3 点击展开代码
14 lines 展开代码

LC703 - 数据流中的第 K 大元素#

Python3 点击展开代码
15 lines 展开代码

合并多个有序结构#

LC23 - 合并 K 个升序链表#

之前合并k个升序链表是通过分治归并法解决,这里可以使用堆来解决这个问题。我们将所有链表当前节点放在最小堆里,每次取出堆顶构造。

Python3 点击展开代码
19 lines 展开代码

需要注意的是,我们必须要防止 Python 直接比较 node,因为链表节点本身不能比较大小。所以我们要存 node.valnode 的元组。但是题目中 node.val 还是可能会相等,然后顺位又去比较 node,所以我们还要在中间塞一个 idx,来保证永远比不到 node

LC373 - 查找和最小的 K 对数字#

最简单的写法就是二重循环全部入大小k的大根堆,每次超限踢出最大的,剩下的就是最小的K个。

Python3 点击展开代码
13 lines 展开代码

但是,这么写显然丢失了题目中的重要信息,即有序。假设nums1和nums2都有三个,我们可以写出3x3的矩阵,每一行都可以看成一个有序链表,比如1、7、11;2、4、6:

2 4 6
1 -> 3 5 7
7 -> 9 11 13
11 -> 13 15 17

显然,我们就可以套用 LC23 的思路,先把每一行第一个数字入堆,然后弹出当前最小对,再继续看这一行的下一个,直到取满 k 个。

Python3 点击展开代码
20 lines 展开代码

数据流与对顶堆#

LC295 - 数据流的中位数#

注意,没说输入的数据是按顺序的,虽然样例是按顺序的。这一题的解法是用两个堆,天然拿出左半边最大的和右半边做小的,注意做好奇偶处理。

Python3 点击展开代码
24 lines 展开代码

贪心 + 优先队列#

有时候,我们需要一边扫描,一边动态加入候选,并且反复取当前最大/最小。

LC253 - 会议室 II#

堆维护"现在占着资源的人"

给一个安排时间的数组,每个会议时间包含开始和结束,返回所需会议室最小数量,也就是: [[0,30],[5,10],[15,20]] -> 2。

这一题,是通过最小堆+优先队列实现的,堆里的元素表示要占用到哪个时间点。这样,新元素如果大于最小堆的最早结束时间,说明可以重复使用这个会议室,我们移除堆顶然后入堆即可:

Python3 点击展开代码
16 lines 展开代码

LC871 - 最低加油次数#

堆维护"过去留下来的备选资源"

这一题的标准思路是,我们维持一个大根堆,用fuel表示最远能到哪里,按顺序扫描所有加油站,把所有"已经能到达的站"的油量放入一个大根堆。如果当前的油不够去下一站/终点,就从大根堆里面拿一个最大的油补上,每弹一次堆,表示加油一次。

所以,这一题实际上是贪心用最大油量加,从而让加油量尽量少。为了实现这个贪心借助了堆结构。

Python3 点击展开代码
15 lines 展开代码

Python heapq 使用要点#

这里把 Python 里最常用的几个点单独记一下:

Python3 点击展开代码
8 lines 展开代码

几个高频注意点:

  • heapq 默认是小根堆
  • heapq.heapify(nums) 是原地建堆,返回值是 None
  • 想模拟大根堆,通常存相反数
  • 处理链表节点、对象、元组时,要注意比较规则
  • 如果元组前几位可能相等,后面又是不可比较对象,需要补一个 idx

一句话记忆:

Python 里优先队列几乎就是 list + heapq。

栈与队列设计题#

栈的设计题#

LC155 - 最小栈#

核心是"普通栈存值 + 辅助栈同步最小值"。难点不在 API,而在于要不要让 min_stack 和主栈等长。本文前面用的是最稳妥的等长写法。

LC225 - 用队列实现栈#

核心是让队列模拟"最后进来的先出去"。最稳定的写法是双队列倒腾,把最后一个元素单独留下来当栈顶。

设计题的核心不是套模板,而是想清楚"我要额外维护什么信息"。

这两题分别对应:

  • LC155:普通栈 + 同步最小值栈
  • LC225:用两个队列模拟"最后进入的先出来"

队列的设计题#

LC232 - 用栈实现队列#

核心是让一个栈负责输入、另一个栈负责输出。只有在输出栈为空时,才把输入栈整体倒过去。

LC622 - 设计循环队列#

核心是循环数组 + 取模。建议统一采用"浪费一个位置"的写法,这样空和满的判断最清楚。

LC641 - 设计循环双端队列#

比 LC622 多的只是"两头都能操作",本质仍然是循环数组和指针含义的统一。

队列设计题常见就三类:

  • 普通队列:先进先出
  • 循环队列:用取模节省空间
  • 双端队列:两头都能插入和删除

这类题一定要先把指针含义定死,不然特别容易写着写着把自己绕晕。最推荐的定义方式是:

  • front 指向队头元素
  • rear 指向队尾后一个空位
  • 空队列:front == rear
  • 满队列:浪费一个位置,判断 (rear + 1) % capacity == front

栈与队列题目的分类判断#

一看到括号匹配、消消乐、撤销操作,就想栈#

因为这类题都在维护"最近一个还没处理完的状态"。括号要和最近的左括号配对,消消乐要和最近的字符比,撤销也总是撤销最近一步。

一看到层序遍历、按顺序处理、最短步数,就想队列#

因为队列天然按顺序推进,而 BFS 又天然按层扩散。凡是"从起点最少几步到终点""一轮一轮蔓延"的题目,优先就该想到队列。

一看到下一个更大元素、找左右边界,就想单调栈#

这类题本质都在找"第一个打破单调性的位置"。只要当前元素一来,前面一批元素的答案就能被结算,这就是单调栈最典型的信号。

一看到滑动窗口最大值、窗口最小值,就想单调队列#

因为窗口在移动,而我们又需要实时拿到窗口最值。暴力 max/min 会超时,于是就要用单调队列维护窗口内部的有效候选。

一看到动态最值、Top K、多路归并,就想优先队列#

堆的关键词不是"排序",而是"动态维护"。候选集一直在变,但每一步只要其中一个最值,这时优先队列就很自然。

栈与队列常见模板#

普通栈模板#

Python3 点击展开代码
7 lines 展开代码

普通队列模板#

Python3 点击展开代码
8 lines 展开代码

双端队列模板#

Python3 点击展开代码
7 lines 展开代码

括号匹配模板#

Python3 点击展开代码
11 lines 展开代码

单调栈模板#

Python3 点击展开代码
7 lines 展开代码

单调队列模板#

Python3 点击展开代码
11 lines 展开代码

优先队列模板#

Python3 点击展开代码
9 lines 展开代码

栈与队列问题总结#

这一篇内容看起来很多,但其实核心只有几句话:

  • 栈解决最近、嵌套、回退
  • 队列解决顺序推进、层序扩散、最短步数
  • 单调栈解决"第一个更大/更小"和边界
  • 单调队列解决滑动窗口最值
  • 优先队列解决动态候选集里的最值问题

如果要把整篇再压缩成一句话,那就是:

不要先背题,而是先判断题目需要维护哪一种顺序。

当你开始能从"维护顺序"的角度看题时,栈、队列、单调栈、单调队列、优先队列这几个专题就会越来越像一家人,而不是零散的很多模板。

专题阅读

算法

这篇文章属于同一条阅读链。你可以直接在这里切换,不用再回到列表页重新找。

当前进度5 / 7

留言区

留言

欢迎纠错、补充、交流。昵称和评论内容必填;如果你愿意,也可以留下联系方式,仅站主可见。

0

正在加载评论...

0 / 2000

阅读导航

文章目录

当前阅读位置将在这里显示

0 节