# 树 *树是层次化数据结构,是文件系统、数据库、编译器和无数面试题背后的基础。本文件涵盖二叉树、二叉搜索树、平衡树、前缀树、线段树、树状数组和并查集,包括遍历模式、递归思维以及逐步增加难度的题目。* - **树**是一个连通的无环图(第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) — 通过并查集检测环