📚 栈 — 后进先出的智慧
栈是最简洁的数据结构之一,却是括号匹配、表达式求值、单调栈的基石
📖 栈的核心概念
栈(Stack)是一种后进先出(LIFO)的数据结构,只允许在栈顶进行插入和删除。栈的操作极为简单——push(入栈)和 pop(出栈),但这简单的规则却能解决括号匹配、表达式求值、深度优先搜索等众多问题。面试中,栈的进阶应用——单调栈,是高频考点。
栈的模型: 后进先出 (LIFO)
┌───┐
│ 5 │ ← 栈顶 (最后入栈,最先出栈)
├───┤
│ 3 │
├───┤
│ 1 │
├───┤
│ 7 │
└───┘ ← 栈底
操作: 时间复杂度
push(x) 入栈 O(1)
pop() 出栈 O(1)
peek() 看顶 O(1)
empty() 判空 O(1)
栈的思维模型:
- 最近相关性: 关注"最近的"配对/匹配
- 递归的迭代版: 用栈模拟递归调用
- 单调性: 维护单调递增/递减的栈
1. 栈的三大应用场景
场景一:匹配/配对问题
括号匹配、XML标签匹配、表达式求值
核心:遇到右括号,找最近的左括号 → 栈的 LIFO 天然匹配!
场景二:单调栈
寻找每个元素"左边/右边第一个更大/更小"的元素
核心:维护栈的单调性,不满足时弹出并处理
场景三:模拟递归
DFS 迭代版、撤销操作(Undo)、函数调用栈
核心:用显式栈代替隐式递归调用栈
2. Python 栈操作速查
# 用 list 模拟栈(推荐)
stack = []
stack.append(1) # push
stack.pop() # pop (末尾)
stack[-1] # peek
len(stack) == 0 # empty
# collections.deque(线程安全,两端O(1))
from collections import deque
stack = deque()
stack.append(1)
stack.pop()
🎯 题目一:有效的括号 (LC 20)
题目描述:给定一个只包括 '(', ')', '{', '}', '[', ']' 的字符串 s,判断字符串是否有效。有效字符串需满足:左括号必须用相同类型的右括号闭合,且左括号必须以正确的顺序闭合。
示例: s = "{[()]}"
遍历过程:
'{' → 入栈: ['{']
'[' → 入栈: ['{', '[']
'(' → 入栈: ['{', '[', '(']
')' → 匹配栈顶'(' → 出栈: ['{', '[']
']' → 匹配栈顶'[' → 出栈: ['{']
'}' → 匹配栈顶'{' → 出栈: []
栈空 → 有效! ✓
反例: s = "([)]"
'(' → ['(']
'[' → ['(', '[']
')' → 栈顶'[' ≠ '(' → 无效! ✗
思路分析
栈匹配法:
1. 遇到左括号 → 入栈
2. 遇到右括号 → 检查栈顶是否为对应左括号
3. 匹配 → 出栈;不匹配 → 无效
4. 遍历结束,栈为空 → 有效;栈非空 → 无效
时间复杂度:O(n),空间复杂度:O(n)
代码实现
class Solution:
def isValid(self, s: str) -> bool:
mapping = {')': '(', '}': '{', ']': '['}
stack = []
for ch in s:
if ch in mapping: # 右括号
top = stack.pop() if stack else '#'
if top != mapping[ch]:
return False
else: # 左括号
stack.append(ch)
return not stack
复杂度分析
时间复杂度:O(n) — 一次遍历
空间复杂度:O(n) — 最坏全部入栈
变体练习
- LC 32 最长有效括号(Hard,栈+动态规划)
- LC 22 括号生成(回溯法生成合法括号)
🎯 题目二:最小栈 (LC 155)
题目描述:设计一个支持 push、pop、top 操作,并能在常数时间内检索到最小元素的栈。
示例操作序列:
push(-2) → data: [-2] min_stack: [-2] → getMin() = -2
push(0) → data: [-2, 0] min_stack: [-2, -2] → getMin() = -2
push(-3) → data: [-2,0,-3] min_stack: [-2,-2,-3]→ getMin() = -3
pop() → data: [-2, 0] min_stack: [-2, -2] → getMin() = -2
top() → 0
getMin() → -2
核心思路: 辅助栈同步记录每个时刻的最小值
思路分析
辅助栈法:
维护两个栈:data_stack 存数据,min_stack 存每个时刻的最小值。
push 时:data_stack 正常入栈,min_stack 入栈 min(当前值, min_stack栈顶)。
pop 时:两个栈同步出栈。
getMin:直接返回 min_stack 栈顶。
时间复杂度:所有操作 O(1),空间复杂度:O(n)
代码实现
class MinStack:
def __init__(self):
self.stack = []
self.min_stack = []
def push(self, val: int) -> None:
self.stack.append(val)
if self.min_stack:
self.min_stack.append(min(val, self.min_stack[-1]))
else:
self.min_stack.append(val)
def pop(self) -> None:
self.stack.pop()
self.min_stack.pop()
def top(self) -> int:
return self.stack[-1]
def getMin(self) -> int:
return self.min_stack[-1]
复杂度分析
时间复杂度:所有操作 O(1)
空间复杂度:O(n) — 辅助栈
空间优化:不用每个位置都存最小值,只在最小值变化时入栈。弹出时如果等于 min_stack 栈顶才弹 min_stack。这样 min_stack 大小可能远小于 n。
变体练习
- LC 716 最大栈(与最小栈对称,但 popMax 需要 O(n))
- LC 844 比较含退格的字符串(栈模拟退格)
🎯 题目三:用栈实现队列 (LC 232)
题目描述:请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty)。
双栈实现队列的原理:
栈A(输入栈) 栈B(输出栈)
┌───┐ ┌───┐
│ 3 │ │ │
├───┤ ├───┤
│ 2 │ │ │
├───┤ ├───┤
│ 1 │ │ │
└───┘ └───┘
push: 直接入栈A
pop: 如果栈B空,将栈A全部倒入栈B
栈A [1,2,3] → 倒入栈B → 栈B [3,2,1]
栈B.pop() = 1 ← 先入先出!
倒入过程实现了"反转的反转"→ FIFO
思路分析
双栈法:
栈 A 负责输入,栈 B 负责输出。
push:直接入栈 A。
pop/peek:如果栈 B 非空,直接操作栈 B;如果栈 B 为空,将栈 A 全部倒入栈 B 再操作。
关键:倒入操作"反转了反转",实现了 FIFO。
均摊时间复杂度:每个元素最多被 push/pop 两次,所以均摊 O(1)。
代码实现
class MyQueue:
def __init__(self):
self.in_stack = []
self.out_stack = []
def push(self, x: int) -> None:
self.in_stack.append(x)
def pop(self) -> int:
self._transfer()
return self.out_stack.pop()
def peek(self) -> int:
self._transfer()
return self.out_stack[-1]
def empty(self) -> bool:
return not self.in_stack and not self.out_stack
def _transfer(self):
if not self.out_stack:
while self.in_stack:
self.out_stack.append(self.in_stack.pop())
复杂度分析
push:O(1) | pop/peek:均摊 O(1)
空间复杂度:O(n)
均摊分析:每个元素最多经历 in_stack push → in_stack pop → out_stack push → out_stack pop,共 4 次操作。n 次 push 对应最多 4n 次栈操作,均摊每次 O(1)。
变体练习
- LC 225 用队列实现栈(对称问题)
- LC 150 逆波兰表达式求值(栈计算后缀表达式)
成就解锁:栈术师 — 掌握括号匹配、最小栈、双栈实现队列三大经典栈应用
LeetCode AC验证:LC 20 Valid Parentheses ✅ | LC 155 Min Stack ✅ | LC 232 Implement Queue using Stacks ✅
📝 课后练习
- LC 496 下一个更大元素 I(单调栈入门)
- LC 739 每日温度(单调栈经典)
- LC 150 逆波兰表达式求值
- LC 71 简化路径(栈处理路径)
- LC 225 用队列实现栈
🔑 本课要点回顾
- 栈 = LIFO,天然适合"最近匹配"问题
- 括号匹配:左括号入栈,右括号匹配栈顶
- 最小栈:辅助栈同步记录每时刻最小值,O(1) 查询
- 双栈实现队列:输入栈+输出栈,倒入实现 FIFO
- 均摊分析:每个元素最多 4 次栈操作,均摊 O(1)
- 单调栈是下一个重点,将在后续课程深入
思考题:如何用 O(1) 额外空间实现最小栈?提示:存储差值而非绝对值。每个元素存 val - current_min,通过差值恢复信息。
🔬 单调栈初探
单调栈是栈最重要的进阶应用,虽然完整讲解在后续课程,但这里先介绍其核心思想,为深入学习打下基础。
单调栈的核心思想:
问题: 找每个元素右边第一个比它大的元素
数组: [2, 1, 5, 6, 2, 3]
维护单调递减栈(从底到顶递减):
i=0(2): 栈空, 入栈 → [2]
i=1(1): 1<2, 入栈 → [2, 1]
i=2(5): 5>1, 弹1 → 1的答案是5
5>2, 弹2 → 2的答案是5
栈空, 入栈 → [5]
i=3(6): 6>5, 弹5 → 5的答案是6
入栈 → [6]
i=4(2): 2<6, 入栈 → [6, 2]
i=5(3): 3>2, 弹2 → 2的答案是3
3<6, 入栈 → [6, 3]
结果: [5, 5, 6, -, 3, -]
"-"表示右边没有比它大的元素
单调栈的核心:在弹出时处理答案。被弹出的元素找到了"右边第一个比它大的元素",就是当前遍历到的元素。这种"延迟处理"的思想非常优雅。
单调栈的时间复杂度也是 O(n)——每个元素最多入栈出栈各一次。这与滑动窗口的均摊分析是同样的思路。
思考题:如果要求"左边第一个比它大的元素"呢?提示:从左往右遍历时,栈中保留的元素就是"左边还没找到答案的"。栈顶就是左边最近的大元素。