剑指 Offer 34. 二叉树中和为某一值的路径

一、题目

题目链接:力扣

给你二叉树的根节点 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

主要思想:

  1. 当前节点不是叶子节点,那么,对于当前节点的左右子树来讲,当前节点可能是路径上的某点,全局数组需要记录路径;
  2. 如果当前节点左右子树搜索完成,表示最终可能符合条件的 通过当前节点的 所有路径 全部搜索完成,不会有其他路径经过这个节点,全局数组不再需要这个值,需要删除。

举个例子:当然是以根节点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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kashine

你的鼓励将是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值