给定一棵二叉树和一个目标和,判断是否存在一条从根到叶子的路径,路径上所有结点值加起来恰好等于目标和。一个常见陷阱是:判断条件只写了"累计和等于目标值就返回真",没有同时要求"当前结点必须是叶子"——这样如果累计和在某个中间结点就恰好凑够了,会被提前误判为找到答案,即使这个结点后面还有子树。本文用真实反例和刻意针对性设计的随机测试验证这个错误,给出正确写法、5 种语言实现,以及回溯探索的分步演示。
本文只用自己的话概括题意,完整约束以官方页面为准。给定一棵二叉树和一个目标和,判断是否存在一条从根到叶子的路径,路径上所有结点值加起来恰好等于目标和(必须是根到叶子,不能是根到任意中间结点)。一共 5 组用例,覆盖:官方示例、根到叶子路径和不等于目标、空树、构造的陷阱反例、两结点树:
[5,4,8,11,null,13,4,7,2,null,null,null,1] target=22 → true [1,2,3] target=5 → false [] target=0 → false [5,5,null,100] target=5 → false [1,2] target=1 → false
题目要求的是"根到叶子"的路径,但一个很容易漏看的地方是:判断条件只写了"当前累计和等于目标值就返回真",没有同时要求"当前结点必须是叶子"。这样一旦从根往下走的过程中,累计和在某个中间结点恰好凑够了目标值,就会被提前误判为找到了一条合法路径,即使这个结点后面还有子树、根本不是终点:
构造的树:根 5 → 左孩子 5(还有子结点 100,不是叶子)\ntarget=5\n正确写法(必须走到叶子才能判断) → false\n陷阱写法(累计和等于target就返回真) → true
陷阱写法在根到左孩子这一步,累计和恰好是 5+... 不对,这里更准确地说是:陷阱写法一开始就判断"当前结点的值等于 target 就返回 true"(这里是根结点 5 本身等于 target=5),完全没有检查根是不是叶子——根其实还有孩子 5,不是路径的终点。为了确认这不是个例,做了一次针对性的随机测试:故意把 target 设成"根到某个内部结点"的路径和(最容易触发陷阱):
20,000 组随机二叉树(target 特意选成某条根到结点路径的和):\n正确写法与陷阱写法结果不同的有 10,105 组,占 50.5%
超过一半的情况会踩坑——因为随机选的 target 有很高概率恰好对应某个内部结点的累计和,陷阱写法看到"和已经凑够了"就提前返回真,完全不检查这条路径是否真的走到了叶子。"必须是叶子"这个约束是题目定义的核心部分,不能只看数字凑没凑够。
hasPathSum(root, target):空树直接返回假;只有走到叶子结点时,才拿"剩余目标值"跟叶子的值比较;否则把剩余目标值减去当前结点的值,分别递归左右子树,只要有一侧能找到就返回真。时间 O(n),空间是递归深度 O(h)。Python、Java、Go、JavaScript、C++ 都是真实编译运行的,用同一组 5 个测试用例。
Python · 解法代码def has_path_sum(root, target): if root is None: return False if root.left is None and root.right is None: # 必须是叶子才能算一条完整路径 return root.val == target remain = target - root.val return has_path_sum(root.left, remain) or has_path_sum(root.right, remain)
$ python3 solution.py [5,4,8,11,null,13,4,7,2,null,null,null,1] target=22 → true [1,2,3] target=5 → false [] target=0 → false [5,5,null,100] target=5 → false [1,2] target=1 → false
Java · 解法代码class Solution { public boolean hasPathSum(TreeNode root, int targetSum) { if (root == null) return false; if (root.left == null && root.right == null) return root.val == targetSum; // 必须是叶子 int remain = targetSum - root.val; return hasPathSum(root.left, remain) || hasPathSum(root.right, remain); } }
$ javac -d build Main.java && java -cp build Main [5,4,8,11,null,13,4,7,2,null,null,null,1] target=22 → true [1,2,3] target=5 → false [] target=0 → false [5,5,null,100] target=5 → false [1,2] target=1 → false
Go · 解法代码func hasPathSum(root *TreeNode, targetSum int) bool { if root == nil { return false } if root.Left == nil && root.Right == nil { // 必须是叶子 return root.Val == targetSum } remain := targetSum - root.Val return hasPathSum(root.Left, remain) || hasPathSum(root.Right, remain) }
$ go run solution.go [5,4,8,11,null,13,4,7,2,null,null,null,1] target=22 → true [1,2,3] target=5 → false [] target=0 → false [5,5,null,100] target=5 → false [1,2] target=1 → false
JavaScript · 解法代码function hasPathSum(root, targetSum) { if (root === null) return false; if (root.left === null && root.right === null) return root.val === targetSum; // 必须是叶子 const remain = targetSum - root.val; return hasPathSum(root.left, remain) || hasPathSum(root.right, remain); }
$ node run.js [5,4,8,11,null,13,4,7,2,null,null,null,1] target=22 → true [1,2,3] target=5 → false [] target=0 → false [5,5,null,100] target=5 → false [1,2] target=1 → false
C++ · 解法代码class Solution { public: bool hasPathSum(TreeNode* root, int targetSum) { if (!root) return false; if (!root->left && !root->right) return root->val == targetSum; // 必须是叶子 int remain = targetSum - root->val; return hasPathSum(root->left, remain) || hasPathSum(root->right, remain); } };
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin [5,4,8,11,null,13,4,7,2,null,null,null,1] target=22 → true [1,2,3] target=5 → false [] target=0 → false [5,5,null,100] target=5 → false [1,2] target=1 → false
共 5 组用例(含边界),以 Python 结果为基准逐组比对:\n Python 通过 5/5\n Java 通过 5/5\n Go 通过 5/5\n JavaScript 通过 5/5\n C++ 通过 5/5
Python 版本的递归实现另外和一个独立写的迭代版本做了随机对拍:迭代版用一个显式栈,栈里存"结点 + 到这个结点为止的累计和"这个二元组,弹出时如果是叶子就直接比较累计和。
随机对拍 20,000 组(target 特意选成某条根到结点路径的和),递归版与迭代栈版的结果全部一致,0 处不一致。
另外四种语言只在上面 5 组固定用例上验证过,没有做大规模随机对拍。
下面的演示用官方示例(目标和 22)的前几层:从根出发不断把剩余目标值减去当前结点的值,一路探索到叶子才检查剩余目标是否归零;某条路走不通就回溯,换下一条路径继续试,直到找到一条真正在叶子结点上和为 0 的路径,或者所有路径都试完。蓝色是正在探索的结点,绿色是已经确认不在最终命中路径上的结点。每一帧都是把上面的解法在这组输入上真实执行记录下来的。