Files
maths-cs-ai-compendium-zh/chapter 14: data structures and algorithms/02. linked lists, stacks, and queues.md
T
flykhan 2536c937e3 feat: 完整中文翻译 maths-cs-ai-compendium(数学·计算机科学·AI 知识大全)
翻译自英文原版 maths-cs-ai-compendium,共 20 章全部完成。

第01章 向量 | 第02章 矩阵 | 第03章 微积分
第04章 统计学 | 第05章 概率论 | 第06章 机器学习
第07章 计算语言学 | 第08章 计算机视觉 | 第09章 音频与语音
第10章 多模态学习 | 第11章 自主系统 | 第12章 图神经网络
第13章 计算与操作系统 | 第14章 数据结构与算法
第15章 生产级软件工程 | 第16章 SIMD与GPU编程
第17章 AI推理 | 第18章 ML系统设计
第19章 应用人工智能 | 第20章 前沿人工智能

翻译说明:
- 所有数学公式 $...$ / $$...$$、代码块、图片引用完整保留
- mkdocs.yml 配置中文导航 + language: zh
- README.md 已翻译为中文(兼 docs/index.md)
- docs/ 目录包含指向各章文件的 symlink
- 约 29,000 行中文内容,排除 .cache/ 构建缓存
2026-05-03 10:23:20 +08:00

411 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 链表、栈和队列
*链表、栈和队列是更复杂数据结构的构建模块。本文件涵盖它们的运行机制,然后构建关键模式:快慢指针、单调栈和基于堆的优先队列,通过逐步增加难度的题目,并在每一步指出常见陷阱。*
- 数组提供了快速的随机访问但插入代价高。**链表**提供了快速插入但没有随机访问。**栈**和**队列**将访问限制在一端或两端,而正是这种限制使它们强大:通过限制你能做的事情,它们简化了你需要考虑的事情。
## 链表
- **单向链表**是一个节点链。每个节点存储一个值和一个指向下一个节点的指针。最后一个节点指向 `null`
```python
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
```
- **相对于数组的优势**:在已知位置插入或删除是 $O(1)$(只需重新指向指针)。无需移动元素。
- **劣势**:访问元素 $i$ 需要 $O(i)$ 次遍历(无随机访问)。缓存局部性差(节点分散在内存中)。
- **双向链表**增加了一个 `prev` 指针,支持向后遍历。用于 LRU 缓存(常数时间删除任何节点)和浏览器历史(前进/后退)。
| 操作 | 单向 | 双向 |
|-----------|--------|--------|
| 按索引访问 | $O(n)$ | $O(n)$ |
| 在头部插入 | $O(1)$ | $O(1)$ |
| 在尾部插入 | $O(n)$ 或 $O(1)$* | $O(1)$ |
| 删除给定节点 | $O(n)$** | $O(1)$ |
| 搜索 | $O(n)$ | $O(n)$ |
*有尾指针时。**需要前驱节点,需要遍历。
- **哨兵节点**(虚拟头/尾节点)简化了边界情况。没有虚拟头节点时,在头部插入或删除头部需要特殊代码。有了虚拟节点,每个真实节点都有前驱。
```python
# 无虚拟节点:头部删除需要特殊处理
def delete_head(head):
if not head:
return None
return head.next
# 有虚拟节点:统一逻辑
dummy = ListNode(0)
dummy.next = head
# 现在每次删除都是:prev.next = prev.next.next
```
- **陷阱**:忘记处理空列表(`head is None`)或单元素列表。始终测试这些边界情况。
---
## 模式:快慢指针(弗洛伊德算法)
- 使用两个以不同速度移动的指针来检测链表的属性。**慢**指针一次移动一步;**快**指针一次移动两步。
### 简单:环形链表
- **问题**:判断一个链表是否有环。
- **模式**:如果有环,快指针最终会追上慢指针(它们会相遇)。如果没有环,快指针会到达 `null`
```python
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
```
- **为什么有效**:如果环的长度为 $c$,快指针每步缩小1个节点的差距。它们必在慢指针进入环后的 $c$ 步内相遇。
- **陷阱**:检查 `fast and fast.next`(而不仅仅是 `fast.next`)。如果 `fast``None`,调用 `fast.next` 会崩溃。
### 中等:寻找链表的中间节点
- **问题**:返回中间节点。
- **模式**:当快指针到达末尾时,慢指针在中间。
```python
def find_middle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow # slow 在中间(偶数长度时为第二个中间节点)
```
### 中等:环形链表 II(寻找环的起点)
- **问题**:返回环开始的节点。
- **模式**:在快指针和慢指针相遇后,将一个指针重置到头部。两者以速度1移动。它们在环的起点相遇。
```python
def detect_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
# 将一个指针重置到头部
slow = head
while slow != fast:
slow = slow.next
fast = fast.next
return slow
return None
```
- **为什么有效**:设从头到环起点的距离为 $a$,从环起点到相遇点的距离为 $b$。慢指针走了 $a + b$。快指针走了 $2(a + b)$。差值为一整圈:$a + b = c$(环长)。所以 $a = c - b$:从头到环起点的距离等于从相遇点到环起点的距离(沿环向前走)。
### 困难:K个一组反转链表
- **问题**:反转链表中每 $k$ 个连续节点。
```python
def reverse_k_group(head, k):
# 检查是否还有 k 个节点
node = head
for _ in range(k):
if not node:
return head
node = node.next
# 反转 k 个节点
prev, curr = None, head
for _ in range(k):
nxt = curr.next
curr.next = prev
prev = curr
curr = nxt
# 当前 head 现在是反转后的组的尾节点
# 递归处理剩余部分
head.next = reverse_k_group(curr, k)
return prev # prev 是这组的新头节点
```
- **陷阱**:原地反转模式(`prev, curr, nxt`)值得记住。画出来:每一步,你将 `curr.next` 指回 `prev`,然后推进所有三个指针。顺序搞错会破坏链表。
---
## 栈
- **栈**是 LIFO(后进先出):最近添加的元素最先被移除。想象一堆盘子。
- 操作:`push(x)` 添加到顶部,`pop()` 从顶部移除,`peek()` 查看顶部不移除。全部 $O(1)$。
- 栈是**递归**(调用栈)、**表达式求值**(中缀转后缀)和**撤销操作**(每个操作被入栈,撤销时弹出最后一个)背后的隐式结构。
### 简单:有效的括号
- **问题**:给定一个由括号 `()[]{}` 组成的字符串,判断它们是否平衡。
- **模式**:将左括号入栈。当看到右括号时,检查栈顶是否匹配。
```python
def is_valid(s):
stack = []
matching = {')': '(', ']': '[', '}': '{'}
for char in s:
if char in matching:
if not stack or stack[-1] != matching[char]:
return False
stack.pop()
else:
stack.append(char)
return len(stack) == 0
```
- **陷阱**:忘记最后检查 `len(stack) == 0`。字符串 "(((" 中没有不匹配的情况,但因为没有闭合的括号,它是无效的。
---
## 模式:单调栈
- **单调栈**维护按排序顺序排列的元素(递增或递减)。当新元素会破坏顺序时,你弹出元素直到顺序恢复。
- **何时使用**:问题要求"对每个元素,找到下一个/上一个更大/更小的元素。"栈的总时间复杂度为 $O(n)$,因为每个元素最多被入栈和出栈一次。
### 中等:每日温度
- **问题**:给定每日温度,对于每一天,找到需要等多少天才会升温。
- **模式**:使用一个索引栈。当当前温度高于栈顶时,弹出并记录距离。
```python
def daily_temperatures(temperatures):
n = len(temperatures)
result = [0] * n
stack = [] # 索引栈,温度递减
for i in range(n):
while stack and temperatures[i] > temperatures[stack[-1]]:
prev = stack.pop()
result[prev] = i - prev
stack.append(i)
return result
```
- 每个元素被入栈一次,最多出栈一次:总计 $O(n)$。
- **陷阱**:在栈中存储索引(而非值)。你需要索引来计算距离。
### 困难:柱状图中最大的矩形
- **问题**:给定一个条形高度数组,找出最大矩形的面积。
- **模式**:对于每个条形,找出它可以向左右延伸多远(即,每侧最近的更短条形)。单调递增栈高效地追踪这个信息。
```python
def largest_rectangle(heights):
stack = [] # 索引栈,高度递增
max_area = 0
heights.append(0) # 哨兵,用于最后清空栈
for i, h in enumerate(heights):
start = i
while stack and stack[-1][1] > h:
idx, height = stack.pop()
max_area = max(max_area, height * (i - idx))
start = idx # 当前条形可以延伸到被弹出条形开始的位置
stack.append((start, h))
heights.pop() # 移除哨兵
return max_area
```
- **陷阱**`start = idx` 这行很微妙。当我们弹出一个比当前条形更高的条形时,当前条形可以向后延伸至被弹出条形开始的位置(因为中间的所有条形至少和被弹出条形一样高)。缺少这行会得到错误的面积。
- **陷阱**:哨兵 `heights.append(0)` 确保栈中所有剩余的条形被处理。没有它,那些右侧从未遇到更短条形的条形会被遗漏。
---
## 队列
- **队列**是 FIFO(先进先出):元素从后面添加,从前面移除。想象商店里排队。
- **双端队列**deque)支持在两端 $O(1)$ 插入和删除。Python 的 `collections.deque` 是标准实现。
- 队列是 **BFS**(广度优先搜索,第14章文件04)、**任务调度**和**消息传递**背后的结构。
### 简单:用栈实现队列
- **问题**:仅使用两个栈实现一个队列。
- **模式**:使用一个栈进行入队操作,一个栈进行出队操作。当出队栈为空时,将所有元素从入队栈转移到出队栈(反转顺序)。
```python
class MyQueue:
def __init__(self):
self.push_stack = []
self.pop_stack = []
def push(self, x):
self.push_stack.append(x)
def pop(self):
if not self.pop_stack:
while self.push_stack:
self.pop_stack.append(self.push_stack.pop())
return self.pop_stack.pop()
def peek(self):
if not self.pop_stack:
while self.push_stack:
self.pop_stack.append(self.push_stack.pop())
return self.pop_stack[-1]
def empty(self):
return not self.push_stack and not self.pop_stack
```
- 每次操作的平摊复杂度 $O(1)$:每个元素最多在两个栈之间移动一次。
---
## 优先队列和堆
- **优先队列**总是返回最小(或最大)的元素,不论插入顺序。标准实现是**二叉堆**。
- **最小堆**是一棵完全二叉树,其中每个父节点都小于其子节点。最小值总是在根节点。以数组形式存储:节点 $i$ 的子节点在位置 $2i + 1$ 和 $2i + 2$。
| 操作 | 时间 |
|-----------|------|
| 插入 | $O(\log n)$ |
| 获取最小值 | $O(1)$ |
| 提取最小值 | $O(\log n)$ |
| 从数组构建堆 | $O(n)$ |
- Python 的 `heapq` 模块提供了最小堆。对于最大堆,将值取反。
```python
import heapq
# 最小堆
h = []
heapq.heappush(h, 5)
heapq.heappush(h, 2)
heapq.heappush(h, 8)
print(heapq.heappop(h)) # 2(最小)
# 最大堆技巧:取反
heapq.heappush(h, -10)
print(-heapq.heappop(h)) # 10(最大)
```
### 中等:数组中的第 K 个最大元素
- **问题**:找到第 k 个最大的元素。
- **模式**:维护一个大小为 $k$ 的最小堆。堆的根节点就是第 k 大的元素。如果堆有 $k$ 个元素且新元素大于根节点,则替换根节点。
```python
import heapq
def find_kth_largest(nums, k):
heap = nums[:k]
heapq.heapify(heap) # O(k)
for num in nums[k:]:
if num > heap[0]:
heapq.heapreplace(heap, num) # 弹出最小值,推入 numO(log k)
return heap[0]
```
- $O(n \log k)$ 时间,$O(k)$ 空间。当 $k \ll n$ 时,这比排序($O(n \log n)$)好得多。
- **陷阱**:使用大小为 $n$ 的最大堆并弹出 $k$ 次也可行但较慢:$O(n + k \log n)$。大小为 $k$ 的最小堆是最优方法。
### 困难:合并 K 个排序链表
- **问题**:合并 $k$ 个已排序链表为一个排序链表。
- **模式**:使用一个包含每个链表头节点的最小堆。弹出最小的节点,将其添加到结果中,并将其下一个节点推入堆中。
```python
import heapq
def merge_k_lists(lists):
heap = []
for i, lst in enumerate(lists):
if lst:
heapq.heappush(heap, (lst.val, i, lst))
dummy = ListNode(0)
curr = dummy
while heap:
val, i, node = heapq.heappop(heap)
curr.next = node
curr = curr.next
if node.next:
heapq.heappush(heap, (node.next.val, i, node.next))
return dummy.next
```
- $O(n \log k)$,其中 $n$ 是总节点数。堆中最多有 $k$ 个元素。
- **陷阱**:堆元组中的 `i`(索引)是用于打破平局的。没有它,当值相等时 Python 会尝试比较 `ListNode` 对象,这会崩溃因为 `ListNode` 不支持 `<`。索确保了一有效的比较。
---
## 常见陷阱总结
| 陷阱 | 示例 | 修复 |
|---------|---------|-----|
| `fast.next` 上的空指针 | 循环检测中使用 `while fast.next` | 检查 `fast and fast.next` |
| 未处理空链表 | 反转 `None` | 添加 `if not head` 守卫 |
| 栈下溢 | 从空栈弹出 | 检查 `len(stack) > 0``if stack` |
| 忘记哨兵 | 直方图遗漏了最后的条形 | 追加 0 来清空栈 |
| 堆中缺少平局打破 | 比较不可比较的对象 | 向堆元组添加索引 |
| 遍历时修改链表 | 遍历时删除节点 | 使用 prev/curr 模式或虚拟头节点 |
---
## 课后练习题(NeetCode
### 链表
- [反转链表](https://neetcode.io/problems/reverse-a-linked-list) — 基础的原地反转
- [合并两个有序链表](https://neetcode.io/problems/merge-two-sorted-linked-lists) — 双指针合并
- [环形链表](https://neetcode.io/problems/linked-list-cycle-detection) — 快慢指针
- [重排链表](https://neetcode.io/problems/reorder-linked-list) — 找中间 + 反转 + 合并
- [删除链表的倒数第 N 个节点](https://neetcode.io/problems/remove-node-from-end-of-linked-list) — 间距为 $n$ 的双指针
- [LRU 缓存](https://neetcode.io/problems/lru-cache) — 哈希表 + 双向链表
### 栈
- [有效的括号](https://neetcode.io/problems/validate-parentheses) — 括号匹配
- [最小栈](https://neetcode.io/problems/minimum-stack) — 在每层跟踪最小值
- [逆波兰表达式求值](https://neetcode.io/problems/evaluate-reverse-polish-notation) — 基于栈的求值
- [每日温度](https://neetcode.io/problems/daily-temperatures) — 单调递减栈
- [柱状图中最大的矩形](https://neetcode.io/problems/largest-rectangle-in-histogram) — 单调递增栈
- [车队](https://neetcode.io/problems/car-fleet) — 带到达时间的栈
### 堆 / 优先队列
- [数据流中的第 K 大元素](https://neetcode.io/problems/kth-largest-integer-in-a-stream) — 大小为 $k$ 的最小堆
- [最后一块石头的重量](https://neetcode.io/problems/last-stone-weight) — 最大堆模拟
- [最接近原点的 K 个点](https://neetcode.io/problems/k-closest-points-to-origin) — 按距离排序的最小堆
- [任务调度器](https://neetcode.io/problems/task-scheduler) — 贪心 + 最大堆 + 冷却时间
- [数据流的中位数](https://neetcode.io/problems/find-median-in-a-data-stream) — 双堆(下半部分用最大堆,上半部分用最小堆)