# 树

*树是层次化数据结构，是文件系统、数据库、编译器和无数面试题背后的基础。本文件涵盖二叉树、二叉搜索树、平衡树、前缀树、线段树、树状数组和并查集，包括遍历模式、递归思维以及逐步增加难度的题目。*

- **树**是一个连通的无环图（第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) — 通过并查集检测环
