1. 问题背景与核心需求
第一次看到LeetCode 112题时,我正坐在星巴克调试一个二叉树遍历的bug。这道看似简单的题目其实藏着不少门道——给定一个二叉树和一个目标值,判断是否存在从根节点到叶子节点的路径,使得路径上所有节点值相加等于目标值。
这个问题的经典解法通常采用深度优先搜索(DFS),但实际面试中,面试官往往会追问:"如果树有100万节点怎么办?"、"能不用递归实现吗?"、"时间复杂度的常数项如何优化?" 这些问题暴露出单纯背诵题解的局限性。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 递归解法:教科书式的起点
2.1 基础递归实现
我们先从最直观的递归解法开始。递归的核心思想是把大问题分解为子问题:
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)
这个实现简洁明了,但有几个关键细节需要注意:
- 空树直接返回False(边界条件)
- 只有在叶子节点才进行最终判断
- 每层递归都减去当前节点值,避免维护额外变量
提示:面试时一定要先处理边界条件,这是面试官考察代码严谨性的重点。
2.2 递归的时空复杂度分析
递归解法的时间复杂度是O(N),因为每个节点只访问一次。空间复杂度则取决于递归深度:
- 最好情况(完全平衡树):O(logN)
- 最坏情况(退化成链表):O(N)
在实际运行中,Python的递归深度默认限制为1000层,这意味着对于深度超过1000的树,这个解法会抛出RecursionError。这是递归解法的重要局限。
3. 迭代解法:突破递归限制
3.1 使用栈的DFS实现
为了解决递归深度限制,我们可以用显式栈实现DFS:
python复制def hasPathSum(root, targetSum):
if not root:
return False
stack = [(root, targetSum - root.val)]
while stack:
node, curr_sum = stack.pop()
if not node.left and not node.right and curr_sum == 0:
return True
if node.right:
stack.append((node.right, curr_sum - node.right.val))
if node.left:
stack.append((node.left, curr_sum - node.left.val))
return False
这个实现有几个优化点:
- 栈中存储的是(node, remaining_sum)元组
- 右子节点先入栈(保证左子节点先处理)
- 提前计算剩余值,减少重复运算
3.2 迭代法的性能考量
迭代解法的优势在于:
- 不受递归深度限制
- 可以处理超大规模树结构
- 更容易添加提前终止条件
但在实际测试中,对于小规模数据(节点数<1000),递归版本通常更快,因为函数调用的开销小于显式栈操作。这是典型的"理论复杂度相同,但常数项不同"的情况。
4. 常见错误与边界情况
4.1 新手容易踩的坑
在面试辅导中,我发现候选人常犯以下错误:
- 忘记处理空树情况
- 在非叶子节点就提前返回True
- 错误累积路径和(应该用减法而非加法)
- 混淆节点值为0的情况与False判断
4.2 特殊测试用例集
建议用这些case验证你的解法:
python复制测试用例集:
1. 空树 (应返回False)
2. 单节点树,值等于目标值 (True)
3. 单节点树,值不等于目标值 (False)
4. 树中有负数值
5. 目标值为0的树
6. 超深树(测试递归深度限制)
7. 所有左子树和都满足,但要求到叶子节点
5. 算法优化与变种问题
5.1 提前终止优化
对于迭代解法,可以在入栈前进行预判:
python复制if node.right and curr_sum - node.right.val >= 0: # 只有剩余值非负才继续
stack.append((node.right, curr_sum - node.right.val))
这个优化在节点值为正数时特别有效,可以剪枝大量不可能路径。
5.2 变种问题:输出所有路径
LeetCode 113题是这个问题的变种,要求输出所有满足条件的路径。解决方案只需要稍作修改:
python复制def pathSum(root, targetSum):
res = []
def dfs(node, path, remaining):
if not node:
return
if not node.left and not node.right and remaining == node.val:
res.append(path + [node.val])
return
dfs(node.left, path + [node.val], remaining - node.val)
dfs(node.right, path + [node.val], remaining - node.val)
dfs(root, [], targetSum)
return res
这个变种考察的是对递归过程中状态维护的能力。
6. 实际工程中的应用场景
虽然这看起来是个纯算法题,但其核心思想在工程中有广泛应用:
- 文件系统路径匹配(如.gitignore规则检查)
- 决策树的条件判断
- 游戏中的伤害计算路径
- 网络路由的代价计算
我曾用类似思路实现过一个配置系统的覆盖规则检查:每个配置节点可能有多个来源,需要检查是否存在从根到当前节点的完整覆盖路径。这与路径求和问题有异曲同工之妙。
7. 不同语言实现的注意事项
7.1 Java实现要点
Java中要注意对象是否为null的判断:
java复制if (root == null) return false;
if (root.left == null && root.right == null) return targetSum == root.val;
7.2 C++实现优化
C++可以利用引用避免不必要的拷贝:
cpp复制bool hasPathSum(TreeNode* root, int targetSum, int currentSum = 0) {
if (!root) return false;
currentSum += root->val;
if (!root->left && !root->right) return currentSum == targetSum;
return hasPathSum(root->left, targetSum, currentSum) ||
hasPathSum(root->right, targetSum, currentSum);
}
7.3 JavaScript的简洁写法
JS可以利用箭头函数和逻辑运算符简化代码:
javascript复制const hasPathSum = (root, sum) =>
!root ? false :
!root.left && !root.right ? sum === root.val :
hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val);
8. 面试中的进阶问题
准备这道题时,应该预判可能的follow-up问题:
- 如果要求路径必须从根到叶,但不一定在叶节点结束怎么办?
- 如何找出和最接近目标值的路径?
- 如果树是BST,可以利用什么性质优化?
- 多线程环境下如何并行化这个算法?
我在亚马逊面试时就被问过第4个问题,关键在于如何安全地共享栈结构并有效划分任务。
