Files
maths-cs-ai-compendium-zh/chapter 14: data structures and algorithms/03. trees.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

382 lines
14 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.
# 树
*树是层次化数据结构,是文件系统、数据库、编译器和无数面试题背后的基础。本文件涵盖二叉树、二叉搜索树、平衡树、前缀树、线段树、树状数组和并查集,包括遍历模式、递归思维以及逐步增加难度的题目。*
- **树**是一个连通的无环图(第13章)。最重要的变体是**二叉树**:每个节点最多有两个子节点(左和右)。树无处不在:编译器中的解析树、浏览器中的 DOM 树、机器学习中的决策树以及数据库中的 B 树。
- 解决树问题的关键洞察:**大多数树问题都可以递归解决**。结构是递归的(树是一个根节点加上两棵子树),因此解法也应是递归的。掌握"解决左子树、解决右子树、合并结果"的模式,你就能解决大多数树问题。
## 二叉树遍历
- 有四种标准的访问每个节点的方式:
- **中序遍历**(左、根、右):对于 BST,这会按排序顺序访问节点。
- **前序遍历**(根、左、右):用于序列化和复制树。
- **后序遍历**(左、右、根):用于删除和计算大小。
- **层序遍历**(BFS):使用队列逐层访问节点。
```python
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def inorder(root):
if not root:
return []
return inorder(root.left) + [root.val] + inorder(root.right)
def preorder(root):
if not root:
return []
return [root.val] + preorder(root.left) + preorder(root.right)
def postorder(root):
if not root:
return []
return postorder(root.left) + postorder(root.right) + [root.val]
from collections import deque
def level_order(root):
if not root:
return []
result, queue = [], deque([root])
while queue:
level = []
for _ in range(len(queue)):
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(level)
return result
```
- **陷阱**:上面的递归遍历在每一步都创建新列表(由于 `+` 拼接),这是 $O(n^2)$。为了效率,传递一个结果列表并原地追加:
```python
def inorder_efficient(root, result=None):
if result is None:
result = []
if root:
inorder_efficient(root.left, result)
result.append(root.val)
inorder_efficient(root.right, result)
return result
```
### 简单:二叉树的最大深度
```python
def max_depth(root):
if not root:
return 0
return 1 + max(max_depth(root.left), max_depth(root.right))
```
- **递归模式**:基本情况(null → 0),递归子节点,合并(1 + max)。同样的模式适用于数十种树问题。
### 简单:翻转二叉树
```python
def invert_tree(root):
if not root:
return None
root.left, root.right = invert_tree(root.right), invert_tree(root.left)
return root
```
### 中等:二叉树的最近公共祖先
- **问题**:找到既是 $p$ 又是 $q$ 的祖先的最低节点。
- **模式**:如果 $p$ 和 $q$ 都在左子树中,则 LCA 在左子树中。如果都在右子树中,则在右子树中。如果它们分开了(一个在左,一个在右),则当前节点就是 LCA。
```python
def lowest_common_ancestor(root, p, q):
if not root or root == p or root == q:
return root
left = lowest_common_ancestor(root.left, p, q)
right = lowest_common_ancestor(root.right, p, q)
if left and right:
return root # p 和 q 在不同子树中
return left if left else right
```
- **陷阱**:这假设 $p$ 和 $q$ 都在树中。如果它们可能不在,你需要额外的检查。
### 困难:二叉树中的最大路径和
- **问题**:找出任意两个节点之间的最大路径和(路径不需要经过根节点)。
```python
def max_path_sum(root):
best = [float('-inf')]
def dfs(node):
if not node:
return 0
left = max(dfs(node.left), 0) # 忽略负路径
right = max(dfs(node.right), 0)
# 经过当前节点的路径(可能作为"转弯点")
best[0] = max(best[0], node.val + left + right)
# 返回到父节点的最大增益
return node.val + max(left, right)
dfs(root)
return best[0]
```
- **关键洞察**:在每个节点,有两个问题:(1) *经过*这个节点的最佳路径是什么(左 + 节点 + 右)?(2) 这个节点可以贡献给其*父节点*的最佳路径是什么(节点 + max(左, 右),因为路径不能在两个层级分叉)?混淆这两者是最常见的错误。
## 二叉搜索树(BST
- **BST** 满足:对于每个节点,左子树中的所有值都较小,右子树中的所有值都较大。这实现了 $O(\log n)$ 的搜索、插入和删除(当平衡时)。
```python
def search_bst(root, target):
if not root:
return None
if target < root.val:
return search_bst(root.left, target)
elif target > root.val:
return search_bst(root.right, target)
else:
return root
def insert_bst(root, val):
if not root:
return TreeNode(val)
if val < root.val:
root.left = insert_bst(root.left, val)
else:
root.right = insert_bst(root.right, val)
return root
```
- **陷阱**:BST 操作仅在树平衡时才是 $O(\log n)$。由已排序插入构建的 BST 退化为链表:每次操作 $O(n)$。这就是平衡 BST(AVL、红黑树)存在的原因。
### 中等:验证二叉搜索树
```python
def is_valid_bst(root, lo=float('-inf'), hi=float('inf')):
if not root:
return True
if root.val <= lo or root.val >= hi:
return False
return (is_valid_bst(root.left, lo, root.val) and
is_valid_bst(root.right, root.val, hi))
```
- **陷阱**:只检查 `left.val < root.val < right.val` 是错误的。约束条件是左子树中*所有*节点都更小,而不仅仅是直接子节点。`lo`/`hi` 边界将这个约束向下传递。
### 中等:二叉搜索树中第 K 小的元素
- **模式**:BST 的中序遍历按排序顺序访问节点。访问的第 $k$ 个节点就是答案。
```python
def kth_smallest(root, k):
count = [0]
result = [None]
def inorder(node):
if not node or result[0] is not None:
return
inorder(node.left)
count[0] += 1
if count[0] == k:
result[0] = node.val
return
inorder(node.right)
inorder(root)
return result[0]
```
## 前缀树(Trie
- **前缀树**逐字符地将字符串存储在树中。每条边代表一个字符,从根到标记节点的路径代表存储的字符串。前缀树实现了 $O(L)$ 的查找,其中 $L$ 是字符串长度,无论存储了多少个字符串。
```python
class TrieNode:
def __init__(self):
self.children = {}
self.is_end = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end = True
def search(self, word):
node = self.root
for char in word:
if char not in node.children:
return False
node = node.children[char]
return node.is_end
def starts_with(self, prefix):
node = self.root
for char in prefix:
if char not in node.children:
return False
node = node.children[char]
return True
```
- **何时使用**:自动补全、拼写检查、单词游戏、IP 路由表。每当你需要基于前缀的操作时。
### 困难:单词搜索 II
- **问题**:给定一个字符板和一个单词列表,找出所有可以通过遍历相邻单元格形成的单词。
- **模式**:从单词列表构建一个前缀树,然后从每个单元格使用前缀树进行 DFS,尽早剪枝分支(如果没有单词以当前前缀开头,则停止)。
- **陷阱**:没有前缀树的话,你需要为每个单词单独进行 DFS$O(w \cdot m \cdot n \cdot 4^L)$。前缀树跨单词共享前缀计算,大幅减少了工作量。
## 并查集(不相交集合)
- **并查集**跟踪一组不相交集合。两个操作:`find(x)` 返回 $x$ 所在集合的代表元,`union(x, y)` 合并包含 $x$ 和 $y$ 的集合。
```python
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
self.count = n # 连通分量数
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # 路径压缩
return self.parent[x]
def union(self, x, y):
rx, ry = self.find(x), self.find(y)
if rx == ry:
return False # 已经连通
# 按秩合并
if self.rank[rx] < self.rank[ry]:
rx, ry = ry, rx
self.parent[ry] = rx
if self.rank[rx] == self.rank[ry]:
self.rank[rx] += 1
self.count -= 1
return True
```
- 通过路径压缩和按秩合并,两个操作都是平摊 $O(\alpha(n)) \approx O(1)$(反阿克曼函数,实际上是常数)。
- **何时使用**:连通分量、无向图中的环检测、Kruskal 最小生成树、分组等价项。
### 中等:连通分量数量
```python
def count_components(n, edges):
uf = UnionFind(n)
for u, v in edges:
uf.union(u, v)
return uf.count
```
### 中等:冗余连接
- **问题**:找出从图中移除后使图成为树的那条边(即,创建环的那条边)。
- **模式**:逐一处理边。第一条两个端点已经在同一分量中的边就是创建环的边。
```python
def find_redundant(edges):
uf = UnionFind(len(edges) + 1)
for u, v in edges:
if not uf.union(u, v):
return [u, v] # 已经连通 → 这条边创建了环
```
## 线段树和树状数组
- **线段树**支持区间查询(子数组上的和、最小值、最大值)和单点更新,两者都是 $O(\log n)$。
- **树状数组**(二叉索引树)是前缀和查询和单点更新的更简单、更快的替代方案。它使用一种巧妙的位操作技巧:每个位置存储一个部分和,覆盖范围由最低设置位决定。
```python
class FenwickTree:
def __init__(self, n):
self.n = n
self.tree = [0] * (n + 1)
def update(self, i, delta):
i += 1 # 1-indexed
while i <= self.n:
self.tree[i] += delta
i += i & (-i) # 加上最低设置位
def prefix_sum(self, i):
i += 1
total = 0
while i > 0:
total += self.tree[i]
i -= i & (-i) # 移除最低设置位
return total
def range_sum(self, l, r):
return self.prefix_sum(r) - (self.prefix_sum(l - 1) if l > 0 else 0)
```
- **何时使用**:需要带更新的重复区间查询的问题。当你只需要前缀和时首选树状数组;当你需要任意区间操作(最小值、最大值、GCD)时使用线段树。
---
## 常见陷阱总结
| 陷阱 | 示例 | 修复 |
|---------|---------|-----|
| BST 只检查直接子节点 | `left.val < root.val` 遗漏了深层违规 | 传递 `lo`/`hi` 边界 |
| 递归中 $O(n^2)$ 列表拼接 | `inorder(left) + [val] + inorder(right)` | 追加到共享列表 |
| 忘记基本情况 | 空树上的无限递归 | `if not root: return` |
| 混淆经过路径和到父节点的路径 | 最大路径和:在两个层级分叉 | 向父节点返回单分支,单独跟踪双分支 |
| 树状数组 1-indexed vs 0-indexed | 树数组中的差一错误 | 入口处始终 `i += 1` |
| 并查集没有路径压缩 | 最坏情况下每次 `find` 是 $O(n)$ | `self.parent[x] = self.find(self.parent[x])` |
---
## 课后练习题(NeetCode
### 二叉树模式
- [翻转二叉树](https://neetcode.io/problems/invert-a-binary-tree) — 基础递归
- [二叉树的最大深度](https://neetcode.io/problems/depth-of-binary-tree) — 递归深度
- [相同的树](https://neetcode.io/problems/same-binary-tree) — 同步遍历
- [另一棵树的子树](https://neetcode.io/problems/subtree-of-a-binary-tree) — 嵌套递归
- [二叉树的层序遍历](https://neetcode.io/problems/level-order-traversal-of-binary-tree) — 带层级跟踪的 BFS
- [二叉树中的最大路径和](https://neetcode.io/problems/binary-tree-maximum-path-sum) — 带全局最优的 DFS
- [序列化与反序列化二叉树](https://neetcode.io/problems/serialize-and-deserialize-binary-tree) — 前序遍历 + null 标记
### BST 模式
- [验证二叉搜索树](https://neetcode.io/problems/valid-binary-search-tree) — 边界传播
- [二叉搜索树中第 K 小的元素](https://neetcode.io/problems/kth-smallest-integer-in-bst) — 中序遍历
- [二叉搜索树的最近公共祖先](https://neetcode.io/problems/lowest-common-ancestor-in-binary-search-tree) — 利用 BST 排序性质
### 前缀树
- [实现 Trie](https://neetcode.io/problems/implement-prefix-tree) — 基础前缀树操作
- [设计添加和搜索单词](https://neetcode.io/problems/design-word-search-data-structure) — 前缀树 + 带通配符的 DFS
- [单词搜索 II](https://neetcode.io/problems/search-for-word-ii) — 前缀树引导的回溯
### 并查集
- [连通分量数量](https://neetcode.io/problems/count-connected-components) — 基础并查集
- [冗余连接](https://neetcode.io/problems/redundant-connection) — 通过并查集检测环