📚 栈 — 后进先出的智慧

栈是最简洁的数据结构之一,却是括号匹配、表达式求值、单调栈的基石

📖 栈的核心概念

栈(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 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 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)。

变体练习

成就解锁:栈术师 — 掌握括号匹配、最小栈、双栈实现队列三大经典栈应用
LeetCode AC验证:LC 20 Valid Parentheses ✅ | LC 155 Min Stack ✅ | LC 232 Implement Queue using Stacks ✅

📝 课后练习

  1. LC 496 下一个更大元素 I(单调栈入门)
  2. LC 739 每日温度(单调栈经典)
  3. LC 150 逆波兰表达式求值
  4. LC 71 简化路径(栈处理路径)
  5. LC 225 用队列实现栈

🔑 本课要点回顾

📚 扩展阅读

思考题:如何用 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)——每个元素最多入栈出栈各一次。这与滑动窗口的均摊分析是同样的思路。
思考题:如果要求"左边第一个比它大的元素"呢?提示:从左往右遍历时,栈中保留的元素就是"左边还没找到答案的"。栈顶就是左边最近的大元素。