刷LeetCode Hot 100的时候,我有个特别深的感受:栈这一块题目数量不算多,但几乎每一道都是经典,而且套路特别固定。只要把“括号匹配”、“单调栈”和“辅助栈”这三类问题吃透,整个Hot100里的栈题基本就通了一大半。这篇文章就来聊聊我对Hot100中栈题目的拆解,包括每一道题的考点、通用模板、还有我实际刷题时踩过的各种坑。不管你是刚开始刷题,还是已经在二刷三刷,我都尽量用大白话把这些题背后的逻辑讲透,方便你直接拿来用。
1. 内容整体设计与思路拆解
1.1 Hot100中栈题目全景
先整理一下我印象里Hot100中带“栈”标签的题目。我按自己的刷题习惯列了个表,包含了题号、考点和难度,方便后面逐个拆:
| 题目 | 题号 | 核心考点 | 难度 |
|---|---|---|---|
| 有效的括号 | 20 | 栈的基础入栈出栈匹配 | 简单 |
| 最小栈 | 155 | 辅助栈维护当前最小值 | 简单 |
| 每日温度 | 739 | 单调栈找右侧第一个更大元素 | 中等 |
| 柱状图中最大的矩形 | 84 | 单调栈找左右边界 | 困难 |
| 接雨水 | 42 | 单调栈/双指针/动态规划 | 困难 |
| 字符串解码 | 394 | 栈处理嵌套结构和重复子串 | 中等 |
| 最长有效括号 | 32 | 栈存下标模拟连续匹配 | 困难 |
| 用栈实现队列 | 232 | 双栈模拟队列 | 简单 |
这张表基本就是Hot100里栈相关题目的主力阵容了。有些题比如“简化路径”、“逆波兰表达式求值”可能在某些版本里也算栈题,但不在Hot100主列表内,咱们先不讨论。
1.2 为什么栈题的套路这么固定
栈最核心的特性就是后进先出,这个特性天然适合处理两类场景:一类是“最近匹配”,比如括号匹配,你最后一次看到的左括号,一定和下一次出现的右括号匹配;另一类是“嵌套展开”,比如字符串解码中“3[a2[c]]”,先展开内部的2[c],再乘3,顺序刚好和栈的进出相反。
而单调栈,是栈的一个进阶用法。它并不是存一堆元素然后随便用,而是在元素入栈和出栈的过程中,始终保持栈内元素的有序性。比如“每日温度”,我们要找每个温度右边第一个比它高的温度,如果我用普通暴力法,每个位置都要往后扫描,复杂度是O(n^2)。用单调栈的话,每个元素只会入栈一次、出栈一次,总复杂度O(n),所以Hot100里凡是和数组相关且要找“下一个更大/更小元素”的题,基本都能用单调栈。
1.3 我推荐的刷题顺序
如果你刚开始刷Hot100的栈,我的建议是千万别被“困难”标签吓到。按这个顺序来会顺很多:
- 先做20题“有效的括号”,理解栈最基础的操作。
- 再做155题“最小栈”,学会用辅助栈保存额外信息。
- 之后做739题“每日温度”,接触单调栈的第一个经典场景。
- 再挑战84题“柱状图中最大的矩形”和42题“接雨水”,彻底吃透单调栈的边界处理。
- 最后做32题“最长有效括号”和394题“字符串解码”,这两道题综合性强,能帮你把栈的思路和其他技巧结合起来。
这个顺序的逻辑是从“怎么用栈”到“什么时候用栈”,再到“栈和其他算法怎么配合”。尤其是84和42这两道困难题,它们本质上是同一个模型的两种变形,一起刷对比着看,效率特别高。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析与实操要点
2.1 括号类:从有效括号到最长有效括号
20题“有效的括号”是栈的入门题。思路很简单:遍历字符串,遇到左括号就入栈,遇到右括号就检查栈顶是不是对应的左括号,如果是就弹出,不是或者栈为空就说明非法。最容易被忽略的一个小剪枝是:如果字符串长度是奇数,直接返回false,不需要遍历。
这道题我最初写的时候犯过一个低级错误:用字典存括号对的时候,key和value写反了。后来我习惯这样写:遇到右括号时,判断栈顶元素和当前字符是否配对。比如用字典pairs = {")": "(", "]": "[", "}": "{"},遇到右括号时,如果栈为空或栈顶不是pairs[char],就返回false。
32题“最长有效括号”就比20题难不少了。它求的不是“是否有效”,而是“最长的连续有效括号子串长度”。我第一次看到这题直接用20题的思路,遇到右括号就出栈,结果发现计数很乱,因为需要处理“()(”这种中间断裂的情况。
正解是栈里存下标,而不是字符,并且要初始化push一个-1进去。为什么是-1?因为“有效括号子串”的起点可能是字符串开头,用-1作为哨兵,计算长度的时候更方便。
具体逻辑:遍历字符串,遇到左括号就把当前下标入栈;遇到右括号就先弹出栈顶(这个弹出可能会弹出一个左括号下标,也可能会弹出之前的哨兵下标),弹出后如果栈空了,说明当前右括号前面没有可匹配的左括号,那这个右括号就变成了新的“哨兵”,我们把当前下标入栈,作为新的分隔点;如果栈不为空,说明从栈顶元素的下一个位置到当前下标之间是有效括号子串,长度就是i - stack[-1]。
举个例子,s = “)()())”,用这方法走一遍:
- i=0,char=')',栈初始为[-1],弹出-1后栈空,push(0),栈变为[0]。
- i=1,char='(',push(1),栈[0,1]。
- i=2,char=')',弹出1,栈非空,栈顶0,长度=2-0=2,更新答案;栈还是[0]。
- i=3,char='(',push(3),栈[0,3]。
- i=4,char=')',弹出3,栈非空,栈顶0,长度=4-0=4,更新答案。
- i=5,char=')',弹出0,栈空,push(5),栈[5]。
- 最终答案是4,对应子串“(())”?这里验证一下:s=“)()())”,索引1~4是“()()”,长度为4,没错。
这个过程的精妙之处在于用下标差算长度,同时用-1和“右括号当哨兵”处理非法位置。我后来发现很多“最长连续有效括号”的写法,都是在这个模板上做微调。
2.2 单调栈核心:每日温度、最大矩形、接雨水
单调栈是Hot100栈题里占比最大、也最容易混淆的部分。我先把最核心的结论放在前面:
- 找右边第一个比当前元素大的元素:维护一个从栈底到栈顶单调递减的栈(栈顶最小)。遍历时,新元素如果大于栈顶,说明栈顶找到了目标,出栈结算。
- 找右边第一个比当前元素小的元素:维护一个从栈底到栈顶单调递增的栈(栈顶最大)。遍历时,新元素如果小于栈顶,栈顶出栈结算。
这个结论一定要记牢。很多教程喜欢说“单调递增栈”或“单调递减栈”,但大家定义角度不一样,有的从栈底到栈顶看,有的从栈顶到栈底看,特别容易晕。我建议你统一按“从栈底到栈顶”的顺序来理解:找更大,栈底到栈顶递减;找更小,栈底到栈顶递增。
739题“每日温度”是典型的“找右边第一个更大元素”。题目给了一个温度数组,让你返回每个位置需要等几天才出现更高温度。暴力解法很简单,两层循环,但Hot100里肯定要优化。用单调栈的话,遍历数组,维护一个递减栈(栈底到栈顶递减),当新温度比栈顶温度高时,说明栈顶温度找到了右边第一个比它高的温度,出栈并计算索引差。
我写这题时的模板大概是这样的:
python复制def dailyTemperatures(temperatures):
n = len(temperatures)
res = [0] * n
stack = []
for i in range(n):
while stack and temperatures[i] > temperatures[stack[-1]]:
prev = stack.pop()
res[prev] = i - prev
stack.append(i)
return res
你注意看,栈里存的是下标,不是温度值。因为计算“等几天”需要用到索引差。判断大小的时候,通过temperatures[stack[-1]]取值。这就是存下标的经典原因。
84题“柱状图中最大的矩形”比739难一个量级。它要求你在一排宽度为1的柱子里,找出面积最大的矩形。核心思路是:对于每一根柱子,把它当作矩形的高,那么矩形的宽度能延伸到多宽?答案是左右两边第一根比它矮的柱子之间的范围。也就是说,对每个柱子,我们需要找到左边第一个小于它的下标left和右边第一个小于它的下标right,那么面积就是height[i] * (right - left - 1)。
这个问题用单调栈怎么做?我们需要找每个位置“右边第一个更小的元素”,同时还要找“左边第一个更小的元素”。遍历数组时,维护一个从栈底到栈顶递增的栈。当新元素小于栈顶对应的柱子高度时,栈顶元素出栈,此时:
- 右边第一个小于栈顶元素的下标就是当前遍历到的
i; - 左边第一个小于栈顶元素的下标就是新的栈顶(如果栈空,说明左边没有更小的,可以认为左边界是-1)。
于是面积就能算了。这里有个特别实用的技巧:在原始数组的头尾各加一根高度为0的柱子。加0的目的是保证所有正常柱子最后都会被弹出结算,避免遍历结束后栈里还留着元素,需要额外处理。
伪代码:
python复制def largestRectangleArea(heights):
heights = [0] + heights + [0]
stack = []
res = 0
for i in range(len(heights)):
while stack and heights[i] < heights[stack[-1]]:
h = heights[stack.pop()]
left = stack[-1] # 左边第一个比 h 矮的下标
right = i # 右边第一个比 h 矮的下标
res = max(res, h * (right - left - 1))
stack.append(i)
return res
这里有个点很容易想不通:为什么出栈后新的栈顶就是左边第一个比它矮的?因为栈是从栈底到栈顶递增的,出栈前的栈顶元素是当前柱子的高度,而在它下面的元素,必然高度小于等于它,并且是在它左边最近的那个“小于等于”它高度的柱子。由于我们维护的是严格递增栈,如果高度相等,可能计算出来的宽度会少算,所以通常遇到相等高度时也可以出栈,最终结果还是对的,因为相等的柱子逐一出栈时会覆盖相同面积。
42题“接雨水”和84题是反向的。接雨水要找的是“每个坑能接多少水”,需要知道一个位置左边最高的柱子和右边最高的柱子,取两者较小值减去当前位置高度。用单调栈做时,我们维护一个从栈底到栈顶递减的栈。当新元素比栈顶高时,说明栈顶元素作为一个凹槽的底部,它的左右两边都有更高的柱子了,可以结算水量。
具体来说,当height[i] > height[stack[-1]]时,弹出栈顶元素top,此时i是右边第一个比它高的,新的栈顶left是左边第一个比它高的,那么接水宽度是i - left - 1,高度是min(height[left], height[i]) - height[top]。累加所有水量即可。
很多同学觉得接雨水难,其实你把它和84题放在一起对比看,会发现它们都是“出栈时结算”的模型,只是一个结算面积,一个结算水量。区别在于:
- 84题维护递增栈,找更小边界;
- 42题维护递减栈,找更大边界。
这两个方向千万别搞混。我一开始就是一直搞混,后来总结了一句口诀:“找大用递减,找小用递增,出栈时结算”。
2.3 辅助栈:最小栈、用栈实现队列
155题“最小栈”是一道看起来简单,但非常考验“空间换时间”思路的题。题目要求实现一个栈,支持push、pop、top以及getMin,所有操作都要求O(1)时间复杂度。难点在于getMin要O(1)返回最小值,而栈里的元素会动态变化。
常规解法是双栈法:一个栈stack存原始数据,另一个栈min_stack存当前最小值。push的时候,stack正常入栈;min_stack则比较新元素和min_stack栈顶,把较小者入栈。这样min_stack的栈顶始终是当前栈内所有元素的最小值。pop的时候两个栈一起pop,这样min_stack也能同步更新。
还有一种优化是节省辅助栈空间的“差值法”,把每个元素与当前最小值的差值存进栈里。但实际工作中,双栈法最直观、最好写、也不容易出错。我个人刷题时优先保证正确性和可读性,所以一直用双栈法。
232题“用栈实现队列”同样是用两个栈。核心思路是:用stack_in负责入队,用stack_out负责出队。push时直接压入stack_in;pop时,如果stack_out为空,就把stack_in里的所有元素全部倒入stack_out,然后从stack_out弹出栈顶。因为stack_in的栈顶是最后入队的元素,倒到stack_out后,原来最先入队的元素会跑到stack_out的栈顶,从而实现了先进先出。
这里有个细节:倒入操作要在pop或peek时判断stack_out是否为空,如果没有判断就倒,会导致顺序错乱。比如先push了1、2,此时stack_in=[1,2],stack_out=[]。如果连续pop两次,第一次pop时把stack_in全部倒入stack_out,stack_out=[2,1],弹出1;第二次pop时stack_out不为空,直接弹出2,顺序是正确的。但如果你每次pop前都倒,第二次pop的时候stack_in是空的,再倒也是空,没问题;但如果中途push了3,然后没有判断stack_out为空就又倒一次,就会把3压到stack_out的栈顶上,导致先弹出3,顺序就错了。所以“只有stack_out为空时才倒”是个必须注意的条件。
2.4 字符串解码:用栈处理嵌套结构
394题“字符串解码”是栈应用里比较综合的一道题。题目类似3[a2[c]]输出accaccacc。核心难点在于数字和字符串的嵌套重复。只要出现嵌套,就是栈的主场。
解法是用两个栈:一个栈存数字,一个栈存字符串。遍历字符串时,维护两个变量:cur_num用于累加当前数字,cur_str用于拼接当前层字符串。
- 遇到数字,更新
cur_num = cur_num * 10 + int(ch)。 - 遇到左括号,说明要进入新的一层。把当前
cur_num压入数字栈,把当前cur_str压入字符串栈,然后重置cur_num = 0,cur_str = ""。 - 遇到右括号,说明当前层结束。从数字栈弹出重复次数
num,从字符串栈弹出上一层字符串prev_str,然后prev_str += cur_str * num,再把结果赋值给cur_str。 - 遇到字母,直接
cur_str += ch。
举个例子3[a2[c]]:
- 遍历到
3,cur_num=3。 - 遇到
[,num_stack=[3],str_stack=[""],重置cur_num=0,cur_str="". - 遇到
a,cur_str="a". - 遇到
2,cur_num=2。 - 遇到
[,num_stack=[3,2],str_stack=["", "a"],重置cur_num=0,cur_str="". - 遇到
c,cur_str="c". - 遇到
],num=2,prev_str="a",cur_str="a" + "c"*2 = "acc". - 遇到最后的
],num=3,prev_str="",cur_str="" + "acc"*3 = "accaccacc".
这个过程中保证字符串栈存的是“进入当前层之前已经拼接好的内容”,这样嵌套返回时,上一层的字符串不会被覆盖。很多初写这道题的人会忘记重置cur_str,导致字符串叠加错乱,我自己也踩过。
3. 实操过程与核心环节实现
3.1 单调栈通用模板与记忆口诀
我现在写单调栈题,基本就是一套模板改改条件。这里直接给一个完整的可复用模板,找“右边更大元素”:
python复制def solve(nums):
stack = []
res = [0] * len(nums)
for i in range(len(nums)):
# 根据需求修改比较符号:找更大用 >,找更小用 <
while stack and nums[i] > nums[stack[-1]]:
idx = stack.pop()
# 结算逻辑,比如 res[idx] = i - idx
stack.append(i)
return res
如果是找“右边更小元素”,把while条件里的>改成<就行。如果是找“左边更大/更小”,可以倒序遍历数组,或者改造结算时取左边边界。
你们发现没有,单调栈的代码结构极其简单:一个for循环,一个while,最后一句push。难的不是写代码,而是判断什么时候结算、结算什么。我的经验是:在出栈时,弹出的那个元素就是“被结算对象”,当前遍历到的元素和新的栈顶,往往就是它的左右边界。把这个规律记牢,所有单调栈问题都能迎刃而解。
3.2 以“柱状图中最大的矩形”为例逐步模拟
纸上谈兵没用,我带着你完整跑一遍84题。假设heights = [2,1,5,6,2,3]。
先给数组前后加0:new_heights = [0,2,1,5,6,2,3,0]。为什么加0?因为0肯定比所有柱子都矮,遍历到最后的0时,栈里所有柱子都会被强制弹出并结算,这样不用再做“遍历结束后检查残留”的步骤。
初始化stack = [],res = 0。
- i=0,h=0,stack空,push(0)。
- i=1,h=2,栈顶是0,h=0<2,满足递增(栈底到栈顶递增),push(1)。
- i=2,h=1,此时h=1 < heights[stack[-1]]=2,弹出栈顶1,高度2,新的栈顶为0,右边界i=2,左边界index=0,面积=2*(2-0-1)=2。接着h=1与栈顶0比较,0<1,停止。push(2)。
- i=3,h=5,比栈顶1大,push(3)。
- i=4,h=6,比栈顶5大,push(4)。
- i=5,h=2,比栈顶6小,弹出4,高度6,新栈顶3,右边界5,左边界3,面积=6*(5-3-1)=6。继续比较h=2与栈顶3(高度5),2<5,弹出3,高度5,新栈顶2,右边界5,左边界2,面积=5*(5-2-1)=10。继续比较h=2与栈顶2(高度1),2>1,停止。push(5)。
- i=6,h=3,比栈顶2大,push(6)。
- i=7,h=0,此时0小于栈顶高度3,需要连续弹出:弹出6,高度3,新栈顶5,右边界7,左边界5,面积=3*(7-5-1)=3;弹出5,高度2,新栈顶2,右边界7,左边界2,面积=2*(7-2-1)=8;弹出2,高度1,新栈顶0,右边界7,左边界0,面积=1*(7-0-1)=6;弹出0(哨兵),新栈空,结束。
最终最大面积是10。手动推一遍之后,你会很清楚地看到,每个柱子被弹出时,它左右两边的“更矮柱子”都已经确定了,面积一次性算完。这个过程比死记代码有用得多。
3.3 接雨水与最大矩形的对照实现
我强烈建议你把84和42一起写一遍。我用两段并排代码来对比。
84最大矩形找左右更小的变体:
python复制def largestRectangleArea(heights):
heights = [0] + heights + [0]
stack = []
res = 0
for i in range(len(heights)):
while stack and heights[i] < heights[stack[-1]]:
h = heights[stack.pop()]
res = max(res, h * (i - stack[-1] - 1))
stack.append(i)
return res
42接雨水找左右更大的变体:
python复制def trap(height):
stack = []
res = 0
for i in range(len(height)):
while stack and height[i] > height[stack[-1]]:
top = stack.pop()
if not stack:
break
left = stack[-1]
width = i - left - 1
h = min(height[left], height[i]) - height[top]
res += width * h
stack.append(i)
return res
你看这两段代码的骨架是几乎一样的:都是while循环在破坏单调性时弹出元素,然后算贡献。区别只是:
- 84的while条件是
<(找更小),42的while条件是>(找更大)。 - 84不用检查栈空(因为加了0哨兵,栈里永远有底),42里如果弹出后栈空了,说明左边没有更高的柱子,无法形成凹槽,直接break。
- 84用
i - stack[-1] - 1算宽度,42用i - stack[-1] - 1算宽度(left是新的栈顶)。 - 84的高度是弹出的柱子高度,42的高度是左右两边较矮的减去弹出的柱子高度。
对比着看,单调栈的核心思想就完全暴露了:出栈时,栈顶元素的“信息”左右边界都已经确定,这就是“结算时刻”。
3.4 栈题代码实现中的几个硬性注意点
这里说几个我实际调试中经常碰到的坑,写代码时一定要留意:
- 单调栈里存下标,而不是值。所有需要计算宽度、距离的题,存索引都是必须的。如果只是判断大小,可以通过索引访问原数组。
- 判断是push下标还是push值时,注意你要访问的是哪一段。容易把
stack[-1]当成值,实际上它是下标,还要再加一层nums[stack[-1]]。 - 栈的初始化。有的题需要先push哨兵,比如32题的-1,84题的0(放到数组前后)。忘了初始化哨兵,很多边界case会直接错。
- while循环内的结算逻辑一定要写在pop之后。因为pop之后栈顶才更新为新的左边界。如果先算面积再pop,就会用到已经过期的栈顶。
- 注意比较符号的方向。如果你在写每一道题时都重新思考“是大于还是小于”,很容易犯迷糊。我的习惯是先把需求翻译成“找右侧更大”还是“找右侧更小”,再决定符号。
4. 常见问题与排查技巧实录
4.1 总是分不清用递增栈还是递减栈,怎么办
这个问题我反复说过,但还是要强调。你只需要记住一句话:“找右边第一个更大的,用递减栈;找右边第一个更小的,用递增栈。”这里的递增/递减指的是从栈底到栈顶。
为什么?我们模拟一下找更大的过程:当新元素比栈顶大时,栈顶出栈,说明栈顶元素遇到了更大的元素。为了让新元素继续存在栈里,它最终会慢慢变成栈底,所以栈底大、栈顶小,整体递减。找更小的过程则相反。
如果你做题时还是记不住,就在草稿纸上画一个例子,比如每日温度的递减栈,画几遍就记住了。备考阶段我每天都会默写单调栈这两种模板各一遍,半个月后完全不会混。
4.2 为什么栈里存下标而不是值,什么时候可以存值
主要看结算时需不需要知道索引。以每日温度为例,需要返回“间隔几天”,所以必须知道两个日期的索引差。最大矩形需要知道左右边界的位置,所以也要存下标。接雨水需要知道两个柱子之间的距离,同样需要索引。
但像最小栈这种辅助栈,只需要存最小值,不涉及索引,直接存值就够了。还有某些题目如果只要求返回布尔值或是否满足条件,存值也可以。
所以判断标准很简单:结算公式里有没有“索引之间的运算”?有就存下标,没有就随便。
4.3 最大矩形为什么要在数组前后加0,接雨水为什么不用
最大矩形用单调栈找左右第一个更矮的柱子。如果没有0哨兵,遍历结束后栈里可能还有剩余元素,它们的右边界其实已经结束了(相当于数组末尾之后),但你没算。加0后,最后一个0一定比所有正常柱子矮,会触发栈里所有元素弹出,这样就不会漏算。
接雨水为什么不用加0?因为接雨水的结算条件是必须存在左右两边更高的柱子才能形成凹槽,如果在末尾加0,0本身不会形成凹槽,也不会强制弹出,所以没意义。相反,接雨水要处理栈空的情况,如果弹出后栈空,说明左边没有更高的柱子,无法积水,应该跳过。
4.4 字符串解码里数字可能是多位数,怎么处理
题目里12[abc]这种,如果只写cur_num = int(ch),遇到两位数就会出错。正确写法是cur_num = cur_num * 10 + int(ch)。我之前就吃过亏,写成了直接累加,结果12[abc]变成“3个abc”,因为1和2被分开解析了。一定要在遇到数字时持续累加,遇到[时才把当前的数字压栈并重置。
4.5 栈题调试的通用小技巧
我刷栈题时,遇到的bug大多数是边界条件和结算时机的问题。我的调试套路是:
- 先用小规模case手动模拟一遍,比如长度3或4的数组,把每一步的栈内容、当前遍历位置、结算结果都写出来。
- 如果对不上,就在代码里关键位置打印
stack、i、current_val。 - 特别注意while循环退出后栈顶变化,以及栈空时的分支。
- 有些题可以用简单暴力解法生成随机测试,再和单调栈解法对拍。比如46题“全排列”这种带排列的不用,但像84题可以随机生成heights,暴力算最大矩形对比。不过Hot100本身题量够用,不必额外造。
5. 个人刷题心得与扩展方向
5.1 栈题的通用解题流程
我现在拿到一道新题,如果怀疑要用栈,会先问自己三个问题:
- 这个问题是否涉及“最近匹配”或“嵌套展开”?
- 是否要找某个元素左边或右边第一个更大/更小?
- 是否需要在遍历过程中维护一个“历史状态”的序列?
只要命中其中一个,栈基本就是首选数据结构。接下来再判断用什么栈:普通栈、辅助栈还是单调栈。普通栈解决括号匹配、嵌套展开;辅助栈解决需要额外状态的场景;单调栈解决和“下一个更大/更小”相关的极值问题。
这个流程80%的栈题都能套上。剩下的比如“用栈实现队列”属于纯纯的栈操作模拟,那就直接背双栈模板就行。
5.2 从栈到递归、二叉树遍历的迁移
栈和递归本质上是相通的。函数调用就是用系统栈实现的,递归的“调用栈”就是栈。所以很多递归题都可以改写成显式栈的迭代版本。Hot100里大量二叉树题,比如中序遍历,迭代写法就用栈:
python复制def inorderTraversal(root):
stack = []
res = []
cur = root
while cur or stack:
while cur:
stack.append(cur)
cur = cur.left
cur = stack.pop()
res.append(cur.val)
cur = cur.right
return res
这个模板就是模拟递归压栈的过程。你学栈的时候,如果顺便把二叉树迭代遍历练一遍,理解会更深。同样,回溯算法也是栈的思想,比如深度优先搜索DFS,本质上是利用调用栈。
5.3 刷Hot100栈题后,我觉得最有价值的收获
刷完这一组题,我最大的感受是:栈题考察的不是“会不会用栈”,而是“能不能识别出这是个栈问题”。很多人卡在84题和42题,不是不会写循环,而是想不到用单调栈。所以我在刷题的过程中,特别注意总结“什么特征提示我该用单调栈”——一旦发现题目里出现“比它大/小”、“左边/右边第一个”、“连续区间贡献”这类关键词,就应该往单调栈方向想。
另外,Hot100的题目互相之间关联性很强。比如接雨水可以用单调栈,也可以用双指针、动态规划;每日温度是单调栈的入门版;柱状图中最大矩形是单调栈的进阶版。你把这些题一起整理成一个“单调栈专题”,远比单独刷题效果好。
最后再分享一个小习惯:我在本地维护了一份“栈题笔记”,每做完一道题,会把这道题的题型、模板、易错点写到笔记里。比如84和42的对比、32题为什么用下标、394题为什么重置cur_str。二刷的时候直接翻笔记,几分钟就能回忆起所有关键点。刷题这种事,不怕慢,就怕没总结。栈这个专题虽然题量不大,但每一道都值得反复咀嚼。
