一、题目
题目链接:力扣
给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
叶子节点 是指没有子节点的节点。
示例 1:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:[[5,4,11,2],[5,8,4,5]]
示例 2:
输入:root = [1,2,3], targetSum = 5
输出:[]
示例 3:
输入:root = [1,2], targetSum = 0
输出:[]
提示:
- 树中节点总数在范围 [0, 5000] 内
- -1000 <= Node.val <= 1000
- -1000 <= targetSum <= 1000
听说这里有个回溯模板:力扣
二、题解
1、思路
🤠 深度优先搜索 DFS
主要思想:
- 当前节点不是叶子节点,那么,对于当前节点的左右子树来讲,当前节点可能是路径上的某点,全局数组需要记录路径;
- 如果当前节点左右子树搜索完成,表示最终可能符合条件的 通过当前节点的 所有路径 全部搜索完成,不会有其他路径经过这个节点,全局数组不再需要这个值,需要删除。
举个例子:当然是以根节点5为例,dfs遍历过程中,首先将根节点值5加入数组,左子树、右子树可能都需要使用到这个值,因此,进入左子树、右子树时不需要从数组中删除这个值。当根节点的左右子树全部搜索完成,表示以后搜索的所有路径中都不应该再包含根节点的值,所以需要从全局数组中删除。
我们可以采用深度优先搜索的方式,枚举每一条从根节点到叶子节点的路径。当我们遍历到叶子节点,且此时路径和恰为目标和时,我们就找到了一条满足条件的路径。
思路还能想到,但是真的不容易实现。
注意:只能使用先序遍历方法!
因为一定要先知道根节点的值。比如下面的树,如果是使用中序遍历,无法实现从根节点一直减减减,减到叶子节点,然后进行判断;如果是后续遍历就更离谱了,所有节点都遍历完了,才知道根节点。
下面代码dfs函数主要作用:
找出root为根节点的树,所有从根到叶子节点路径总和等于target的路径。
进入递归后,相当于,在4为根节点的左子树中,找target - 5的路径,并且在8为根节点的左子树中,找target - 5的路径,然后取并集。......
最终,递归到所有的叶子结点,即判断叶子节点是否为target - 前面路径的值。
🤠 广度优先搜索 BFS
按照层次遍历的方式,用两个队列,一个记录当前结点的左右子节点,一个记录到此节点为止的路径和,遍历到叶子之后,如果路径和为target,那么通过哈希表中保存的父节点回溯到根部,找到路径中所有结点。
2、代码实现
🤠 深度优先搜索 DFS
代码中的target怎么直接减了,怎么重用?
所有的target都是形参,临时变量,值传递,会拷贝一份。从根节点开始看,target - 5后,dfs函数对左子树和右子树进行处理,target变为target - 5,也就是说,根节点左孩子和右孩子都继承了一个临时变量,互不干扰。
对于左下角的7和2两个叶子结点,他们所继承的target相等,想象在dts函数中,root指向11,target进来之后减去11,然后判断,再使用dfs对于左子树的处理(进入7之后target减去7,此时的target和2无关,形参值传递,2中仍是减去11之后的target),使用dfs对于右子树的处理(进入2之后target减去2,此时的target和7无关,形参值传递),对于7、2从11继承的target相等。
也就是说两个结点继承于同一个值(父节点),这两个节点从父节点继承的值相等。
只有一个path,如果有多条路径满足条件怎么办?
看右侧红框,root指向7(叶子)的时候,将7添加到数组path中判断,5+4+11+7不等于target,那么不满足条件,开始dfs函数遍历7的左右子树,当然是直接返回,下面代码关键一句,pop_back()将7弹出。此时root回到指向11的dfs函数体内,经过前面的步骤,dfs处理左子树7已经处理完成,该使用dfs函数处理右子树(叶子)2,发现与target相等,将path添加到最终结果中,继续判断2的左右子树,为空直接返回。
也就是说处理完某个节点的左右子树后,会把root指向的该节点值从path中删除。
root指向11的时候,dfs处理完左子树,将7从path中删除,dfs处理完右子树的时候,将2删除,再将11删除。
class Solution {
public:
vector<vector<int>> pathSum(TreeNode* root, int target) {
dfs(root, target);
return res;
}
private:
vector<vector<int>> res;// 最终结果
vector<int> path;// 路径的值
// 找出root为根节点的树,所有从根到叶子节点
// 路径总和等于target的路径
void dfs(TreeNode* root, int target)
{
// 当前为空直接返回,因为没有值val(截止条件)
if(root == NULL)return;
// 记录所经历路径值,记录当前root
path.emplace_back(root->val);
// 已经经历过root,减去
// 理解为所有路径中一定要经过root,现已将root的val放入数组,只需要在root左右子树递归寻找是否存在根到叶子的路径和为target - root->val
target -= root->val;
// 判断根到叶子的和是否为目标和
if(root->left == NULL && root->right == NULL && target == 0)res.emplace_back(path);
// 找出root左右子树,所有从根到叶子路径总和等于target的路径
// 至此,target = target - 已经减去所有经历过结点值
dfs(root->left, target);// 遍历左子树
// 左子树遍历不会改变下面的target值,target是值传递形参
dfs(root->right, target);// 遍历右子树
// 关键中的关键
// 左、右子树经过该节点的路径已经检验完,该找到的答案已经找到
// 不再有路径包含该节点,因此直接丢弃,方便其他搜索使用path变量
// 第一次运行到这里的一定是叶子
path.pop_back();
}
};
第二次的写法:
class Solution {
public:
vector<vector<int>> pathSum(TreeNode* root, int target) {
vector<int> path;// 用的是局部变量,因此即便不满足target减到叶子节点为0,也无需弹栈,因为都是值传递,不会对另一条路径产生任何影响
dfs(root, target, path);
return res;
}
private:
vector<vector<int>> res;
void dfs(TreeNode* root, int target, vector<int> path)
{
// 终止条件
if(root == NULL)return;
// 减去当前值并记录路径
target -= root->val;
path.emplace_back(root->val);
// 判断是否是叶子 并且路径值和刚好等于target
if(root->left == NULL && root->right == NULL && target == 0)res.emplace_back(path);
dfs(root->left, target, path);
dfs(root->right, target, path);
}
};
🤠 广度优先搜索 BFS
别看代码长,其实比上面好理解。。。
易错点:容易误把根节点root当做当前根节点node。
class Solution {
public:
vector<vector<int>> pathSum(TreeNode* root, int target) {
if(root == NULL)return res;
queue<TreeNode*> node_que;// 记录下层结点
queue<int> sum_que;// 记录对于下层结点 经过路径和
node_que.push(root);
sum_que.push(0);
while(!node_que.empty())
{
// 拿出当前层一个结点
TreeNode* node = node_que.front();
node_que.pop();// 表示这个结点已经得到处理
// 计算需要传给下层结点的已经历路径和
int sum_temp = sum_que.front() + node->val;
sum_que.pop();// 表示加上这个结点的路径和计算完毕
// 路径和计算完毕,判断当前结点是否是满足条件的结点
if(node->left == NULL && node->right == NULL)
{
if(sum_temp == target)get_path(node);// 回溯寻找路径,并加入结果
}
// 如果不是继续查找
if(node->left != NULL)
{
// 为下层结点记录父节点
parent[node->left] = node;
// 记录下层结点和已经过的路径
node_que.emplace(node->left);
sum_que.emplace(sum_temp);
}
if(node->right != NULL)
{
parent[node->right] = node;
node_que.emplace(node->right);
sum_que.emplace(sum_temp);
}
}
return res;
}
private:
vector<vector<int>> res;
// 哈希表用来记录父节点
unordered_map<TreeNode*, TreeNode*> parent;
// 上面搜寻到符合条件的根节点后,调用此函数回溯哈希表,找路径加入res
void get_path(TreeNode* node){
vector<int> path;
path.emplace_back(node->val);// 先把叶子放进去
// 如果能找到父节点,将父节点的值插入
while(parent.find(node) != parent.end())
{
path.emplace_back(parent[node]->val);// 将父节点的值插入
node = parent[node];// 更新node
}
reverse(path.begin(), path.end());
res.emplace_back(path);
}
};
不使用哈希表存储父节点,而是在找到叶子节点后,自上到下寻找叶子节点所在路径。但很遗憾超出时间限制,但是测试案例全部通过。
class Solution {
public:
vector<vector<int>> pathSum(TreeNode* root, int target) {
if(root == NULL)return res;
queue<TreeNode*> node_que;
queue<int> sum_que;
vector<int> path;
node_que.push(root);
sum_que.push(0);
while(!node_que.empty())
{
int num = node_que.size();
for(int i = 0; i < num; i++)
{
TreeNode* node = node_que.front();
int sum_temp = sum_que.front();
sum_temp += node->val;
node_que.pop();
sum_que.pop();
if(node->right == NULL && node->left == NULL && sum_temp == target)recur(root, node, path);
if(node->left != NULL)
{
node_que.push(node->left);
sum_que.push(sum_temp);
}
if(node->right != NULL)
{
node_que.push(node->right);
sum_que.push(sum_temp);
}
}
}
return res;
}
private:
vector<vector<int>> res;
// 回溯查找会超时!
void recur(TreeNode* root, TreeNode* yezi, vector<int> path)
{
if(root == NULL)return;
path.emplace_back(root->val);// 记录路径
if(root == yezi)res.emplace_back(path);
recur(root->left, yezi, path);
recur(root->right, yezi, path);
}
};
3、复杂度分析
🤠 深度优先搜索 DFS
时间复杂度:O(n2);最坏时间复杂度,应该是指添加到结果中时,要把当前的结果复制进去,这个过程仅对一个节点就会有O(N)复杂度了,题解下方好多人讨论这个。
空间复杂度:O(n)。
🤠 广度优先搜索 BFS
时间复杂度:O(n2);同上。
空间复杂度:O(n)。哈希表,队列。
4、运行结果
🤠 深度优先搜索 DFS
🤠 广度优先搜索 BFS