一、核心思路
题目要求:路径方向只能向下,路径不用从根开始、不用到叶子节点,统计节点和等于targetSum的路径总数。
前缀和定义:从根节点走到当前节点,沿途节点累加总和。 公式:
当前前缀和 − 祖先前缀和 = 两点之间的路径和变形:若当前前缀和 - targetSum在哈希表存在,说明存在一段路径和等于targetSum。 HashMap:记录当前递归路径上前缀和出现次数;递归返回时回溯,撤销本次新增前缀和,防止左右分支数据互相污染。
二、完整 Java 代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int pathSum(TreeNode root, int targetSum) {
Map<Long,Integer> map = new HashMap<>();
map.put(0L,1); // 预设虚拟前缀和0,处理从根出发直接满足条件的路径
return dfs(root, map, targetSum, 0L);
}
int dfs(TreeNode node, Map<Long, Integer> map, int target, long currSum) {
if(node == null) return 0;
int res = 0;
// 步骤1:更新当前前缀和
currSum += node.val;
// 步骤2:查表,统计符合条件路径数量
res += map.getOrDefault(currSum - target, 0);
// 步骤3:将当前前缀和存入map,供子节点查询
map.put(currSum, map.getOrDefault(currSum, 0) + 1);
// 步骤4:递归遍历左右子树
res += dfs(node.left, map, target, currSum);
res += dfs(node.right, map, target, currSum);
// 步骤5:回溯!离开节点前撤销本次前缀和记录
map.put(currSum, map.get(currSum) - 1);
return res;
}
}
三、实例演示
样例输入:root = [10,5,-3,3,2,null,11,3,-2,null,1],targetSum = 8 树结构:
10
/ \
5 -3
/ \ \
3 2 11
/ \ \
3 -2 1
答案:共 3 条路径:5→3、5→2→1、-3→11
| 递归访问节点 | 当前前缀和 currSum | 查找 currSum-8 | map 内存储的前缀和(仅当前根到节点路径) | 新增路径数量 | res 累计 | |||
|---|---|---|---|---|---|---|---|---|
| 10 | 10 | 2 | {0:1} | 0 | 0 | |||
| 5 | 15 | 7 | {0:1,10:1} | 0 | 0 | |||
| 3 | 18 | 10 | {0:1,10:1,15:1} | 1 | 1 | |||
| 3 | 21 | 13 | {0:1,10:1,15:1,18:1} | 0 | 1 | 回溯,21 计数 - 1 | ||
| -2 | 16 | 8 | {0:1,10:1,15:1,18:1} | 0 | 1 | 回溯,16 计数 - 1 | 回溯,18 计数 - 1 | |
| 2 | 17 | 9 | {0:1,10:1,15:1} | 0 | 1 | |||
| 1 | 18 | 10 | {0:1,10:1,15:1,17:1} | 1 | 2 | 回溯,18 计数 - 1 | 回溯,17 计数 - 1 | 回溯,15 计数 - 1 |
| -3 | 7 | -1 | {0:1,10:1} | 0 | 2 | |||
| 11 | 18 | 10 | {0:1,10:1,7:1} | 1 | 3 | 回溯,18 计数 - 1 | 回溯,7 计数 - 1 | 回溯,10 计数 - 1 |
四、复杂度分析
- 时间复杂度:O (n),n 为节点数量,每个节点访问一次
- 空间复杂度:O (n),递归栈深度 + 哈希表存储前缀和,最坏为单链树
五、易错点总结
- 初始化
map.put(0L,1):虚拟前缀和,用于匹配从根节点开始就满足总和等于 targetSum 的路径。 - 回溯不可省略:左子树遍历完成后,需要撤销 map 中当前节点的前缀和,防止右分支读取左分支残留数据,造成计数错误。
- 前缀和变量必须使用
long:节点值累加可能超出 int 范围,引发溢出;同时匹配Map<Long,Integer>的 key 类型,避免类型转换报错。 - 变量拼写:
currSum,不要写成 currtSum,会报找不到符号的编译错误。 - map 维护的是当前单条从上到下路径的前缀和,不是整棵树所有节点的前缀和。
六、个人感悟
这道题是数组前缀和思想迁移到二叉树上。二叉树深度优先搜索,一条递归链等价于数组;但是二叉树存在左右分叉,所以递归返回时要做回溯,保证哈希表只保存当前根到当前节点这条路径上的前缀和,左右分支互不干扰。