深入剖析二叉树路径和问题:从暴力法到前缀和优化(C++实现)
力扣437. 路径总和 III
问题描述
给定一个二叉树的根节点 root
和一个整数 targetSum
,要求找出二叉树中所有路径,这些路径上的节点值之和等于 targetSum
。路径的定义是从父节点指向子节点的方向(向下),且不需要从根节点开始,也不需要在叶子节点结束。
示例分析
示例1:
// 输入:
// 10
// / \
// 5 -3
// / \ \
// 3 2 11
// ...
// targetSum = 8
// 预期输出:3
示例2:
// 输入:
// [5,4,8,11,null,13,4,7,2,null,null,5,1]
// targetSum = 22
// 预期输出:3
C++解题实现
方法一:暴力递归法(双重DFS)
class Solution {
public:
int pathSum(TreeNode* root, int targetSum) {
if (!root) return 0;
// 计算以当前节点为起点的路径数
int count = dfs(root, targetSum);
// 递归计算左右子树的路径数
count += pathSum(root->left, targetSum);
count += pathSum(root->right, targetSum);
return count;
}
private:
int dfs(TreeNode* node, long long sum) {
if (!node) return 0;
int count = 0;
sum -= node->val;
if (sum == 0) {
count++;
}
count += dfs(node->left, sum);
count += dfs(node->right, sum);
return count;
}
};
复杂度分析:
-
时间复杂度:O(n²)
-
空间复杂度:O(n)
方法二:前缀和优化(哈希表+DFS)
/**
* 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 {
unordered_map<long long,int> cnt; // 前缀和哈希表,键为long long防止溢出
int res=0; // 结果计数器
void dfs(TreeNode* curnode, long long sum, int targetSum) {
if(!curnode) return;
// 更新当前路径和
sum += curnode->val;
// 检查是否存在满足条件的前缀和
if(cnt.find(sum-targetSum) != cnt.end())
res += cnt[sum-targetSum];
// 记录当前前缀和
cnt[sum]++;
// 递归处理左右子树
if(curnode->left) dfs(curnode->left, sum, targetSum);
if(curnode->right) dfs(curnode->right, sum, targetSum);
// 回溯,恢复哈希表状态
cnt[sum]--;
}
public:
int pathSum(TreeNode* root, int targetSum) {
cnt[0] = 1; // 初始化空路径和
dfs(root, 0, targetSum);
return res;
}
};
时间复杂度分析
-
时间复杂度:O(n),其中n是二叉树的节点数
-
分析:每个节点仅被访问一次,在访问每个节点时,哈希表的查找、插入和删除操作都是平均O(1)时间复杂度
空间复杂度分析
-
空间复杂度:O(n)
-
分析:
-
递归调用栈的深度在最坏情况下为O(n)(当树退化为链表时)
-
哈希表
cnt
在最坏情况下需要存储O(n)个不同的前缀和
-
关键点详解
-
前缀和哈希表设计
-
使用
unordered_map<long long, int>
存储前缀和及其出现次数 -
long long
类型确保大数计算不会溢出(节点值可达1e9,多个节点累加可能超过int范围) -
哈希表记录从根节点到当前节点的路径和出现的次数
-
-
核心算法逻辑
-
初始化:
cnt[0]=1
处理从根节点开始的路径 -
路径和更新:
sum += curnode->val
计算当前路径和 -
结果统计:查找
sum-targetSum
是否存在哈希表中 -
状态维护:递归前后正确更新哈希表
-
-
回溯机制
-
在递归返回时执行
cnt[sum]--
恢复状态 -
确保不同分支的路径计算不会相互干扰
-
维护哈希表仅包含当前路径的前缀和信息
-
边界情况处理
-
空树处理
if(!curnode) return;
直接返回,不影响结果计数
-
大数处理
long long sum; // 使用64位整数存储路径和
防止节点值累加溢出
-
负数和零处理
-
算法天然支持节点值为负数的情况
-
targetSum
为0的情况也能正确处理
-
常见问题解答
Q1:为什么哈希表要使用long long而不是int?
A:节点值范围是[-1e9,1e9],当多个节点值累加时可能超出int范围(约±2.1e9)。例如,三个1e9的节点相加就是3e9,超过了int最大值。使用long long(范围约±9e18)可以避免溢出问题。
Q2:为什么要初始化cnt[0]=1?
A:这表示存在一个空路径的和为0。这样当从根节点到某节点的路径和正好等于targetSum时,sum-targetSum=0
能在哈希表中找到匹配。例如,如果从根节点到当前节点的和正好是targetSum,我们需要能够统计这种情况。
Q3:为什么要在递归返回时执行cnt[sum]--?
A:这是回溯的关键步骤。当递归返回到父节点时,当前节点的路径和不应该再被其他分支的路径计算所使用。通过减少计数,我们确保哈希表只包含当前路径上的前缀和。
Q4:如何处理从任意节点开始的路径?
A:通过维护从根节点到当前节点的路径和sum,并检查sum-targetSum是否存在,我们实际上检查了所有以当前节点为终点的路径。哈希表记录的前缀和对应着路径的起点可能在树的任何位置。
算法正确性证明
-
完整性:算法遍历了树的所有节点,考虑了所有可能的路径
-
正确性:
-
当
sum-targetSum
存在于哈希表中时,说明存在一个前缀和,使得这两个节点之间的路径和等于targetSum -
回溯机制确保每个路径的计算都是独立的
-
-
终止性:递归必然终止,因为树是有限大小的
性能优化建议
-
迭代实现:可以用显式栈实现DFS,避免递归栈溢出
-
哈希表清理:对于特别深的树,可以定期清理哈希表中不必要的前缀和
-
并行计算:对于大型树,可以考虑并行处理不同子树
扩展应用
这种前缀和+哈希表的方法还可用于解决:
-
二叉搜索树中特定和的路径查找
-
二叉树中最大路径和问题
-
其他树形结构的路径统计问题
-
图形结构中类似的前缀和问题
respect!