1. 刷题背景与价值
最近在力扣(LeetCode)上集中刷了一波树相关的题目,时间跨度从2月26日到3月5日。作为程序员基本功的重要组成部分,树结构在面试中的出现频率居高不下。这段时间我系统性地梳理了二叉树、二叉搜索树、N叉树等各类树形结构的解题套路,收获颇丰。
树结构之所以重要,是因为它完美体现了递归思想和分治策略。在实际工程中,文件系统、数据库索引、路由算法等场景都大量运用了树的概念。通过这轮集中训练,我不仅巩固了基础算法,还发现了许多之前忽略的解题技巧。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 题目分类与解题策略
2.1 基础遍历问题
树的遍历是其他高级问题的基础。除了要熟练掌握前序、中序、后序的递归写法,更重要的是理解它们的迭代实现。比如用栈模拟递归过程时,后序遍历就需要特殊的标记法。
python复制# 后序遍历迭代写法示例
def postorderTraversal(root):
res = []
stack = [(root, False)]
while stack:
node, visited = stack.pop()
if node:
if visited:
res.append(node.val)
else:
stack.append((node, True))
stack.append((node.right, False))
stack.append((node.left, False))
return res
注意:迭代写法虽然代码量稍大,但在处理大型树时能避免递归深度过大导致的栈溢出问题。
2.2 二叉搜索树特性
BST(二叉搜索树)的题目往往利用其中序遍历有序的特性。例如第98题验证BST,常见错误是只检查当前节点与左右子节点的关系,而忽略了整个子树的边界条件。
python复制def isValidBST(root):
def helper(node, lower=float('-inf'), upper=float('inf')):
if not node:
return True
val = node.val
if val <= lower or val >= upper:
return False
return helper(node.left, lower, val) and helper(node.right, val, upper)
return helper(root)
2.3 路径和问题
这类问题通常需要同时跟踪路径信息和状态。以第112题路径总和为例,不仅需要判断是否存在路径,有时还需要记录具体路径(如第113题)。
python复制def hasPathSum(root, targetSum):
if not root:
return False
if not root.left and not root.right:
return targetSum == root.val
return hasPathSum(root.left, targetSum - root.val) or \
hasPathSum(root.right, targetSum - root.val)
3. 高频题型深度解析
3.1 最近公共祖先(LCA)
第236题LCA是经典面试题。递归解法利用了后序遍历的特性,当在左右子树分别找到目标节点时,当前节点就是LCA。
python复制def lowestCommonAncestor(root, p, q):
if not root or root == p or root == q:
return root
left = lowestCommonAncestor(root.left, p, q)
right = lowestCommonAncestor(root.right, p, q)
if left and right:
return root
return left if left else right
3.2 序列化与反序列化
第297题要求实现树的序列化。采用前序遍历配合特殊分隔符是最直观的方案。注意处理空指针的情况,否则无法保证反序列化的唯一性。
python复制def serialize(root):
def helper(node):
if node:
vals.append(str(node.val))
helper(node.left)
helper(node.right)
else:
vals.append('#')
vals = []
helper(root)
return ' '.join(vals)
def deserialize(data):
def helper():
val = next(vals)
if val == '#':
return None
node = TreeNode(int(val))
node.left = helper()
node.right = helper()
return node
vals = iter(data.split())
return helper()
3.3 树形DP问题
第337题打家劫舍III是典型的树形动态规划。需要在每个节点决策是否选择当前节点,同时考虑子节点的状态。
python复制def rob(root):
def helper(node):
if not node:
return (0, 0)
left = helper(node.left)
right = helper(node.right)
rob = node.val + left[1] + right[1]
not_rob = max(left) + max(right)
return (rob, not_rob)
return max(helper(root))
4. 刷题技巧与优化策略
4.1 递归转迭代的通用方法
虽然递归代码简洁,但面试时常被要求写出迭代版本。通用方法是使用栈模拟调用过程,显式维护调用状态。对于前序遍历:
python复制def preorderTraversal(root):
res = []
stack = [root]
while stack:
node = stack.pop()
if node:
res.append(node.val)
stack.append(node.right)
stack.append(node.left)
return res
4.2 空间复杂度优化
某些问题可以通过修改树结构来避免额外空间。例如Morris遍历能在O(1)空间完成中序遍历,利用叶子节点的空指针临时存储信息。
python复制def morrisInorder(root):
res = []
curr = root
while curr:
if not curr.left:
res.append(curr.val)
curr = curr.right
else:
pre = curr.left
while pre.right and pre.right != curr:
pre = pre.right
if not pre.right:
pre.right = curr
curr = curr.left
else:
pre.right = None
res.append(curr.val)
curr = curr.right
return res
4.3 测试用例设计
有效的测试用例应该包括:
- 空树
- 单节点树
- 完全二叉树
- 退化成链表的树
- 随机生成的树
例如验证BST时,特别要测试这种case:
code复制 5
/ \
1 6
/ \
3 7
虽然每个节点的直接子节点满足BST条件,但3<5不满足全局性质。
5. 常见错误与调试技巧
5.1 指针修改问题
在处理树结构时,容易在递归过程中意外修改指针。比如在删除节点时,应该先递归处理子树,再决定是否删除当前节点。
python复制def deleteNode(root, key):
if not root:
return None
if key < root.val:
root.left = deleteNode(root.left, key)
elif key > root.val:
root.right = deleteNode(root.right, key)
else:
if not root.left:
return root.right
if not root.right:
return root.left
min_node = findMin(root.right)
root.val = min_node.val
root.right = deleteNode(root.right, root.val)
return root
5.2 边界条件处理
树的题目特别容易在边界条件上出错。比如计算树高度时,空树应该返回0还是-1?不同题目可能有不同约定,需要仔细阅读题目描述。
python复制def maxDepth(root):
if not root:
return 0 # 或者-1,取决于题目定义
return 1 + max(maxDepth(root.left), maxDepth(root.right))
5.3 重复计算问题
在递归过程中,如果不加注意可能会重复计算相同子树。例如计算平衡二叉树时,可以通过后序遍历同时计算高度和平衡性。
python复制def isBalanced(root):
def helper(node):
if not node:
return 0, True
left_height, left_balanced = helper(node.left)
right_height, right_balanced = helper(node.right)
balanced = left_balanced and right_balanced and abs(left_height - right_height) <= 1
return max(left_height, right_height) + 1, balanced
return helper(root)[1]
6. 进阶题目推荐
完成基础题目后,可以挑战这些进阶题:
-
- 二叉树中的最大路径和
-
- 恢复二叉搜索树
-
- 完全二叉树的节点个数
-
- 从前序与中序遍历序列构造二叉树
-
- 从先序遍历还原二叉树
特别是第124题,需要理解路径的多种可能性:
python复制def maxPathSum(root):
res = -float('inf')
def helper(node):
nonlocal res
if not node:
return 0
left = max(0, helper(node.left))
right = max(0, helper(node.right))
res = max(res, left + right + node.val)
return max(left, right) + node.val
helper(root)
return res
7. 刷题计划建议
根据我的经验,有效的树专题训练应该分三个阶段:
- 基础阶段(2天):掌握各种遍历方式及其变种
- 强化阶段(3天):攻克BST、LCA、序列化等经典问题
- 冲刺阶段(2天):解决树形DP、Morris遍历等难题
每天建议安排:
- 上午:学习2-3种题型,理解模板代码
- 下午:独立完成5-8道相关题目
- 晚上:复盘错题,整理解题模板
使用表格记录进度是个好方法:
| 日期 | 完成题目 | 掌握程度 | 待改进点 |
|---|---|---|---|
| 2.26 | 94,144,145 | 熟练 | 迭代写法不熟 |
| 2.27 | 98,101,104 | 掌握 | BST边界条件 |
| ... | ... | ... | ... |
8. 工程实践中的树结构
虽然刷题主要关注算法,但了解树在实际项目中的应用也很重要:
- 文件系统:目录树结构
- 数据库:B+树索引
- 网络:路由表的前缀树
- UI框架:DOM树渲染
例如实现一个简单的文件系统树:
python复制class FileNode:
def __init__(self, name, is_file=False):
self.name = name
self.is_file = is_file
self.children = []
def find_file(root, path):
nodes = path.split('/')
curr = root
for name in nodes:
if not name: continue
found = None
for child in curr.children:
if child.name == name:
found = child
break
if not found:
return None
curr = found
return curr if curr.is_file else None
9. 可视化调试技巧
对于复杂的树问题,画图能极大帮助理解。可以:
- 手工绘制小规模的树示例
- 使用在线工具可视化树结构
- 打印树的层级结构
打印树结构的代码示例:
python复制def print_tree(root):
levels = []
def helper(node, level):
if len(levels) == level:
levels.append([])
levels[level].append(str(node.val) if node else '#')
if node:
helper(node.left, level+1)
helper(node.right, level+1)
helper(root, 0)
for i, level in enumerate(levels):
print(f'Level {i}:', ' '.join(level))
10. 性能分析与优化
当树问题出现TLE时,可以考虑:
- 剪枝:提前终止不必要的递归
- 记忆化:缓存重复计算结果
- 迭代替代递归:减少函数调用开销
- 利用树的性质:如BST的排序特性
例如在查找LCA时增加剪枝:
python复制def lowestCommonAncestor(root, p, q):
result = None
def helper(node):
nonlocal result
if not node or result:
return False
left = helper(node.left)
right = helper(node.right)
mid = node == p or node == q
if mid + left + right >= 2:
result = node
return mid or left or right
helper(root)
return result
经过这轮集中刷题,我对树结构的理解更加深入了。最大的收获是建立了系统的解题框架:先判断问题类型(遍历、搜索、构造等),再选择合适的解法模板,最后处理边界条件。这种结构化思维比单纯记忆解法要有效得多。
