1. 二叉树分解问题概述
二叉树分解是算法与数据结构领域的经典问题类型,它要求将一个复杂的二叉树问题拆解为更小的子问题,通过递归或迭代的方式逐步解决。这种解题模式在LeetCode等编程题库中频繁出现,也是大厂面试的常考点。
我在刷题和实际工程中遇到过数十种二叉树分解问题,发现它们虽然表面各异,但核心思路高度一致。掌握这种模式后,面对诸如路径求和、子树判断、最近公共祖先等问题时,都能快速找到突破口。本文将分享我总结的通用解题框架和实战技巧。
2. 二叉树分解的核心思路
2.1 分治思想的应用
二叉树天然适合分治策略——每个节点的左右子树都是独立的子问题。以二叉树最大深度为例:
python复制def maxDepth(root):
if not root: # 基线条件
return 0
left_depth = maxDepth(root.left) # 分解左子树问题
right_depth = maxDepth(root.right) # 分解右子树问题
return max(left_depth, right_depth) + 1 # 合并结果
这个模板包含三个关键步骤:
- 基线条件(空节点处理)
- 分解子问题(递归处理左右子树)
- 合并结果(取最大值加1)
提示:90%的二叉树分解问题都遵循这个模式,区别仅在于第三步的合并逻辑。
2.2 后序遍历的妙用
后序遍历(左右根)与分治策略完美契合。我们来看LeetCode 124题(二叉树中的最大路径和)的解法:
python复制def maxPathSum(root):
res = -float('inf')
def dfs(node):
nonlocal res
if not node:
return 0
left = max(dfs(node.left), 0) # 左子树最大贡献
right = max(dfs(node.right), 0) # 右子树最大贡献
res = max(res, left + right + node.val) # 更新全局最大值
return max(left, right) + node.val # 返回当前节点最大贡献
dfs(root)
return res
这里有两个关键点:
- 子树贡献值可能为负,用max(..., 0)过滤
- 全局最大值可能跨越当前节点,需要单独维护
3. 常见问题类型与解法
3.1 路径类问题
路径问题通常需要跟踪路径上的状态。以LeetCode 437(路径总和III)为例:
python复制def pathSum(root, targetSum):
from collections import defaultdict
prefix = defaultdict(int)
prefix[0] = 1
res = 0
def dfs(node, curr_sum):
nonlocal res
if not node:
return
curr_sum += node.val
res += prefix[curr_sum - targetSum]
prefix[curr_sum] += 1
dfs(node.left, curr_sum)
dfs(node.right, curr_sum)
prefix[curr_sum] -= 1
dfs(root, 0)
return res
这个解法使用了前缀和技巧:
- 用哈希表记录路径前缀和
- 查找curr_sum - targetSum是否存在
- 回溯时清理状态
3.2 子树判断问题
判断子树结构时,通常需要辅助函数。LeetCode 572(另一棵树的子树)的解法:
python复制def isSubtree(root, subRoot):
def isSame(s, t):
if not s and not t:
return True
if not s or not t:
return False
return s.val == t.val and isSame(s.left, t.left) and isSame(s.right, t.right)
if not root:
return False
if isSame(root, subRoot):
return True
return isSubtree(root.left, subRoot) or isSubtree(root.right, subRoot)
这里采用双重递归:
- 外层递归遍历主树节点
- 内层递归比较子树结构
4. 高级技巧与优化
4.1 记忆化递归
当存在重复计算时,可以用记忆化优化。以LeetCode 337(打家劫舍III)为例:
python复制def rob(root):
memo = {}
def dfs(node):
if not node:
return 0
if node in memo:
return memo[node]
# 抢当前节点
val1 = node.val
if node.left:
val1 += dfs(node.left.left) + dfs(node.left.right)
if node.right:
val1 += dfs(node.right.left) + dfs(node.right.right)
# 不抢当前节点
val2 = dfs(node.left) + dfs(node.right)
memo[node] = max(val1, val2)
return memo[node]
return dfs(root)
记忆化将时间复杂度从O(2^n)降到O(n),空间复杂度O(n)。
4.2 迭代解法
虽然递归更直观,但迭代解法有时更高效。二叉树前序遍历的迭代实现:
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
迭代法的优势:
- 避免递归栈溢出
- 可以控制遍历顺序
- 某些场景下更易理解
5. 常见错误与调试技巧
5.1 空指针问题
二叉树问题最常见的错误是忘记处理空节点。建议在递归开始时统一处理:
python复制def traverse(root):
if not root: # 必须的防御性检查
return ...
# 正常处理逻辑
5.2 状态污染
当需要在递归中维护状态时(如路径和),要注意回溯:
python复制def pathSum(root, target):
path = []
res = []
def dfs(node):
if not node:
return
path.append(node.val) # 前进
# ...处理逻辑
dfs(node.left)
dfs(node.right)
path.pop() # 必须回溯
忘记pop()会导致路径状态错误。
5.3 重复计算识别
通过打印递归树可以发现重复计算:
python复制def dfs(node, indent=0):
print(" "*indent + f"Visit {node.val if node else 'None'}")
if not node:
return
dfs(node.left, indent+1)
dfs(node.right, indent+1)
如果同一节点多次出现,就需要考虑记忆化。
6. 实战问题解析
6.1 LeetCode 236(最近公共祖先)
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 or right
这个解法巧妙之处在于:
- 如果找到p或q就直接返回
- 如果左右子树都有返回值,当前节点就是LCA
- 否则返回非空的那一侧
6.2 LeetCode 114(二叉树展开为链表)
python复制def flatten(root):
if not root:
return
flatten(root.left)
flatten(root.right)
left = root.left
right = root.right
root.left = None
root.right = left
p = root
while p.right:
p = p.right
p.right = right
关键步骤:
- 递归处理左右子树
- 将左子树插入右子树位置
- 找到新右子树的末端接上原右子树
7. 模板代码与速查表
7.1 通用递归模板
python复制def solve(root):
# 1. 处理基线条件
if not root:
return ...
# 2. 递归解决子问题
left = solve(root.left)
right = solve(root.right)
# 3. 合并结果
result = ... # 根据具体问题实现
return result
7.2 常见问题速查
| 问题类型 | 关键技巧 | 时间复杂度 |
|---|---|---|
| 路径求和 | 前缀和+哈希表 | O(n) |
| 子树判断 | 双重递归 | O(mn) |
| 最近公共祖先 | 后序遍历+状态判断 | O(n) |
| 序列化/反序列化 | 前序遍历+特殊字符标记 | O(n) |
8. 扩展与变种问题
8.1 多叉树分解
二叉树思路可以推广到多叉树。以N叉树最大深度为例:
python复制def maxDepth(root):
if not root:
return 0
max_child = 0
for child in root.children:
max_child = max(max_child, maxDepth(child))
return max_child + 1
8.2 带父指针的二叉树
当节点包含parent指针时,可以优化某些问题。如查找两个节点的最近公共祖先:
python复制def lowestCommonAncestor(p, q):
a, b = p, q
while a != b:
a = a.parent if a else q
b = b.parent if b else p
return a
这种解法将空间复杂度降到O(1)。
经过多年实践,我发现二叉树分解问题的核心在于识别问题是否可以分解为子问题,以及如何正确合并结果。建议初学者从简单的最大深度、对称二叉树等问题入手,逐步过渡到更复杂的路径和、子树判断等问题。在面试中,清晰地解释你的分治思路往往比直接写代码更重要。