力扣链接: LeetCode 437 (路径总和 III),【难度:中等;通过率:通过率:47.8%】
道题的难度在于,它打破了“从根到叶”的限制,要求我们找出树中任意节点到其任意后代节点的路径,其和等于目标值。这需要我们转变思路,采用更灵活的递归策略
一、 题目描述
给定一个二叉树的根节点 root
,和一个整数 targetSum
,求该二叉树里节点值之和等于 targetSum
的 路径 的数目
路径 不需要从根节点开始,也不需要在叶子节点结束,但路径方向必须是向下的(只能从父节点到子节点)
示例:
输入: root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出: 3
解释: 和为 8 的路径有 3 条,分别是:
1. 5 -> 3
2. 5 -> 2 -> 1
3. -3 -> 11
二、 核心思路:双重递归,职责分离
这道题的核心难点在于“路径可以从任意节点开始”。为了解决这个问题,我们可以将问题分解为两个子问题:
-
问题一:如何选择路径的起点?
- 树中的任何一个节点都可以是路径的起点。因此,我们需要一种方法来遍历树中的每一个节点
-
问题二:如何计算从一个固定起点出发的所有路径?
- 一旦我们固定了起点,问题就退化为:从这个起点开始,向下遍历其所有子孙节点,找出所有和为
targetSum
的路径
- 一旦我们固定了起点,问题就退化为:从这个起点开始,向下遍历其所有子孙节点,找出所有和为
这种清晰的职责划分,自然地引出了双重递归的解法:
- 外层递归 (
pathSum
):它的任务是遍历树,为内层递归选择起点。它会对当前节点root
说:“你来当一次起点”,然后对自己说:“我的任务还没完,我还要让我的左孩子和右孩子也去当起点” - 内层递归 (
dfs
):它的任务是从一个固定的起点出发,向下进行深度优先搜索,计算所有可能的路径和,并统计满足条件的路径数量
三、 代码实现与深度解析
【一种直观解法】:
public class Solution {
int target;
int resultCount; // 结果收集
/**
* 对每个root节点的左右子树调用pathSum递归处理
* 同时dfs向下遍历,添加和的值,搜集结果
* @param root
* @param targetSum
* @return
*/
public int pathSum(TreeNode root, int targetSum) {
target = targetSum;
if (root == null) {
return 0;
}
// 从root(遍历当前(子)树)开始
dfs(0, root);
// 从root左子树开始
pathSum(root.left, targetSum); // 本质是“子问题”的拆分!
// 从root右子树开始
pathSum(root.right, targetSum);
return resultCount;
}
/**
* 从当前的节点,向下遍历,并且对所有情况收集和
* @param curSum 路径中已经收集到的和
* @param curNode 当前节点
*/
public void dfs(long curSum, TreeNode curNode) {
if (curNode == null) {
// 只有搜索完毕才return
return;
}
// 累加结果
curSum += curNode.val;
if (curSum == target) {
resultCount++;
// 并不return 而是继续搜索
}
// 搜索左子树
if (curNode.left != null) {
dfs(curSum, curNode.left);
}
// 搜索右子树
if (curNode.right != null) {
dfs(curSum, curNode.right);
}
}
}
四、 效率分析与优化方向
- 时间复杂度:在最坏情况下(树退化为链表),时间复杂度为 O(N²)。对于平衡二叉树,时间复杂度为 O(N log N)。这是因为外层递归访问 N 个节点,对于每个节点,内层递归
dfs
最多访问其所有子孙节点 - 空间复杂度:O(H),主要由递归栈的深度决定,H 是树的高度。最坏情况下为 O(N)
- 优化方向:这种双重递归存在大量重复计算。例如,在计算从根节点出发的路径时,我们已经算过了
root -> A -> B
的和,但稍后以节点A
为起点时,我们又会重新计算A -> B
的和。这个问题可以通过前缀和 + 哈希表的方法进行优化,将时间复杂度降至 O(N)
五、 路径总和“三部曲”对比分析
现在,让我们将 LC437 与 LC112 和 LC113 进行对比,看看这个系列是如何层层递进的
LeetCode 112 (路径总和) | LeetCode 113 (路径总和 II) | LeetCode 437 (路径总和 III) | |
---|---|---|---|
路径定义 | 必须从根到叶 | 必须从根到叶 | 任意节点到其后代节点 |
问题目标 | 判断存在性 (返回 boolean ) | 记录所有满足条件的路径 (返回 List<List<Integer>> ) | 统计所有满足条件的路径数量 (返回 int ) |
核心算法 | 单次 DFS + (隐式)回溯 | 单次 DFS + 显式回溯 | 双重递归 (或 前缀和优化) |
回溯方式 | 隐式回溯 (传递 int ) | 显式回溯 (传递 List ,需 remove ) | 隐式回溯 (在 dfs 中传递 long ) |
剪枝 | 可以 (找到一个即可) | 不可以 (需找到所有) | 不可以 (需统计所有) |
关键点 | 在叶子节点判断 sum == target | 在叶子节点判断后,深拷贝路径 new ArrayList<>(path) | 职责分离:一个递归选起点,一个递归算路径和 |
总结:
- LC112 是基础,考察的是最简单的“是否存在”问题,用一次 DFS 就能解决
- LC113 在 112 的基础上增加了“记录所有路径”的要求,引入了显式回溯和结果深拷贝的概念
- LC437 则彻底改变了规则,放宽了路径的定义,使得单次 DFS 不足以解决问题,必须采用双重递归或更高级的前缀和技巧来确保覆盖所有可能的路径起点;优化后的前缀和解法,参考:LeetCode437:路径总和Ⅲ 前缀和+哈希表的最优解
LeetCode 112 (路径总和) 及 LeetCode 113 (路径总和 II) 等多题对比讲解参考: