LeetCode 113. 路径总和 II
问题描述
给定二叉树的根节点 root
和一个整数 targetSum
,找出所有从根节点到叶子节点的路径,其中路径上节点值的总和等于 targetSum
。
示例:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:[[5,4,11,2],[5,8,4,5]]
解释:
5 → 4 → 11 → 2 = 22
5 → 8 → 4 → 5 = 22
算法思路
深度优先搜索(DFS) + 回溯:
- DFS遍历:从根节点开始递归遍历每个节点。
- 路径记录:维护当前路径列表,记录遍历过的节点值。
- 路径和更新:实时计算当前路径和(或传递累计和)。
- 叶子节点检查:到达叶子节点时,若路径和等于
targetSum
,将当前路径加入结果。 - 回溯:递归返回前从路径中移除当前节点,确保路径状态正确。
代码实现
import java.util.ArrayList;
import java.util.List;
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 List<List<Integer>> pathSum(TreeNode root, int targetSum) {
List<List<Integer>> result = new ArrayList<>(); // 存储所有符合条件的路径
List<Integer> currentPath = new ArrayList<>(); // 存储当前遍历路径
dfs(root, targetSum, 0, currentPath, result); // 启动DFS
return result;
}
/**
* 深度优先搜索(DFS)辅助函数
*
* @param node 当前遍历的节点
* @param targetSum 目标和
* @param currentSum 当前路径累计和
* @param currentPath 当前路径(节点值列表)
* @param result 结果集
*/
private void dfs(TreeNode node, int targetSum, int currentSum,
List<Integer> currentPath, List<List<Integer>> result) {
if (node == null) return; // 节点为空,直接返回
currentPath.add(node.val); // 将当前节点加入路径
currentSum += node.val; // 更新累计和
// 叶子节点检查
if (node.left == null && node.right == null) {
if (currentSum == targetSum) {
// 满足条件时,添加当前路径的拷贝到结果集
result.add(new ArrayList<>(currentPath));
}
} else {
// 递归遍历左右子树
dfs(node.left, targetSum, currentSum, currentPath, result);
dfs(node.right, targetSum, currentSum, currentPath, result);
}
// 回溯:移除当前节点,恢复路径状态
currentPath.remove(currentPath.size() - 1);
// 注意:currentSum 是局部变量,自动回溯,无需显式修改
}
}
注释
-
初始化:
result
:存储所有符合条件的路径(列表的列表)。currentPath
:动态记录当前遍历路径的节点值。
-
DFS 函数:
- 终止条件:节点为空时返回。
- 记录路径:
currentPath.add(node.val)
:将当前节点值加入路径。currentSum += node.val
:更新路径累计和。
- 叶子节点处理:
- 左右子节点均为空时判定为叶子节点。
- 若路径和等于
targetSum
,将当前路径的拷贝加入result
(避免后续修改影响结果)。
- 递归子树:非叶子节点时,继续遍历左右子树。
- 回溯:
currentPath.remove(size-1)
:移除当前节点,确保路径正确回溯。currentSum
是局部变量(基本类型),递归返回时自动恢复,无需显式回退。
算法分析
- 时间复杂度:O(n),每个节点访问一次。
- 空间复杂度:O(n),递归栈深度和路径存储(最坏情况二叉树退化为链表)。
- 核心点:DFS遍历 + 回溯维护路径状态。
算法过程
输入 root = [5,4,8,11,null,13,4,7,2,null,null,5,1]
, targetSum=22
:
- 遍历左子树:
- 路径:
[5,4,11,7]
→ 和=27 ≠ 22 → 回溯移除7
。 - 路径:
[5,4,11,2]
→ 和=22 → 加入结果。
- 路径:
- 遍历右子树:
- 路径:
[5,8,13]
→ 和=26 ≠ 22 → 回溯。 - 路径:
[5,8,4,5]
→ 和=22 → 加入结果。 - 路径:
[5,8,4,1]
→ 和=18 ≠ 22 → 回溯。
- 路径:
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:示例
TreeNode root = new TreeNode(5,
new TreeNode(4,
new TreeNode(11,
new TreeNode(7),
new TreeNode(2)),
new TreeNode(8,
new TreeNode(13),
new TreeNode(4,
new TreeNode(5),
new TreeNode(1))));
System.out.println("Test 1: " + solution.pathSum(root, 22));
// 预期: [[5,4,11,2],[5,8,4,5]]
// 测试用例2:空树
System.out.println("Test 2: " + solution.pathSum(null, 0));
// 预期: []
// 测试用例3:单节点满足条件
TreeNode root3 = new TreeNode(1);
System.out.println("Test 3: " + solution.pathSum(root3, 1));
// 预期: [[1]]
// 测试用例4:无满足条件的路径
TreeNode root4 = new TreeNode(1, new TreeNode(2), null);
System.out.println("Test 4: " + solution.pathSum(root4, 3));
// 预期: [](路径 1→2 和=3,但2不是叶子节点)
}
关键点
- 路径定义:必须从根节点到叶子节点(叶子节点无左右子树)。
- 回溯:确保递归返回时路径状态正确。
- 结果存储:添加路径时需创建新列表(
new ArrayList<>(currentPath)
)。 - 局部变量:
currentSum
使用局部变量可避免显式回溯。 - 边界处理:空树、单节点树、无解情况需正确处理。
LeetCode 437. 路径总和 III
问题描述
给定二叉树的根节点 root
和一个整数 targetSum
,找出路径和等于 targetSum
的路径数量。路径不需要从根节点开始,也不需要在叶子节点结束,但必须是从父节点指向子节点的向下路径。
示例:
输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和为 8 的路径有:
1. 5 → 3
2. 5 → 2 → 1
3. -3 → 11
算法思路
前缀和 + 深度优先搜索(DFS):
- 前缀和:
- 序列的前n项和,对于一个数组a,其前缀和数组s定义为:
- s[i] = a[0] + a[1] + … + a[i]
- 例如,对于数组a = [1, 2, 3, 4, 5],其前缀和数组s为:
- s = [1, 3, 6, 10, 15]
- 序列的前n项和,对于一个数组a,其前缀和数组s定义为:
- 前缀和记录:
- 使用哈希表
prefixSumMap
记录从根节点到当前节点的路径上所有前缀和及其出现次数。 - 初始化:
prefixSumMap.put(0, 1)
(空路径和为 0)。
- 使用哈希表
- DFS遍历:
- 递归遍历每个节点,更新当前路径和
currentSum
。 - 计算
currentSum - targetSum
,若差值存在于哈希表中,则存在满足条件的路径。 - 更新哈希表,递归左右子树,回溯时维护哈希表状态。
- 递归遍历每个节点,更新当前路径和
代码实现
import java.util.HashMap;
import java.util.Map;
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> prefixSumMap = new HashMap<>();
prefixSumMap.put(0L, 1); // 空路径前缀和为0
return dfs(root, targetSum, 0L, prefixSumMap);
}
/**
* 深度优先搜索(DFS)辅助函数
*
* @param node 当前节点
* @param targetSum 目标和
* @param currentSum 当前路径累计和
* @param prefixSumMap 前缀和哈希表
* @return 返回路径数量
*/
private int dfs(TreeNode node, int targetSum, long currentSum,
Map<Long, Integer> prefixSumMap) {
if (node == null) return 0; // 节点为空,返回0
currentSum += node.val; // 更新当前路径和
int count = 0;
// 检查是否存在路径满足 currentSum - prefixSum = targetSum
count += prefixSumMap.getOrDefault(currentSum - targetSum, 0);
// 更新当前路径和的出现次数
prefixSumMap.put(currentSum, prefixSumMap.getOrDefault(currentSum, 0) + 1);
// 递归遍历左右子树
count += dfs(node.left, targetSum, currentSum, prefixSumMap);
count += dfs(node.right, targetSum, currentSum, prefixSumMap);
// 回溯:移除当前路径和,避免影响其他分支
prefixSumMap.put(currentSum, prefixSumMap.get(currentSum) - 1);
if (prefixSumMap.get(currentSum) == 0) {
prefixSumMap.remove(currentSum);
}
return count;
}
}
注释
-
初始化:
prefixSumMap
:存储前缀和及其出现次数,初始化(0,1)
表示空路径和为 0。
-
DFS 函数:
- 终止条件:节点为空时返回 0。
- 更新路径和:
currentSum += node.val
。 - 路径计数:
currentSum - targetSum
的差值若在prefixSumMap
中存在,则存在有效路径。count
累加差值出现的次数。
- 更新哈希表:当前路径和出现次数 +1。
- 递归子树:遍历左右子树,累加路径数量。
- 回溯维护:
- 将当前路径和的出现次数 -1。
- 若次数为 0 则移除该键值(避免哈希表膨胀)。
算法分析
- 时间复杂度:O(n),每个节点访问一次。
- 空间复杂度:O(n),哈希表和递归栈空间(最坏情况树退化为链表)。
- 核心点:前缀和思想 + 回溯维护哈希表状态。
算法过程
输入 root = [10,5,-3,3,2,null,11,3,-2,null,1]
, targetSum=8
:
- 遍历节点 10:
currentSum=10
,10-8=2
→ 无匹配。- 哈希表:
{0:1, 10:1}
- 遍历节点 5:
currentSum=15
,15-8=7
→ 无匹配。- 哈希表:
{0:1, 10:1, 15:1}
- 遍历节点 3:
currentSum=18
,18-8=10
→ 匹配到10
(路径:5→3)。- 哈希表:
{0:1, 10:1, 15:1, 18:1}
- 最终匹配路径:
- 5 → 3(和=8)
- 5 → 2 → 1(和=8)
- -3 → 11(和=8)
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例
TreeNode root1 = new TreeNode(10,
new TreeNode(5,
new TreeNode(3,
new TreeNode(3),
new TreeNode(-2)),
new TreeNode(2,
null,
new TreeNode(1))),
new TreeNode(-3,
null,
new TreeNode(11)));
System.out.println("Test 1: " + solution.pathSum(root1, 8)); // 3
// 测试用例2:路径包含负值
TreeNode root2 = new TreeNode(1,
new TreeNode(-2,
new TreeNode(1,
new TreeNode(-1),
null),
new TreeNode(3)),
new TreeNode(-3,
new TreeNode(-2),
null));
System.out.println("Test 2: " + solution.pathSum(root2, -1)); // 4
// 测试用例3:空树
System.out.println("Test 3: " + solution.pathSum(null, 0)); // 0
// 测试用例4:单节点满足条件
TreeNode root4 = new TreeNode(5);
System.out.println("Test 4: " + solution.pathSum(root4, 5)); // 1
// 测试用例5:单节点不满足条件
TreeNode root5 = new TreeNode(5);
System.out.println("Test 5: " + solution.pathSum(root5, 3)); // 0
}
关键点
-
前缀和思想:
- 任意路径和 =
currentSum - prefixSum
。 - 查找
prefixSum = currentSum - targetSum
。
- 任意路径和 =
-
哈希表维护:
- 存储从根节点到当前节点的所有前缀和。
- 回溯时及时移除当前路径和,避免污染其他分支。
-
路径定义:
- 路径必须向下(父节点→子节点)。
- 起点不必是根节点,终点不必是叶子节点。
-
负值处理:
- 使用
long
类型避免整数溢出。 - 哈希表需支持负值前缀和。
- 使用
-
回溯必要性:
- 左右子树独立遍历,需维护哈希表状态一致性。