栈、队列与堆:单调栈、优先队列与Top K问题

深入讲解栈与队列的实现原理与应用场景,详解单调栈在Next Greater Element中的应用,优先队列与堆的Top K问题解法,以及二叉堆的建堆与堆排序算法,配合代码实现与复杂度分析。

栈、队列与堆

栈(Stack)和队列(Queue)是最基础的线性数据结构,而堆(Heap)是实现优先队列的核心。这三者在面试中都有大量经典题目。

一、栈(Stack)

特性

后进先出(LIFO, Last In First Out)

push(1) push(2) push(3)
  ↓
[1, 2, 3] ← top

pop() → 3
pop() → 2

基础实现

class Stack:
    def __init__(self):
        self.items = []

    def push(self, x):
        self.items.append(x)

    def pop(self):
        if self.is_empty():
            raise IndexError("pop from empty stack")
        return self.items.pop()

    def peek(self):
        return self.items[-1] if not self.is_empty() else None

    def is_empty(self):
        return len(self.items) == 0

    def size(self):
        return len(self.items)

经典应用:括号匹配

def is_valid(s: str) -> bool:
    stack = []
    pairs = {')': '(', ']': '[', '}': '{'}
    for char in s:
        if char in pairs:
            if not stack or stack[-1] != pairs[char]:
                return False
            stack.pop()
        else:
            stack.append(char)
    return not stack

单调栈(Monotonic Stack)

维护栈内元素单调递增或递减,用于解决「下一个更大/更小元素」问题。

def next_greater_elements(nums):
    """找到每个元素右边第一个更大的元素"""
    n = len(nums)
    result = [-1] * n
    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

扩展:循环数组

def next_greater_elements_circular(nums):
    n = len(nums)
    result = [-1] * n
    stack = []
    for i in range(2 * n):  # 模拟循环
        idx = i % n
        while stack and nums[idx] > nums[stack[-1]]:
            result[stack.pop()] = nums[idx]
        if i < n:
            stack.append(idx)
    return result

二、队列(Queue)

特性

先进先出(FIFO, First In First Out)

基础实现

from collections import deque

class Queue:
    def __init__(self):
        self.items = deque()

    def enqueue(self, x):
        self.items.append(x)

    def dequeue(self):
        if self.is_empty():
            raise IndexError("dequeue from empty queue")
        return self.items.popleft()

    def front(self):
        return self.items[0] if not self.is_empty() else None

    def is_empty(self):
        return len(self.items) == 0

双端队列(Deque)

两端都可以插入删除,是更灵活的数据结构。

from collections import deque

d = deque()
d.append(1)      # 右侧入队
d.appendleft(2)  # 左侧入队
d.pop()          # 右侧出队
d.popleft()      # 左侧出队

应用:滑动窗口最大值(LeetCode 239)

def max_sliding_window(nums, k):
    result = []
    dq = deque()  # 存索引,维护递减
    for i, num in enumerate(nums):
        while dq and nums[dq[-1]] < num:
            dq.pop()
        dq.append(i)
        if dq[0] <= i - k:
            dq.popleft()
        if i >= k - 1:
            result.append(nums[dq[0]])
    return result

用栈实现队列 / 用队列实现栈

面试常考的设计题,考察对两种结构的理解。

# 用两个栈实现队列
class MyQueue:
    def __init__(self):
        self.stack_in = []
        self.stack_out = []

    def push(self, x):
        self.stack_in.append(x)

    def pop(self):
        self.peek()
        return self.stack_out.pop()

    def peek(self):
        if not self.stack_out:
            while self.stack_in:
                self.stack_out.append(self.stack_in.pop())
        return self.stack_out[-1]

    def empty(self):
        return not self.stack_in and not self.stack_out

三、堆(Heap)

二叉堆性质

  • 完全二叉树(适合数组存储)
  • 大根堆:父节点 ≥ 子节点
  • 小根堆:父节点 ≤ 子节点

数组表示

        10          index 0
       /  \
      8    9        index 1, 2
     / \   / \
    5  6  7  4      index 3, 4, 5, 6
  • 父节点 i 的左子节点:2i + 1
  • 父节点 i 的右子节点:2i + 2
  • 子节点 i 的父节点:(i - 1) // 2

堆化(Heapify)

def heapify_down(arr, n, i):
    largest = i
    left = 2 * i + 1
    right = 2 * i + 2
    if left < n and arr[left] > arr[largest]:
        largest = left
    if right < n and arr[right] > arr[largest]:
        largest = right
    if largest != i:
        arr[i], arr[largest] = arr[largest], arr[i]
        heapify_down(arr, n, largest)

def build_heap(arr):
    n = len(arr)
    for i in range(n // 2 - 1, -1, -1):
        heapify_down(arr, n, i)

建堆复杂度:O(n)(不是 O(n log n),可用等比数列证明)

堆排序

def heap_sort(arr):
    n = len(arr)
    build_heap(arr)
    for i in range(n - 1, 0, -1):
        arr[0], arr[i] = arr[i], arr[0]  # 最大值放到末尾
        heapify_down(arr, i, 0)
    return arr

复杂度:时间 O(n log n),空间 O(1)(原地排序)。

Top K 问题

思路:维护一个大小为 K 的小根堆,遍历数组。

import heapq

def top_k_largest(nums, k):
    heap = nums[:k]
    heapq.heapify(heap)
    for num in nums[k:]:
        if num > heap[0]:
            heapq.heapreplace(heap, num)
    return heap

复杂度:O(n log k),当 k « n 时优于 O(n log n) 的全排序。

变体:

  • 第 K 大:维护小根堆(堆顶是第 K 大)
  • 第 K 小:维护大根堆(Python 用负数模拟)
  • Top K 频率:哈希表计数 + 堆

四、经典面试题

题号题目考点难度
LeetCode 20有效的括号栈基础Easy
LeetCode 155最小栈辅助栈Medium
LeetCode 739每日温度单调栈Medium
LeetCode 232用栈实现队列设计Easy
LeetCode 225用队列实现栈设计Easy
LeetCode 239滑动窗口最大值单调队列Hard
LeetCode 215数组中的第K个最大元素快速选择/堆Medium
LeetCode 347前 K 个高频元素哈希 + 堆Medium
LeetCode 295数据流的中位数双堆Hard

五、面试常见问题

Q: 栈和队列的本质区别是什么?

  • 栈:LIFO,适合「回溯」「撤销」「表达式求值」
  • 队列:FIFO,适合「BFS」「缓冲」「任务调度」

Q: 为什么说堆适合找 Top K?
堆可以在 O(log k) 时间内维护一个固定大小的有序集合,总复杂度 O(n log k)。当 k 很小时,远优于排序的 O(n log n)。

Q: 实际工程中什么时候用堆?

  • 优先队列(任务调度)
  • 合并 K 个有序数组
  • 滑动窗口统计
  • 流式数据的 Top K(无法全量排序)

相关文章:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页