提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
本文将围绕以下问题展开讨论
如何求解二叉树的路径总和问题(求解:二叉树中 路径之和 为某一定值的路径总数)?
本文将采用两种方法解决此问题(文章末尾附完整代码)
一、如何理解二叉树路径总和
题目链接: [点击跳转] Leetcode 437. 路径总和 III
如下是具体题目要求:
主体要求是需要使树上的节点之和等于给定的目标值 targetSum,除此之外这些节点还需要满足一些条件:
1.节点必须是相邻的。
2.必须是由上到下的(例如图中,即便是3-3-5-2-1这条路径之和满足目标值,也不能计入)
二、方法一(深度优先搜索)
两层递归
相信各位同学在读题时,脑海里直接就会想到这个方法:
将二叉树中的全部非叶节点,全部设为根节点,再依此向下遍历两侧路径,每次遇到路径之和等于目标值的时候,就使计数器+1。
如此一来,将树中的所有节点全部遍历一遍就会得到我们想要的路径总数。
代码如下(示例):
首先我们定义了一个函数pathSum,这个函数通过传入根节点,以及目标值,就可以返回我们所需的路径总和。
int pathSum(TreeNode* root, int targetSum) {
return ans;
}
然后,我们考虑到,如果传入的节点为空,那么我们需要返回一个 0 ,用来表示没有路径。
int pathSum(TreeNode* root, int targetSum) {
if(root==nullptr){
return 0;
}
return ans;
}
好了,现在我们应该思考如何去实现遍历子树的路径了,于是我们定义了一个递归函数length,通过传入节点和目标和,返回 该根节点下的 满足要求的路径数 。运用递归的方式,将该节点下的左侧节点和右侧节点传入,同时减去根节点的值。
举例说明:
当我们找符合目标值为12的路径时,先找到7,然后接着向下搜索3和4,这个时候,我们就不需要再找targetSum=12的情况了,我们可以直接以3或者4为根节点,去找targetSum=5的情况(5=12-7)
7
/ \
3 4
/ \
2 1
同时设置一个计数器sum,从上到下搜索时,每次将targetSum减去的根节点值,所以当targetSum等于当前节点值的时候,就正好是到当前节点的路径和等于初始的targetSum,因此我们可以得到如下代码:
int length(TreeNode* root,long targetSum){//这里target用long类型
int sum=0;//记录每个根节点下子树路径数
if(root==nullptr){
return 0;
}
if(targetSum==root->val){
sum++;
}
sum=sum+length(root->left,targetSum-root->val);
sum=sum+length(root->right,targetSum-root->val);
return sum;
}
这个时候我们已经解决了如何以一个固定根节点搜索路径数的问题了。接下来我们们就需要考虑到,符合题意的路径,不一定是经过最上方的根节点的,也有可能是在二叉树的中间某段出现。
因此我们可以再次运用递归的方式,将二叉树中所有的节点,都以根节点的方式处理,找到所有的路径,代码如下:
int ans=0;//全局变量,统计所有根节点下的路径
int pathSum(TreeNode* root, int targetSum) {
if(root==nullptr){
return 0;
}
ans=length(root,targetSum);
ans=ans+pathSum(root->left,targetSum);
ans=ans+pathSum(root->right,targetSum);
return ans;
}
完整代码如下:
/**
* 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:
int ans=0;//全局变量,统计所有根节点下的路径
int length(TreeNode* root,long targetSum){//这里target用long类型
int sum=0;//记录每个根节点下子树路径数
if(root==nullptr){
return 0;
}
if(targetSum==root->val){
sum++;
}
sum=sum+length(root->left,targetSum-root->val);
sum=sum+length(root->right,targetSum-root->val);
return sum;
}
int pathSum(TreeNode* root, int targetSum) {
if(root==nullptr){
return 0;
}
ans=length(root,targetSum);
ans=ans+pathSum(root->left,targetSum);
ans=ans+pathSum(root->right,targetSum);
return ans;
}
};
三、方法二(前缀和)
前面所讲的方法的虽然可以很清晰地将所有节点搜索后,解出路径总和,但是由于我们先运用一次递归(将所有节点都设置为一遍根节点),又运用一次递归(将根节点下的所有正确路径找到),节点被多次重复搜索,因此我们的时间复杂度为O(n^2)。
那么有没有一种方法可以减少重复的路径计算呢?
前缀和
我们可以通过map<key,value>,将路径的长度用key保存下来,用value来储存这一长度的路径的数量。
代码如下(示例):
unordered_map<long,int> mapx;//用来统计,长度为x的路径,一共有y条
int ans=0;
int pathSum(TreeNode* root, int targetSum) {
mapx[0]=1;//初始化,表示路径长为0的有1条
return ans;
}
现在,我们需要思考,传入 根节点root 和 目标值targetSum 后,返回的ans 就是我们想要得到的路径总和,那么应该如何实现呢?
现在我们观察下图:
targetSum=11
5
/ \
4 8
/ / \
2 3 1
我们可以很轻松地找到这样两条路径5-4-2和8-3
在寻找5-4-2这条路径时,与方法一相同:
我们先找到5这个节点,然后再找他的子节点4;计算5+4=9,而9!=11,所以5-4不是我们要找的。
接下来继续找2这个节点,计算9+2=11,11==11,因此找到了5-4-2这个路径。
那么在找8-3这条路径时,如果也按照方法一,必然会从5开始,到8,然后再到3或1,我们发现,在计算从5开始的路径长度时,我们一开始就把5加了进去,所以是一定找不到8-3这个路径的。
所以有没有办法在计算从5开始的路径时,在后续计算中,我们还可以减去5(即路径不从5开始)
targetSum=11
5
/ \
4 8
/ / \
2 3 1
如上图,5-8-3=16这条路径是不符合的,但是8-3=11是符合的。
在实际操作中我们没有办法直接计算8+3=11,我们只能通过5+8+3再减去5,才能得到11这个数字。
然而现在的问题并不是如何得到11这个数字,而是在给出 targetSum=11 后,找到谁是这个11的根节点(在此图中需要找到8这个根节点)
因此用 总路径长sum-targetSum 就可以得到这个节点
unordered_map<long,int> mapx;//用来统计,长度为x的路径,一共有y条
int ans=0;
long sum=0;
void length(TreeNode* root,int targetSum){
if(root==nullptr){
return ;
}
sum+=root->val;//总路径长度+当前节点
if(mapx.count(sum-targetSum)){//判断总路径长减去目标值是否在mapx中出现
ans+=mapx[sum-targetSum];//如果出现,说明子树中有符合的路径
}
mapx[sum]++;//将当前的路径长度存入mapx的key中,value从0变为1
length(root->left,targetSum);
length(root->right,targetSum);
mapx[sum]--;//回溯
sum -= root->val;
}
这里解释一下回溯:如何进行回溯的?我们为什么要回溯?
mapx[sum]--;//回溯
sum -= root->val;
}
targetSum=11
5
/ \
4 8
/ / \
2 3 1
在5-4-2这条路径上,当访问到节点2时,需要再向上返回(因为我们不知道经过节点4是否有其他可行路径)
执行mapx[sum]–,将mapx[11]的值减1
执行sum -= root->val,将路径回退一次
(不用担心最后会少算一条路径,因为我们的路径总数是用ans来储存的)
完整代码如下
/**
* 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,int> mapx;//用来统计,长度为x的路径,一共有y条
//mapx[0]=1;//初始化,表示路径长为0的有1条
int ans=0;
long sum=0;
void length(TreeNode* root,int targetSum){
if(root==nullptr){
return ;
}
sum+=root->val;
if(mapx.count(sum-targetSum)){
ans+=mapx[sum-targetSum];
}
mapx[sum]++;
length(root->left,targetSum);
length(root->right,targetSum);
mapx[sum]--;
sum -= root->val;
}
int pathSum(TreeNode* root, int targetSum) {
mapx[0]=1;//初始化,表示路径长为0的有1条
length(root,targetSum);
return ans;
}
};
时间复杂度为O(n)