LeetCode 树 路径总和相关题目总结
LeetCode树的路径总和相关题目总结
1、LeetCode 112 路径总和 easy
这道题属于路径和相关题目中最基本的一道,所以也是比较适合用来分析思路的,不用考虑其他细节,影响整体思路。
分析:
确定思路 一条路径是指从根节点到一个叶子节点的节点通路,因此可以借助二叉树的遍历思想,其中先序遍历是从根节点开始,一直遍历到叶子节点再回溯寻找第二个叶子节点;因此,用在这里很合适。刚刚好遍历完所有的节点并且遵从根节点层层深入到叶子节点的顺序。其实,在这里,先序遍历就是二叉树的深度优先搜索,所以我暂且把路径相关的题目与二叉树的深度优先搜索思想联系起来。
递归实现 递归算法的写法主要分为两部分,首先是设定递归终止条件,然后设置递归入口,然后根据二叉树的先序遍历确定递归结构(先访问root节点,然后先后递归遍历左子树和右子树)。本例中,前两个if是递归终止的条件,根据题意整理一下,应该不难明白吧;后面分别递归左子树和右子树,只不过这里递归调用中的sum参数发生了变化,利用了减法思想,每一次递归调用,相当于当前路径前进一步。函数入口参数没有cursum这一参数,所以这里不计算先前路径中节点值的累加值cursum,而是将sum参数变成sum-root->val,减法思想也等价于调用前计算cursum。
减法思想+深度优先搜索递归代码
bool hasPathSum(TreeNode* root, int sum) {
if(!root) //前两个if为递归终止条件
return false;
if(!root->left&&!root->right&&sum==root->val)
return true;
if(hasPathSum(root->left,sum-root->val)) //后两个if开始递归
return true;
if(hasPathSum(root->right,sum-root->val))
return true;
return false;
}
计算cursum+深度优先搜索递归代码
由于参数个数的原因,仅仅对_hasPathSum()函数做了一层封装
bool hasPathSum(TreeNode* root,int sum){
return _hasPathSum(root,sum);
}
bool _hasPathSum(TreeNode* root, int sum,int cursum=0) {
if(!root)
return false;
cursum+=root->val; //更新cursum
if(!root->left&&!root->right&&cursum==sum)
return true;
if(_hasPathSum(root->left,sum,cursum)) //传入更新后的cursum
return true;
if(_hasPathSum(root->right,sum,cursum))
return true;
cursum-=root->val; //回溯过程中对cursum的复原
return false;
}
以上是两个递归版本。我们知道,一般而言,递归总是可以转化为迭代,先序遍历和深度优先搜索都是有迭代版本的,所以很自然的想到,本题也有迭代版本。
迭代实现迭代实现可以根据二叉树先序遍历的迭代版本改编,主要由前进到叶子节点+判断cursum是否等于sum+回溯三部分组成,如果与先序遍历的迭代版本一起学习更好!
bool hasPathSum(TreeNode* root,int sum){
stack<TreeNode* > s;
TreeNode *cur=root, *pre=NULL;
int cursum=0;
while (!s.empty()||cur) //前进至叶子节点
{
while(cur){
s.push(cur);
cursum+=cur->val;
cur=cur->left;
}
cur=s.top();
if(!cur->left&&!cur->right&&sum==cursum) //判断sum==cursum
return true;
if{cur->right&&pre!=cur->rgiht} //if...else组成是否回溯的条件判断
cur=cur->right;
else{
pre=cur;
cursum-=cur->val;
s.pop();
cur=NULL;
}
}
return false;
}
2、LeetCode 113 路径总和 II medium
这道题的思路与112题的整体思路完全一致,不过较上一题复杂之处在于此题的返回值不再是遇到true就返回,而是需要返回符合从根节点到叶子节点的路径中cursum==sum的完整路径,因此递归调用时,需要额外考虑参数传递和保存。
递归实现递归实现跟112题仍然类似,但是递归调用中,我们需要多次对返回的vector<vector<int>> res进行push_back()操作。为了确保仅对最上层调用方的vector<vector<int>> res进行操作,所以仅仅只能采用传引用的方式保证对同一个res向量进行操作。
递归实现+减法思想代码
vector<vector<int>> pathSum(TreeNode* root, int sum) {
vector<vector<int>> res;
vector<int> tmp;
dfs(root, res, tmp, sum); //对res传引用,确保每次都是对pathSum()内的res进行操作
//dfs_1(root,res,tmp,sum);
//dfs_2(root,0,res,tmp,sum);
return res;
}
void dfs(TreeNode* root, vector<vector<int>>& res, vector<int>& tmp, int sum) { //深度优先搜索 tmp传引用版本
if (!root)
return;
tmp.push_back(root->val);
if (!root->left && !root->right) {
if (root->val == sum)
res.push_back(tmp);
}
dfs(root->left, res, tmp, sum - root->val); //减法思想
dfs(root->right, res, tmp, sum - root->val);
tmp.pop_back();
}
void dfs_1(TreeNode* root, vector<vector<int>>& res, vector<int> tmp, int sum) { //深度优先搜索 tmp传值版本
if (!root)
return;
tmp.push_back(root->val);
if (!root->left && !root->right) {
if (root->val == sum)
res.push_back(tmp);
}
dfs_1(root->left, res, tmp, sum - root->val); //减法思想
dfs_1(root->right, res, tmp, sum - root->val);
}
void dfs_2(TreeNode* root, int cursum, vector<vector<int>>& res, vector<int>& tmp, int sum) {
if (!root) //深度优先搜索 更新cursum版本
return;
cursum+= root->val;
tmp.push_back(root->val);
if (!root->left && !root->right) {
if (cursum== sum)
res.push_back(tmp);
}
dfs_2(root->left, cursum, res, tmp, sum); //计算更新cursum
dfs_2(root->right, cursum, res, tmp, sum);
cursum-= root->val;
tmp.pop_back();
}
注意到,这里的dfs_1()中vector<int> tmp并没有传引用,而是采用了传值,在每一次dfs()调用时中都将产生临时副本。每一次dfs_1()调用中,都是对临时变量tmp进行操作,所以调用结束后不用复原。实参完成传值之后,再对实参进行修改将不影响传递到形参的内容,所以这里递归层层返回的回溯过程中,不用对tmp进行pop_back()操作,每一次调用dfs()函数,都会产生临时的tmp,当前dfs()执行完毕就释放tmp,对其余dfs()内的tmp没有任何影响 ;而传引用则不一样,需要在回溯过程中对已经修改的tmp复原。总体来讲,对tmp传引用,效率更高,但需要在dfs()最后添加pop_back()以还原,传值则效率低一些。
3、LeetCode 437 路径总和 III easy
此题与之前两题不一样的地方可以有两种不同理解方式,因此也产生了两种不同的解题解法:
A理解深度优先搜索产生的路径不要求从根节点开始到叶子节点结束,可以从任何节点开始,到叶子节点结束;因此,我们要以每一个节点为根节点执行深度优先搜索,就可以达到目的。这也会产生双重递归。
int dfs(TreeNode* root,int sum){ //深度优先搜索
int Count=0;
if(!root)
return Count;
if(sum==root->val)
Count+=1;
return Count+=(dfs(root->left,sum-root->val)+dfs(root->right,sum-root->val));
}
int pathSum(TreeNode* root, int sum) { //对每一个节点执行深度优先搜索
int Count=0;
if(!root)
return 0;
if(root)
Count+=dfs(root,sum);
return Count+=(pathSum(root->left,sum)+pathSum(root->right,sum));
}
B理解在于我只对以root为根节点的二叉树执行一次深度优先搜索,但是在搜索过程中不仅仅记录cursum==sum的路径,而且记录cursum中子路径满足subcursum==sum的子路径。听起来很绕,举个例子,比如当前搜索到了cursum(x1->x2->x3->x4),它的子路径subcursum包含(x1->x2->x3->x4)、(x2->x3->x4)、(x3->x4)、(x4),我们需要将这四条路径的节点累加和分别与sum比较,满足subcursum==sum就记录下来。
int pathSum(TreeNode* root, int sum) {
vector<int> s;
int Count=0,cursum=0;
dfs(root,Count,sum,cursum,s);
return Count;
}
void dfs(TreeNode* root,int& Count,int sum,int cursum,vector<int>& s){
if(!root)
return;
s.push_back(root->val);
cursum+=root->val;
if(cursum==sum) Count++;
int subcursum=cursum;
for(unsigned int i=0;i<s.size()-1;i++){ //for循环计算是否有子路径满足subcursum==sum
subcursum-=s[i];
if(subcursum==sum)
Count++;
}
dfs(root->left,Count,sum,cursum,s);
dfs(root->right,Count,sum,cursum,s);
s.pop_back();
}
for循环只执行s.size()-1次是因为当执行第s.size()次时,subcursum==0,如果sum也等于0,则无法区分。
每搜索一个新的节点都将执行一次for循环,时间复杂度很高,属于暴力解法;这里可以有更好的方式替代for循环。
hash_map+深度优先搜索
如果我们将每一个节点对应的cursum值和他出现的次数组成一个(key-value)键值对,那么每次遍历新节点的时候,计算cursum并在hash_map中存储这个键值对。然后,在遍历下一个节点的时候,上一个节点的cursum就成了oldcursum,并且当前hash_map中已经包含了从根节点到此节点之前的路径上所有的oldcursum,这时候,通过查找m[cursum-sum]的值,便知道符合oldsum+sum=cursum的路径有几条。例如:当前遍历的路径为(x1->x2->x3);在遍历到x4的时候,此前哈希表中已经存储了m[x1]=1;m[x1+x2]=1;m[x1+x2+x3]=1;如果,sum=x3+x4,那么,满足cursum=oldsum+sum的oldsum值为(x1+x2),哈希表中已经记录了此oldcursum,并且出现的次数为1,所以,共有一条路径满足。这样,每遍历到一个新节点,只需要查表m[cursum-sum]和记录m[cursum]++就可以。在函数最后,应该执行m[cursum]–1,避免当前路径上的节点的cursum值成为另一条路径(通过回溯到另一路径)上节点的oldcursum。
因为所有dfs()维护的都是同一张hash_map,所以应该传引用。
int pathSum(TreeNode* root, int sum) {
unordered_map<int, int> m;
m[0] = 1;
return findPath(root, sum, 0, m);
}
int dfs_1(TreeNode* root, int sum, int cursum, unordered_map<int, int>& m) {
if (!root)
return 0;
cursum += root->val;
int tmp = m[cursum - sum];
m[cursum]++;
tmp += dfs_1(root->left, sum, cursum, m) + dfs_1(root->right, sum, cursum, m);
m[cursum]--;
return tmp;
}
4、LeetCode 257 二叉树的所有路径
虽然这道题与总和没关系,但毕竟和路径有关系,所以也是需要用到深度优先搜索思想;就放到这里一起总结啦!
因为思想类似,而且更简单,直接放上代码吧!
递归代码
vector<string> binaryTreePaths(TreeNode* root) {
vector<string> res;
string tmp;
dfs(root, res, {});
//dfs_1(root,res,tmp);
return res;
}
void dfs(TreeNode* root, vector<string>& res, string tmp) { //对tmp传值
if (!root)
return;
if (root) {
tmp.append(to_string(root->val));//将int转化为string,注意,to_string()只有在C++11及其之后标准才支持,VS2017可以编译通过
tmp.append("->");
}
if (!root->left && !root->right) {
tmp.pop_back();
tmp.pop_back();
res.push_back(tmp);
}
dfs(root->left, res, tmp);
dfs(root->right, res, tmp);
}
void dfs_1(TreeNode* root, vector<string>& res, string& tmp){ //对tmp传引用
if(!root)
return;
if(root){
tmp.append(to_string(root->val));
tmp.append("->");
}
if(!root->left&&!root->right){
tmp.pop_back();
tmp.pop_back(); //删掉末尾的"->"两个char
res.push_back(tmp); //将此完整路径插入res
}
dfs_1(root->left,res,tmp);
dfs_1(root->right,res,tmp);
tmp.pop_back(); //回溯复原tmp
while(!tmp.empty()&&tmp.back()!='>')
tmp.pop_back();
}
迭代版本代码
vector<string> binaryTreePaths(TreeNode* root) {
vector<string> res;
string tmp;
stack<TreeNode*> s;
TreeNode *pre=NULL,*cur=root;
while(cur||!s.empty()){ //前进
while(cur){
tmp.append(to_string(cur->val));
tmp.append("->");
s.push(cur);
cur=cur->left;
}
cur=s.top();
if(!cur->left&&!cur->right) //插入当前路径
{
tmp.pop_back();
tmp.pop_back();
res.push_back(tmp);
}
if(cur->right&&pre!=cur->right)
cur=cur->right;
else{ //回溯
pre=cur;
s.pop();
tmp.pop_back();
while(!tmp.empty()&&tmp.back()!='>')
tmp.pop_back();
cur=NULL;
}
}
return res;
}