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/ 构建缓存
This commit is contained in:
@@ -0,0 +1,410 @@
|
||||
# 链表、栈和队列
|
||||
|
||||
*链表、栈和队列是更复杂数据结构的构建模块。本文件涵盖它们的运行机制,然后构建关键模式:快慢指针、单调栈和基于堆的优先队列,通过逐步增加难度的题目,并在每一步指出常见陷阱。*
|
||||
|
||||
- 数组提供了快速的随机访问但插入代价高。**链表**提供了快速插入但没有随机访问。**栈**和**队列**将访问限制在一端或两端,而正是这种限制使它们强大:通过限制你能做的事情,它们简化了你需要考虑的事情。
|
||||
|
||||
## 链表
|
||||
|
||||
- **单向链表**是一个节点链。每个节点存储一个值和一个指向下一个节点的指针。最后一个节点指向 `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) # 弹出最小值,推入 num:O(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) — 双堆(下半部分用最大堆,上半部分用最小堆)
|
||||
Reference in New Issue
Block a user