1. 题目背景与核心问题
LeetCode 437题"路径总和 III"是二叉树类问题中的经典题目,主要考察对树结构的遍历和路径统计能力。题目要求:给定一个二叉树的根节点和一个目标和,找出路径和等于给定值的路径数量。这里的路径不需要从根节点开始,也不需要在叶子节点结束,但必须保证路径方向是向下的(只能从父节点到子节点)。
这道题在面试中出现频率较高,因为它能同时考察候选人对递归、深度优先搜索(DFS)以及前缀和等算法的掌握程度。我在第一次遇到这个问题时,花了近两小时才找到最优解,后来通过反复练习和总结,终于形成了系统的解题思路。
2. 基础解法:双重递归分析
2.1 暴力递归思路
最直观的解法是使用双重递归:
- 外层递归遍历每个节点
- 内层递归计算以当前节点为起点的所有路径和
def pathSum(root, targetSum): if not root: return 0 def dfs(node, current_sum): if not node: return 0 current_sum += node.val count = 1 if current_sum == targetSum else 0 return count + dfs(node.left, current_sum) + dfs(node.right, current_sum) return dfs(root, 0) + pathSum(root.left, targetSum) + pathSum(root.right, targetSum)这种解法的时间复杂度是O(n²),对于每个节点都要遍历其所有子节点。在树退化为链表的最坏情况下,性能会急剧下降。
2.2 复杂度优化思考
暴力解法的主要问题在于存在大量重复计算。比如当我们计算完节点A的所有子路径后,在计算节点A的父节点时,又会重复计算A及其子节点的路径。这种重复计算导致了不必要的性能损耗。
3. 最优解:前缀和+哈希表
3.1 前缀和概念引入
前缀和技巧通常用于数组问题,但同样适用于树结构。核心思想是:记录从根节点到当前节点的路径和,然后通过哈希表快速查找是否存在满足条件的子路径。
具体来说:
- 维护一个哈希表prefix_sum_count,记录路径和出现的次数
- 当前路径和curr_sum减去targetSum的值如果在哈希表中存在,说明存在符合条件的子路径
- 回溯时需要及时清理哈希表,避免影响其他路径的计算
3.2 代码实现与解析
def pathSum(root, targetSum): from collections import defaultdict prefix_sum_count = defaultdict(int) prefix_sum_count[0] = 1 # 初始状态:和为0出现1次 def dfs(node, curr_sum): if not node: return 0 curr_sum += node.val # 查找是否有满足条件的子路径 res = prefix_sum_count.get(curr_sum - targetSum, 0) # 更新当前路径和的出现次数 prefix_sum_count[curr_sum] += 1 # 递归处理左右子树 res += dfs(node.left, curr_sum) res += dfs(node.right, curr_sum) # 回溯,恢复状态 prefix_sum_count[curr_sum] -= 1 return res return dfs(root, 0)这个算法的时间复杂度降到了O(n),因为每个节点只需要访问一次。空间复杂度主要取决于哈希表的大小和递归栈的深度,最坏情况下也是O(n)。
4. 关键细节与边界条件
4.1 哈希表初始化
prefix_sum_count[0] = 1这一初始化非常重要。它表示在路径开始前,和为0的路径已经出现过1次。这样当从根节点开始的路径和正好等于targetSum时,我们就能正确计数。
4.2 回溯处理
在递归返回前必须执行prefix_sum_count[curr_sum] -= 1,这是为了保证在计算不同分支时不会互相干扰。比如左子树计算完成后,在计算右子树时,左子树的路径和不应该被计入。
4.3 数值范围考虑
题目没有限制节点值的范围,如果节点值很大,curr_sum可能会溢出。但在Python中整数不会溢出,所以不需要特别处理。如果用其他语言实现,可能需要考虑使用长整型。
5. 测试用例设计
5.1 基础测试用例
# 用例1:简单二叉树 # 10 # / \ # 5 -3 # / \ \ # 3 2 11 # 目标值8,预期结果35.2 特殊边界用例
# 用例2:空树 # 目标值任意,预期结果0 # 用例3:单节点树 # 节点值5,目标值5,预期结果1 # 用例4:所有节点值相同 # 2 # / \ # 2 2 # 目标值2,预期结果35.3 复杂用例
# 用例5:路径和可能多次出现 # 5 # / \ # 4 8 # / / \ # 11 13 4 # / \ / \ # 7 2 5 1 # 目标值22,预期结果36. 常见错误与调试技巧
6.1 忘记回溯
这是最常见的错误之一。如果在递归返回前没有及时清理哈希表中的记录,会导致后续计算错误。可以通过打印哈希表内容来验证回溯是否正确。
6.2 初始化错误
忘记初始化prefix_sum_count[0] = 1会导致漏计从根节点开始的满足条件的路径。可以通过测试单节点树来验证。
6.3 路径方向混淆
题目要求路径必须是向下的,有些解法可能会错误地统计了从子节点到父节点的路径。可以通过构造不对称的树来验证。
7. 算法扩展与变种
7.1 输出所有路径
如果题目要求输出所有满足条件的路径而不仅仅是计数,我们可以修改算法,用哈希表记录路径和对应的节点列表,然后在找到满足条件的路径时,输出对应的节点序列。
7.2 路径必须从根到叶
这是路径总和问题的经典版本(LeetCode 112题)。相比本题,这个变种更简单,因为路径的起点和终点都固定了。
7.3 路径方向不限
如果取消路径必须是向下这一限制,问题会变得更加复杂,可能需要将树转换为图来处理。这种情况在实际面试中较少出现。
8. 个人刷题心得
这道题我前后做了5遍才完全掌握。最大的收获是理解了前缀和技巧不仅适用于数组,也可以灵活应用于树结构。在实际编码时,有几点特别需要注意:
画图辅助理解:对于树问题,画出具体的树结构并手动计算路径和,能帮助理解算法的执行过程。
分步验证:先实现暴力解法,确保理解题意;再逐步优化到前缀和解法,这样能更好地理解优化点在哪里。
边界测试:特别注意空树、单节点树、所有节点值相同等特殊情况,这些往往是算法出问题的地方。
回溯清理:这是最容易出错的地方,可以在递归进入和返回时打印当前状态,帮助定位问题。
经过多次练习后,我现在能在15分钟内写出无bug的前缀和解法。这道题的解题思路也帮助我解决了其他类似的问题,比如"和为K的子数组"等。