[原题]
给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
[示例1]
输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和等于 8 的路径有 3 条,如图所示。
[示例2]
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:3
[解题思路]
-深度优先搜索-
这是一道基于二叉树的算法题,要求我们统计沿父节点->子节点方向的路径中相邻节点总和为targetSum的路径总数,我们称其为有效路径。根据题意,每一条有效路径的起点既可以是二叉树的根节点,也可以是其他任意非空节点。
接下来,让我们用dfs的方法设计一个辅助函数,当传入参数起始节点的指针t和目标数targetSum时,能够得到对应有效路径的总数。
二叉树的遍历,势必涉及到调用函数递归。而递归则需要考虑两个重要的因素:递归终止条件及函数内部的自我调用。
显然,整个过程需要从传入的起始节点开始不断递归,直到指针t最终指向null时终止递归,此时不再具有有效路径,返回0。
接着,我们便需要考虑函数自我调用时满足的条件。由于我们需要查找相邻且和为targetSum的节点,那么一定满足起始节点值+子节点1值+子节点2值+......+子节点n值==targetSum,因此,我们可以对传入的参数targetSum与节点值t->val不断作差,直到targetSum与某个节点值相等时则可认为存在有效路径。
因此,该函数的返回值由三个部分组成:(1)若该节点传入的targetSum与节点值t->val相等,则有效路径数+1 (2)左子节点的有效路径数 (3)右子节点的有效路径数
这样,有效路径数不断向上一层传递,最终就可以得到包含起始节点的有效路径总数。
接下来,我们以示例1中所给二叉树值为5的节点作为起始节点,演示一下该函数调用的过程。
这段代码如下:
long dfsOfNode(TreeNode* t, long targetSum)
{
if (!t) return 0;
return dfsOfNode(t->left, targetSum - t->val) + dfsOfNode(t->right, targetSum - t->val) + (t->val == targetSum ? 1 : 0);
}
现在,我们完成了辅助函数的编写,然而,正如之前所分析的,任何一个非空节点都可能作为一个有效路径的起始节点,因此我们需要递归访问每个节点,获得其作为起始节点时的有效路径数。
同样地,让我们用递归的方式来完成pathSum的函数编写:若根节点不为空,返回值由三部分组成:(1)根节点作为起始节点时的有效路径数 (2)左子节点调用pathSum递归访问获取的有效路径数 (3)右子节点调用pathSum递归访问获取的有效路径数
最终,可以得到如下代码:
class Solution {
long dfsOfNode(TreeNode* t, long targetSum)
{
if (!t) return 0;
return dfsOfNode(t->left, targetSum - t->val) + dfsOfNode(t->right, targetSum - t->val) + (t->val == targetSum ? 1 : 0);
}
public:
int pathSum(TreeNode* root, int targetSum) {
if (!root) return 0;
return dfsOfNode(root, targetSum) + pathSum(root->left, targetSum) + pathSum(root->right, targetSum);
}
};
其时间复杂度为o(n^2),由于递归需要栈存储,其空间复杂度为o(n)。
-前缀和+深度优先搜索+回溯-
显然,刚才的方法中经过了许多次重复的遍历,我们能否找到时间复杂度更低的方案呢?这里,我们可以采用前缀和+dfs+回溯的方法,大大降低时间消耗。
前缀和,即该节点与之前所有节点值的总和。在这里,我们选择使用散列表来统计每一个节点的前缀和及每个前缀和出现的总次数。在单一方向的路径下,任意两个节点间的前缀和之差,就是两个节点值的差值。利用这一原理,我们只需要在单一方向的路径上,寻找前缀和差值为targetSum的两个节点,就可以统计有效路径的个数了。
当然,这里有两点需要我们注意。
首先,由于我们遍历的顺序总是沿着父节点->子节点方向的,因此在判断一个节点是否为有效路径时,只需要看其前缀和与目标和targetSum的差值是否被散列表统计过即可。
其次,我们在整个过程中需要始终按照单一方向的原则去统计前缀和,如果散列表中存放着二叉树中多个分支的前缀和,那么统计也就失去了原有的意义。因此,我们需要在每一层递归调用时对散列表进行回溯(即返回该层函数调用前的状态)。
我们仍旧以示例1为例,作出最先遍历的单一方向路径示意图:
剩余的一些细节我会以批注的形式呈现出来,接下来就是代码部分:
class Solution {
int ans;//用来记录有效路径总数
int _target;//存储最初的targetSum
//传入参数:节点指针t、该节点之前的前缀和presum、记录前缀和对应出现次数的散列表prefix
void dfs(TreeNode* t, long int presum, unordered_map<long int, int>& prefix)
{
//当指针为空时,直接返回
if (!t) return;
//计算出该节点的前缀和
presum += t->val;
//有效路径的判断,若成立则ans加上对应前缀和出现的次数
if (prefix[presum - _target])
ans += prefix[presum - _target];
//统计本节点前缀和
prefix[presum]++;
//递归访问左右节点
dfs(t->left, presum, prefix);
dfs(t->right, presum, prefix);
//回溯
prefix[presum]--;
}
public:
int pathSum(TreeNode* root, int targetSum) {
//散列表的声明及成员变量初始化
unordered_map<long int, int> prefix;
_target = targetSum;
ans = 0;
//若某个节点本身值就与目标和相等,两者作差为0,但应认为是有效路径,故初始化prefix[0]为1
prefix[0] = 1;
//dfs函数的调用,根节点之前无前缀和,故第二个参数直接传入0
dfs(root, 0, prefix);
//返回ans
return ans;
}
};
最终,时间复杂度o(n),空间复杂度o(n)。