📌 题目链接:437. 路径总和 III - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:树、深度优先搜索(DFS)、前缀和、哈希表、回溯
⏱️ 目标时间复杂度:O(N)(前缀和优化法)
💾 空间复杂度:O(N)
📖 题目分析
给定一棵二叉树的根节点
root和一个整数targetSum,要求统计所有向下路径(从父到子,不必从根开始,也不必在叶结束)中,节点值之和等于targetSum的路径数目。
关键点:
- 路径必须是连续且向下的(即只能从祖先到后代)。
- 起点可以是任意节点,终点也可以是任意节点(只要在起点下方)。
- 节点值可为负数!这意味着路径和可能先增大后减小,甚至多次命中目标值。
- 不能只考虑从根出发的路径,这是与“路径总和 I/II”的核心区别。
💡 举个例子:若某条路径上存在
[10, 5, -3, 3],而targetSum = 8,那么:
5 → 3(5+3=8)10 → 5 → -3 → 3 → -2中的5 → 2 → 1(5+2+1=8)10 → -3 → 11(10-3+11=18 ≠ 8,但其他组合可能成立)
因此,暴力枚举每个起点再向下搜索是一种可行思路,但效率低;更优解是借鉴数组中“和为 K 的子数组” (LeetCode 560)的思想——前缀和 + 哈希表。
⚙️ 核心算法及代码讲解
本题有两种主流解法:
✅ 方法一:双重递归(朴素 DFS)—— O(N²)
- 对每个节点,都当作起点,向下搜索所有可能路径。
- 时间复杂度高,但在面试中容易写出,适合保底。
✅✅ 方法二:前缀和 + DFS + 哈希表(推荐)—— O(N)
- 核心思想:在一条从根到当前节点的路径上,若存在两个前缀和
prefix[i]和prefix[j](i < j),满足prefix[j] - prefix[i] = targetSum,则从第 i+1 个节点到第 j 个节点的路径和为targetSum。 - 使用哈希表记录当前路径上各前缀和出现的次数。
- 关键细节:进入子树前记录当前前缀和,退出时要回溯(remove 或 decrement) ,避免不同分支互相干扰。
⚠️ 注意:由于节点值范围大(±1e9),且路径长可达 1000,总和可能溢出
int,必须使用long long!
下面重点讲解方法二的 C++ 实现(带详细行注释):
class Solution {
public:
unordered_map<long long, int> prefix; // 哈希表:记录当前路径上前缀和的出现次数
// curr: 从根到当前节点的路径和(不含当前节点?不!这里包含!)
// 实际上,curr 在进入函数后立即 += root->val,所以代表包含当前节点的前缀和
int dfs(TreeNode* root, long long curr, int targetSum) {
if (!root) return 0;
int ret = 0;
curr += root->val; // 更新当前前缀和(包含当前节点)
// 检查是否存在前缀和 = curr - targetSum
// 若存在,说明从那个前缀和之后到当前节点的路径和为 targetSum
if (prefix.count(curr - targetSum)) {
ret = prefix[curr - targetSum]; // 累加所有满足条件的路径数
}
// 将当前前缀和加入哈希表(用于后续子节点查询)
prefix[curr]++;
// 递归左右子树
ret += dfs(root->left, curr, targetSum);
ret += dfs(root->right, curr, targetSum);
// 回溯!退出当前节点前,移除其对哈希表的影响
// 否则右子树会错误地看到左子树的前缀和
prefix[curr]--;
return ret;
}
int pathSum(TreeNode* root, int targetSum) {
prefix[0] = 1; // 初始化:空路径的前缀和为 0,出现 1 次
// 这样当某条路径从根开始恰好等于 targetSum 时,也能被正确计数
return dfs(root, 0, targetSum);
}
};
🔑 为什么初始化
prefix[0] = 1?
假设从根到某节点的路径和正好是targetSum,那么curr - targetSum = 0。如果没有prefix[0]=1,就无法计数这条完整路径。
🧩 解题思路(分步骤)
方法二前缀和步骤详解:
-
定义前缀和:从根节点到当前节点(含)的路径上所有节点值之和。
-
建立映射:用哈希表
prefix记录当前 DFS 路径上所有前缀和及其出现次数。 -
初始化:
prefix[0] = 1,表示“空路径”的前缀和为 0(用于匹配从根开始的合法路径)。 -
DFS 遍历:
-
进入节点,更新当前前缀和
curr += root->val。 -
查询
curr - targetSum是否存在于prefix中:- 若存在,说明有
prefix[curr - targetSum]条路径以当前节点为终点、和为targetSum。
- 若存在,说明有
-
将当前
curr加入prefix。 -
递归处理左右子树。
-
回溯:从
prefix中移除当前curr(避免影响兄弟子树)。
-
-
返回结果:累计所有满足条件的路径数。
💡 类比数组问题:这本质上就是把树的一条根到叶的路径看作一个数组,求其中“和为 targetSum 的连续子数组个数”。而 DFS 保证我们遍历了所有可能的“数组”(每条根到叶路径)。
📊 算法分析
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 双重递归 | O(N²) | O(N)(递归栈) | 节点少、面试快速实现 |
| 前缀和 + 哈希 | O(N) | O(N)(哈希表 + 递归栈) | 最优解,面试加分项 |
🎯 面试建议:
- 先写双重递归,说明思路清晰。
- 再提出优化:能否避免重复计算?引出前缀和思想。
- 强调回溯的必要性(否则哈希表污染不同分支)。
- 提醒整数溢出问题(必须用
long long)。
💻 代码
✅C++
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// Definition for a binary tree node.
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class Solution {
public:
unordered_map<long long, int> prefix;
int dfs(TreeNode* root, long long curr, int targetSum) {
if (!root) return 0;
int ret = 0;
curr += root->val;
if (prefix.count(curr - targetSum)) {
ret = prefix[curr - targetSum];
}
prefix[curr]++;
ret += dfs(root->left, curr, targetSum);
ret += dfs(root->right, curr, targetSum);
prefix[curr]--;
return ret;
}
int pathSum(TreeNode* root, int targetSum) {
prefix[0] = 1;
return dfs(root, 0, targetSum);
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
// 构建示例1: [10,5,-3,3,2,null,11,3,-2,null,1]
TreeNode* root = new TreeNode(10);
root->left = new TreeNode(5);
root->right = new TreeNode(-3);
root->left->left = new TreeNode(3);
root->left->right = new TreeNode(2);
root->right->right = new TreeNode(11);
root->left->left->left = new TreeNode(3);
root->left->left->right = new TreeNode(-2);
root->left->right->right = new TreeNode(1);
Solution sol;
cout << "Example 1: " << sol.pathSum(root, 8) << " (expected: 3)" << endl;
// 构建示例2: [5,4,8,11,null,13,4,7,2,null,null,5,1]
TreeNode* root2 = new TreeNode(5);
root2->left = new TreeNode(4);
root2->right = new TreeNode(8);
root2->left->left = new TreeNode(11);
root2->right->left = new TreeNode(13);
root2->right->right = new TreeNode(4);
root2->left->left->left = new TreeNode(7);
root2->left->left->right = new TreeNode(2);
root2->right->right->left = new TreeNode(5);
root2->right->right->right = new TreeNode(1);
cout << "Example 2: " << sol.pathSum(root2, 22) << " (expected: 3)" << endl;
return 0;
}
✅JavaScript
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
var pathSum = function(root, targetSum) {
const prefix = new Map();
prefix.set(0, 1); // 初始化空路径前缀和
return dfs(root, prefix, 0, targetSum);
};
const dfs = (root, prefix, curr, targetSum) => {
if (root == null) {
return 0;
}
let ret = 0;
curr += root.val;
// 查找是否存在前缀和 = curr - targetSum
ret = prefix.get(curr - targetSum) || 0;
// 更新当前前缀和的计数
prefix.set(curr, (prefix.get(curr) || 0) + 1);
// 递归子树
ret += dfs(root.left, prefix, curr, targetSum);
ret += dfs(root.right, prefix, curr, targetSum);
// 回溯:恢复哈希表状态
prefix.set(curr, prefix.get(curr) - 1);
return ret;
};
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!