路径总和:剩余值往下传,只在叶子结账
每经过一个节点就从目标和里减去它。真正能返回 true 的地方只有叶子:它的值是否刚好等于还剩的数。
hasPathSum(n, remain):空节点 false;若 n 是叶子,返回 n.Val == remain;否则问左孩子或右孩子是否存在 hasPathSum(孩子, remain−n.Val)。路径必须从根到叶,内部节点即使把剩余减到 0 也不算。时间 O(n),空间 O(h)。
这是 LeetCode 112. Path Sum。给你二叉树根和整数 targetSum,判断是否存在一条从根走到叶子的路径,沿途节点值之和等于 targetSum。叶子是左右孩子都空的节点。空树没有路径,返回 false。
主例层序 [5, 4, 8, 11, null, 13, 4, 7, 2],targetSum = 22。路径 5→4→11→2 的和是 22,返回 true。5→4→11→7 是 27,不是。右边 5→8→4 下面还有孩子,不能在 4 停。
枚举所有根到叶路径再求和,能做对。缺的是沿途就可以把目标和收成「还差多少」:走到节点 n 时,问题变成子树里有没有一条往下的路径凑齐 remain−n.Val。本文用减法当递归参数,并坚持只在叶子结账。
向下递归:剩余值递减
「从根到叶的和等于 target」等价于:带着 remain=target 出发,每经过一个节点就把 remain 换成 remain−节点值,问能不能走到某个叶子时,叶子值正好等于手里的 remain。两句话是同一条路径上的加减。
第一直觉用加法:把当前和当参数,到叶子看和是否等于 target。和减法同构。减法的好处是参数越走越小,和题面的「还差多少」对齐。空孩子直接 false,不要把空当成和为 0 的叶子。
用手走主例左脊,对应三帧。根 5,22−5=17。左孩子 4,17−4=13。再左 11,13−11=2。演示停在这里:还剩 2,下面两个叶子分别是 7 和 2,下一节结账。
减法视角remain 是「包含当前节点在内还需要凑齐的数」。传给孩子之前先减掉自己,孩子看到的是后缀还该是多少。
叶子处判定
到达节点时若左右都空,这才是路径终点。判断写成 n.Val == remain:进来时 remain 还包含这一格该贡献的数。主例走到 11 后 remain=2,左叶 7:7≠2,失败;右叶 2:2==2,成功。演示把失败那条画成已经减过 7 的 −5,成功那条画成减过 2 的 0,是同一件事的事后视角。
内部节点即使 remain 减完自己变成 0,也不能返回 true——路径还没到叶。主例若 target 改成 9,根到 4 已经用完,4 还有孩子 11,这条不算。左右子树用或:一边成功就够,不必找齐所有路径。
空树在函数入口返回 false。单节点树就是叶子,直接比根值与 target。负值、零值都合法,不要提前因 remain 变负剪枝——后面若有负数还能加回来,主例没有这种情况,但题目允许。
Go:递归减法
func hasPathSum(root *TreeNode, target int) bool {if root == nil { return false }if root.Left == nil && root.Right == nil {return root.Val == target}return hasPathSum(root.Left, target-root.Val) ||hasPathSum(root.Right, target-root.Val)}
1空节点不是叶子,返回 false。一边孩子为空时,空的那次递归会走这句,不会误判。
2左右都空才比 Val 与 remain。主例 7≠2 得 false,2==2 得 true。
3内部节点把 target−Val 传下去。主例 22→17→13→2。
4或运算短路:左叶成功就不必再走右子树。要列出全部路径是 LC113。
总结
带着剩余值往下减,只在叶子问是否刚好。主例 5−4−11−2 凑齐 22。
- 内部节点剩余为 0 也不算到。路径定义卡在叶子。
- 空孩子返回 false,不要当成和为 0 的叶。
- remain 变负不能剪:后面的负节点还能把和拉回来。