bigywater.tech
算法与数据结构 · LeetCode 112

LeetCode 112. 路径总和

给定一棵二叉树和一个目标和,判断是否存在一条从根到叶子的路径,路径上所有结点值加起来恰好等于目标和。一个常见陷阱是:判断条件只写了"累计和等于目标值就返回真",没有同时要求"当前结点必须是叶子"——这样如果累计和在某个中间结点就恰好凑够了,会被提前误判为找到答案,即使这个结点后面还有子树。本文用真实反例和刻意针对性设计的随机测试验证这个错误,给出正确写法、5 种语言实现,以及回溯探索的分步演示。

50.5%
故意把目标和设成某条根到内部结点的路径和时,一半的随机树会被陷阱写法误判为存在合法路径
4 步
分步演示:不断把剩余目标值往下传,只有走到叶子才检查是否归零,不对就回溯,另附 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)
真实实测 · Python · 前几组用例的输出
$ 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); } }
真实实测 · Java · 前几组用例的输出
$ 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 · 前几组用例的输出
$ 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); }
真实实测 · JavaScript · 前几组用例的输出
$ 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); } };
真实实测 · C++ · 前几组用例的输出
$ 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 的路径,或者所有路径都试完。蓝色是正在探索的结点,绿色是已经确认不在最终命中路径上的结点。每一帧都是把上面的解法在这组输入上真实执行记录下来的。

路径总和:只有走到叶子才检查剩余目标是否归零
正在探索的结点确认不在最终答案路径上找到的目标叶子

    参考与说明

    • 官方题目页:LeetCode 112. 路径总和。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚。
    • 没有覆盖:需要返回具体路径内容而不只是布尔值的写法(下一题 113 题)。
    京ICP备2026067502号-1京公网安备11010502063487号