最近在二刷 LeetCode Hot 100,这个系列文章写到第 12 期,正好轮到「栈」。
说实话,Hot 100 里栈相关题目的数量不算多,但每道都称得上小而经典:20 有效的括号、155 最小栈、84 柱状图中最大的矩形、42 接雨水……这几道题分布在算法面试、周赛、日常手撕代码里出现频率都很高。我以前第一遍刷的时候是跟着题号硬刷,结果遇到 42 接雨水和 84 柱状图这种题直接懵,总觉得栈就是个“先进后出”的容器,用起来却抓不住时机。后来把栈专题拿出来集中复盘,才发现栈题根本不是靠背题,而是靠几个固定模型:括号对称匹配、最小栈历史记录、单调栈找边界、栈加回溯处理嵌套场景。把这几类模型吃透,Hot 100 的栈题基本就通了。
这篇文章我会按“题型分类 → 题单总体评估 → 核心套路拆解 → 代码级实操 → 踩坑实录 → 工程应用”这个顺序来整理。内容适合正在二刷 Hot 100 的选手,也适合刚学完基础数据结构、想知道栈到底能解决什么问题的读者。Hot 100 各平台/版本的题序会有小幅差异,我以栈相关性最强的几道题为对齐标准来写。
1. 先盘清楚:Hot 100 栈题到底在考什么?
1.1 栈类题目的三大题型
栈这个数据结构本身很简单,就一句话:后进先出。但为什么面试官这么爱考栈?因为栈考的不是“你会不会 push 和 pop”,而是你能否在正确的时间点做正确的操作。Hot 100 里的栈题,剥开外壳之后基本就三类。
第一类是“对称匹配型”。代表性题目是 20. 有效的括号。它的本质是嵌套结构合法性判断,比如 ([{}]) 这种,用栈去匹配成对的符号。这类题的核心是:遇到左符号就入栈,遇到右符号就检查栈顶是否匹配。虽然题目简单,但它背后的能力是“解析嵌套文本”,JSON 解析器、编译器语法检查、HTML 标签配对都在用同一套逻辑。
第二类是“单调栈型”。代表性题目是 739. 每日温度、84. 柱状图中最大的矩形、42. 接雨水。这类题考的是寻找“下一个更大/更小元素”或者“左右边界”。单调栈维护的是一个有序的栈内序列,配合元素出栈时的时机来计算答案。Hot 100 里最难的几道栈题几乎都属于这个模型,而且它们之间高度相似:84 会了,42 基本就通了一半。
第三类是“状态栈型”。代表性题目是 155. 最小栈、394. 字符串解码。这类题用栈记录历史状态,要么是记录历史最小值,要么是记录嵌套上下文,配合回溯和展开来完成多层结构处理。最小的状态就是括号嵌套加数字重复的展开,做完 394 再做编译原理里的 AST 相关题目,会有一种“原来如此”的感觉。
1.2 为什么笔试面试总盯着这几道题
一个很现实的问题是:栈题在所有数据结构题里占比并不高,但出镜率极高。原因不是栈本身难,而是栈背后的能力模型很值钱。
第一,栈题能顺带考察“时间复杂度和空间复杂度的取舍”。比如 155. 最小栈,题目要求 O(1) 取最小值,最自然的想法是每次遍历找最小值,但这样 getMin 是 O(n),要压到 O(1) 就得额外维护一个辅助栈。这不是脑筋急转弯,而是工程里常见的时间换空间、空间换时间的权衡。
第二,栈题天然和递归、回溯、深度优先搜索绑定。函数调用本来就用调用栈实现,很多递归能改写成栈迭代,比如 394. 字符串解码既可以用 DFS 递归,也可以用栈迭代。会做栈题,意味着你对“嵌套结构”和“递归调用关系”有直觉,这对理解系统设计里的调用链、消息队列里的重入、任务调度里的依赖关系都有帮助。
第三,栈题可以设计成递进式追问。面试官从“写一个判断有效括号的函数”开始,然后追问“如果是包含星号的通配符匹配呢”“如果括号类型变成三种呢”“如果要求最长有效括号长度呢”,一道基础题能延伸出 32. 最长有效括号这种 Hard 题。考察的是候选人能不能从基础模型上做泛化,而不是背模板。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 把题单摊开:栈专题要掌握到什么程度
2.1 七道核心题总览表
Hot 100 页面在不同时间点会有调整,不同版本甚至会把接雨水归类到双指针、把最长有效括号归类到动态规划。从“栈解法”角度出发,我按自己的刷题节奏整理了一份栈专题题单,并且按难度和必刷程度打了分:
| 题目 | 题号 | 核心模型 | 推荐解法 | 时间复杂度 | 空间复杂度 | 必刷指数 |
|---|---|---|---|---|---|---|
| 有效的括号 | 20 | 对称匹配 | 辅助栈 | O(n) | O(n) | 必刷 |
| 最小栈 | 155 | 状态记录 | 辅助栈 | O(1) 全部操作 | O(n) | 必刷 |
| 每日温度 | 739 | 单调栈 | 单调递减栈 | O(n) | O(n) | 必刷 |
| 字符串解码 | 394 | 嵌套展开 | 双栈迭代 | O(n) | O(n) | 高 |
| 柱状图中最大的矩形 | 84 | 单调栈 | 单调递增栈 + 哨兵 | O(n) | O(n) | 高 |
| 接雨水 | 42 | 单调栈 | 单调递减栈 | O(n) | O(n) | 高 |
| 最长有效括号 | 32 | 栈记录索引 | 栈底哨兵法 | O(n) | O(n) | 中高 |
另外,如果版本里有 71. 简化路径,也建议顺手刷一下。它表面是在模拟文件路径解析,实际上考察的是对栈进出时机的理解,尤其是遇到 .. 时要出栈、遇到正常目录名要入栈,这种题目可以作为热身练习。
2.2 怎么判断一道题该不该用栈
很多同学卡在“不知道什么时候该想到栈”这一步。我给一个自己的判断标准:一道题里出现“嵌套结构”“最近匹配”“逐层展开”“撤销/恢复”“单调边界”这些特征时,大概率能往栈上想。
举几个具体场景。括号成对出现,天然符合“最近匹配”,用栈最舒服。计算器求值要考虑运算符优先级,本质也是先把一部分操作暂存,等优先级更高的算完再回来处理,这个“暂存再回来”的过程就是栈。浏览器的前进后退为什么用两个栈?因为你后退之后前进历史会被清空,这就是栈的天然性质。React Fiber、Redux 中间件这些前端技术里也大量使用调用栈来维护执行上下文。
还有一个小技巧:如果题目要求的结果和“数组里每个元素向左或向右看第一个满足某个条件的元素位置”有关,那就别犹豫,直接往单调栈上想。这不是玄学,因为“第一个大于/小于当前元素”这种问题,暴力解法是 O(n²),单调栈可以把每个元素最多入栈一次、出栈一次,整体降到 O(n)。面试时你说出这个复杂度分析,比单纯背答案有用得多。
2.3 栈的两种实现:容器栈和数组模拟
刷题的时候栈的实现不纠结,Python 里直接用列表当栈,Java 里用 Deque,C++ 用 stack。但我建议自己在练习时偶尔用数组模拟一下栈,尤其是在处理最大矩形和接雨水这种下标敏感的问题时,数组模拟会方便很多。
比如 C++ 的 stack<int> 不支持随机访问,你想看栈顶下面一个元素是什么就麻烦了;而用 vector<int> st 模拟时,st.back() 是栈顶,st[st.size() - 2] 可以拿到次栈顶。很多写 84 柱状图题解的人喜欢用数组模拟,不是没道理的——当你想在弹出栈顶之后立刻拿新栈顶作为左边界时,数组模拟会让代码更直白
