Chapter 04

栈与队列

受限的线性结构——单调栈、优先队列与双端队列的妙用

栈(Stack)

栈是后进先出(LIFO,Last In First Out)的线性结构。只能在一端(栈顶)进行插入和删除。

栈的示意(从下到上增长)
1 ← bottom
2
3 ← top (push/pop 在这里)
# Python 用 list 实现栈
stack = []
stack.append(1)    # push → [1]
stack.append(2)    # push → [1, 2]
stack.append(3)    # push → [1, 2, 3]
stack.pop()        # pop  → 3,剩余 [1, 2]
stack[-1]          # peek → 2(查看栈顶)

# 从链表实现栈(教学用)
class Stack:
    def __init__(self):
        self._data = []    # 实际用 list 即可

    def push(self, val): self._data.append(val)
    def pop(self):       return self._data.pop()
    def peek(self):      return self._data[-1]
    def is_empty(self):  return len(self._data) == 0
    def size(self):      return len(self._data)

应用:括号匹配

def is_valid(s: str) -> bool:
    stack = []
    pairs = {')': '(', ']': '[', '}': '{'}
    for ch in s:
        if ch in '([{':
            stack.append(ch)            # 左括号入栈
        elif not stack or stack[-1] != pairs[ch]:
            return False               # 右括号不匹配
        else:
            stack.pop()                 # 匹配成功,弹出
    return len(stack) == 0            # 栈空说明全部匹配

队列(Queue)

队列是先进先出(FIFO,First In First Out)的线性结构。从队尾入队,从队头出队。

from collections import deque

q = deque()
q.append(1)        # 入队(队尾)
q.append(2)
q.append(3)
q.popleft()        # 出队(队头)→ 1
q[0]               # 查看队头 → 2

# BFS 模板(图/树的层序遍历)
def bfs(start):
    queue = deque([start])
    visited = {start}
    while queue:
        node = queue.popleft()
        process(node)
        for neighbor in get_neighbors(node):
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)
💡

为什么用 deque 而不是 list?
list.pop(0) 是 O(n)(需移动所有元素),而 deque.popleft() 是 O(1)。BFS 中频繁出队,必须用 deque。

单调栈

单调栈是在普通栈的基础上,维护栈内元素单调递增或递减的性质。每个元素最多入栈出栈一次,总体 O(n)。

核心思路:当新元素入栈时,将所有"比它大(或比它小)"的栈顶元素弹出,同时这些被弹出的元素的"下一个更大/更小元素"就是当前新元素。

下一个更大元素

def next_greater_element(nums: list) -> list:
    n = len(nums)
    result = [-1] * n    # 默认 -1(右侧无更大元素)
    stack = []            # 单调递减栈,存下标

    for i in range(n):
        # 当前元素比栈顶大 → 栈顶找到了"下一个更大元素"
        while stack and nums[i] > nums[stack[-1]]:
            idx = stack.pop()
            result[idx] = nums[i]
        stack.append(i)

    return result

# 示例:[2, 1, 2, 4, 3]
# 结果:[4, 2, 4, -1, -1]

柱状图中最大的矩形

def largest_rectangle(heights: list) -> int:
    # 单调递增栈:找每根柱左右两侧第一个比它矮的柱
    stack = [-1]   # 哨兵,方便处理边界
    heights = heights + [0]  # 末尾加 0 强制清空栈
    max_area = 0

    for i, h in enumerate(heights):
        while stack[-1] != -1 and heights[stack[-1]] > h:
            height = heights[stack.pop()]
            width = i - stack[-1] - 1   # 宽度 = 当前位置 - 左边界 - 1
            max_area = max(max_area, height * width)
        stack.append(i)

    return max_area
# 时间 O(n),每个元素最多入栈出栈一次

优先队列(堆)

优先队列每次出队的是优先级最高的元素(通常是最小值或最大值)。Python 的 heapq 是最小堆。

import heapq

# 最小堆
heap = []
heapq.heappush(heap, 3)
heapq.heappush(heap, 1)
heapq.heappush(heap, 4)
heapq.heappop(heap)   # 返回 1(最小值)

# 最大堆:存负数模拟
max_heap = []
heapq.heappush(max_heap, -3)
heapq.heappush(max_heap, -1)
-heapq.heappop(max_heap)   # 返回 3(最大值)

# Top K 最大元素 — O(n log k)
def top_k_largest(nums: list, k: int) -> list:
    # 维护大小为 k 的最小堆
    # 堆中保存的是 k 个最大值,堆顶是其中最小的
    heap = nums[:k]
    heapq.heapify(heap)
    for x in nums[k:]:
        if x > heap[0]:
            heapq.heapreplace(heap, x)  # 弹出最小值,加入新元素
    return heap
heappush/popO(log n) heapifyO(n)

双端队列(Deque)

双端队列两端都可以 O(1) 进行入队和出队。可以同时模拟栈和队列。

滑动窗口最大值

from collections import deque

def max_sliding_window(nums: list, k: int) -> list:
    # 单调递减双端队列,存下标
    # 队列头部始终是当前窗口内最大值的下标
    dq = deque()
    result = []

    for i, x in enumerate(nums):
        # 移除窗口外的下标(队头)
        if dq and dq[0] < i - k + 1:
            dq.popleft()
        # 移除所有比当前元素小的下标(队尾),保持单调递减
        while dq and nums[dq[-1]] < x:
            dq.pop()
        dq.append(i)
        # 窗口形成后开始记录答案
        if i >= k - 1:
            result.append(nums[dq[0]])

    return result
# 时间 O(n),空间 O(k)

用两个栈实现队列

class MyQueue:
    def __init__(self):
        self.inbox = []    # 入队栈
        self.outbox = []   # 出队栈

    def push(self, val):
        self.inbox.append(val)

    def pop(self) -> int:
        self._transfer()
        return self.outbox.pop()

    def peek(self) -> int:
        self._transfer()
        return self.outbox[-1]

    def _transfer(self):
        # outbox 空时,将 inbox 所有元素倒入 outbox(逆转顺序)
        if not self.outbox:
            while self.inbox:
                self.outbox.append(self.inbox.pop())

# 摊还分析:每个元素最多入栈出栈各 2 次 → 摊还 O(1)

选择数据结构的依据
需要 LIFO → 栈(Python list)
需要 FIFO → 队列(collections.deque)
需要按优先级出队 → 堆(heapq)
需要维护区间最大/最小 → 单调双端队列
需要"下一个更大/更小元素" → 单调栈

表达式求值(栈的综合应用)

利用两个栈分别存储操作数和运算符,可以实现支持括号和优先级的四则运算表达式求值。

def calculate(s: str) -> int:
    """计算只含 +、-、( 、) 和空格的表达式(LeetCode 224)"""
    stack = []       # 存储数字和括号前的符号
    result = 0
    num = 0
    sign = 1       # 当前符号:+1 或 -1

    for ch in s:
        if ch.isdigit():
            num = num * 10 + int(ch)   # 处理多位数
        elif ch == '+':
            result += sign * num   # 将上一个数字加入结果
            num = 0
            sign = 1
        elif ch == '-':
            result += sign * num
            num = 0
            sign = -1
        elif ch == '(':
            stack.append(result)   # 将括号前的结果压栈
            stack.append(sign)     # 将括号前的符号压栈
            result = 0
            sign = 1
        elif ch == ')':
            result += sign * num
            num = 0
            result = stack.pop() * result + stack.pop()   # 恢复括号外的状态

    result += sign * num
    return result
⚠️

常见陷阱
1. 用 list.pop(0) 代替 deque.popleft():前者 O(n),BFS 时性能灾难性退化
2. 单调栈忘记处理末尾元素:循环结束后栈内可能还有未处理的元素
3. 优先队列中存储可变对象(如 list):heapq 比较时会比较整个对象,改用 (priority, counter, item) 格式避免歧义
4. 用两个栈实现队列时,pop/peek 触发 transfer 后 outbox 的顺序:确保只在 outbox 为空时才转移

📌

本章小结
栈和队列是受限的线性结构,正是这种"限制"使它们在特定场景下极其高效:
· 栈(LIFO):函数调用、括号匹配、DFS、表达式求值。Python 用 list。
· 队列(FIFO):BFS、层序遍历、任务调度。Python 用 deque(popleft O(1))。
· 单调栈:O(n) 解决"下一个更大/更小元素"、柱状图最大矩形。
· 单调双端队列:O(n) 解决滑动窗口最大/最小值。
· 优先队列(堆):O(log n) 的最值操作,Top-K 问题首选。
两个栈模拟队列:每个元素最多入栈出栈各 2 次,摊还时间 O(1)。