Day18【二叉树】513.找树左下角的值、112.路径总和、113.路径总和ii、106.从中序与后序遍历序列构造二叉树、105.从前序与中序遍历序列构造二叉树

文章介绍了使用层序遍历和递归回溯方法解决二叉树问题,包括找到树的最后一层最左边的节点值以及路径总和问题。通过两种不同的回溯实现方式展示了如何寻找最底层最左边的节点,并讨论了在递归过程中何时使用回溯以及如何优化回溯过程。此外,还提供了从前序与中序遍历序列构造二叉树的递归解法。
摘要由CSDN通过智能技术生成

513.找树左下角的值

题目链接/文章讲解/视频讲解

即找到最后一层从左到右第一个节点的值,首先想到层序遍历

回顾一下层序遍历:需要通过队列进行迭代、遍历节点应该有push子节点的操作、外层循环遍历层内层循环遍历节点......

代码如下 

/**
 * 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 findBottomLeftValue(TreeNode* root) {
        queue<TreeNode*> que;
        int res;
        if (root)
            que.push(root);
        while (!que.empty()) {    // 当队列非空,就要一层一层遍历了
            int size = que.size();
            bool firstFlag = true;    // 遍历该层节点前的准备工作,比如即将遍历的层有多少个节点、标记每层首个节点的标识
            for (int i = 0; i < size; ++i) {    // 开始遍历该层的每一个节点
                TreeNode * node = que.front();
                que.pop();
                if (firstFlag) {
                    res = node -> val;
                    firstFlag = false;
                }    // 记录每层的第一个节点进res
                if (node -> left) que.push(node -> left);
                if (node -> right) que.push(node -> right);    // 遍历每个节点需要push其子节点
            }
            
        }
        return res;    // 最后遍历到了最后一层,res存的是最后一层的第一个节点
    }
};

用迭代法进行层序遍历似乎很简单

有没有办法用递归

我们要找到最底层的最左边的节点,即我们要找到深度最大的最左边的叶子节点

那么如何找最左边的呢?只要保证优先左边搜索,然后记录深度最大的叶子节点,此时就是树的最后一行最左边的值(这样的话,每遍历到深度大于当前已记录的深度最大值的节点时,该节点一定是所遍历过的最深层的最左边节点

本题要用回溯去做

/**
 * 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 maxDepth = INT_MIN;
    int depth = 1;
    int res;
    int findBottomLeftValue(TreeNode* root) {
        backtracking(root);
        return res;
    }
    void backtracking(TreeNode * root) {
        if (root -> left == nullptr && root -> right == nullptr)    // 回溯的终止条件,到达了叶子节点
        {
            if (depth > maxDepth) { 
                maxDepth = depth;
                res = root -> val;    // 做记录
            }
            return;
        }
        if (root -> left) {    // 没到叶子节点,就要继续遍历其子节点
            depth++;
            backtracking(root -> left);
            depth--;    // 回溯
        }
        if (root -> right) {
            depth++;
            backtracking(root -> right);
            depth--;    // 回溯
        }

    }
};

另一种回溯的写法,思路有点儿像Day17中的二叉树的所有路径

class Solution {
public:
    int maxDepth = INT_MIN;
    int depth = 0;
    int res;
    int findBottomLeftValue(TreeNode* root) {
        backtracking(root);
        return res;
    }
    void backtracking(TreeNode * root) {
        depth++;    // 进入当前节点了,需要将深度+1
        if (root -> left == nullptr && root -> right == nullptr)
        {    // 如果到达了叶子节点
            if (depth > maxDepth) {
                maxDepth = depth;
                res = root -> val;    // 做记录
            }
            depth--;    // 到达了叶子节点了,记录完成后需要往回走,往回走需要回溯
            return;
        }
        if (root -> left) {    // 遍历当前节点的叶子
            backtracking(root -> left);
        }
        if (root -> right) {
            backtracking(root -> right);
        }
        depth--;    // 离开当前节点往回,往回走需要回溯
    }
};

这个时候我们想想,在递归的时候,什么时候想到用回溯什么时候想到定义递归函数作用然后三部曲

答:如果递归函数解决的问题能够分解为子问题,再调用递归函数本身解决子问题,就是定义递归函数作用然后上三部曲;如果没办法分解成子问题,只有老老实实从树根走到叶子,撞南墙后再反着走去寻找其他叶子,这种情况就需要涉及回溯。一种是逆向思维的过程,一种是正向思维的过程

回溯有多种写法,我们先通过做题找点儿感觉,后面总结一个模板再去解决更难的问题

112.路径总和、113.路径总和ii 

题目链接/文章讲解/视频讲解

112.路径总和

本题也需要用到回溯,因为无法分解为子问题调用递归,而且需要从根走到叶子,一路上收集路径上的节点值

一种可行的解法

/**
 * 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 sum = 0;
    bool flag = false;
    bool hasPathSum(TreeNode* root, int targetSum) {
        if (root)
            backtracking(root, targetSum);
        return flag;
    }
    void backtracking(TreeNode* root, int target) {
        sum += (root -> val);    // 到了一个节点,加上该节点的val
        if (root -> left == nullptr && root -> right == nullptr) {    // 如果到了叶子
            if (sum == target) {
                flag = true; 
                sum -= (root -> val);
                return;    // 记录、回溯并返回
            }
            else{
                sum -= (root -> val);
                return;    // 回溯并返回
            }
        }
        if (root -> left != nullptr) {    // 没到叶子,继续向下遍历
            backtracking(root -> left, target);
        }
        if (root -> right != nullptr) {
            backtracking(root -> right, target);
        }
        sum -= (root -> val); 
        return;    // 遍历完了该节点的所有所有叶子,回溯并返回
    }
};

上面的思路遍历搜索了可能的路径,如果搜索过路径总和等于目标值的,那么返回一定是true 

一种优化方式是:只要找到一条满足路径总和等于目标值的,就不用再继续搜索了 

也就是当回溯到某个节点的时候,需要看看之前是否已经搜索到了满足条件的路径,如果已经搜索到了,直接往上返回 

因此,我们的回溯函数需要返回值,用于标记“是否搜索到了满足条件的路径” 

以后记住,如果是需要遍历整棵树的,往往不需要返回值;如果只希望遍历一部分树,就可以通过回溯函数的返回值标记什么时候停止遍历 

优化代码如下:

class Solution {
public:
    int sum = 0;
    bool hasPathSum(TreeNode* root, int targetSum) {
        if (root)
            return backtracking(root, targetSum);
        return false;
    }
    bool backtracking(TreeNode* root, int target) {
        sum += (root -> val);    // 到了一个节点,加上该节点的val
        if (root -> left == nullptr && root -> right == nullptr) {    // 如果到了叶子
            if (sum == target) { 
                return true;    // 记录并返回true,告诉上一层不需要继续搜索了,因为不需要继续搜索了,因此不用回溯了
            }
            else{
                sum -= (root -> val);
                return false;    // 回溯并返回false,告诉上一层还需要继续搜索
            }
        }
        if (root -> left != nullptr) {    // 没到叶子,继续向下遍历搜索
            bool flag = backtracking(root -> left, target);
            if (flag == true)   // 如果在该节点的子节点搜索到了,直接返回true告诉其上层节点不需要继续搜索了,同理就不需要回溯了
                return true;
        }
        if (root -> right != nullptr) {
            bool flag = backtracking(root -> right, target);
            if (flag == true)   // 搜索到了,返回true告诉上层节点,无需回溯
                return true;
        }
        sum -= (root -> val); 
        return false;    // 遍历完了该节点的所有所有叶子,还没找到,回溯并返回false,告诉其上层节点还需要继续找
    }
};

113.路径总和ii  

这活生生就是一道回溯。没办法分解成子问题,只有老老实实从树根走到叶子,撞南墙后再反着走去寻找其他叶子,走的路上需要记录走过的路径,找到满足条件的路径时候需要加到结果集里

/**
 * 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:
    vector<int> path;   // 用于记录走过的路径
    vector<vector<int> > res;   // 结果集
    vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
        if (root)
            backtracking(root, targetSum, 0);
        return res;
    }
    void backtracking(TreeNode * root, int targetSum, int sum) {
        sum += root -> val; 
        path.push_back(root -> val);    // 到了一个节点,记录路径,计算路径总和
        if (root -> left == nullptr && root -> right == nullptr)    // 如果这个节点是叶子
        {
            if (sum == targetSum)
            {
                res.push_back(path);
                sum -= root -> val;
                path.pop_back();
                return; // 判断加入结果集,因为要找到所有的路径,因此还需要继续搜索别的路径,需要回溯
            } else
            {
                sum -= root -> val;
                path.pop_back();
                return; // 回溯
            }
        }
        if (root -> left != nullptr) {  // 如果该节点不是叶子,继续遍历搜索
            backtracking(root -> left, targetSum, sum);
        }
        if (root -> right!= nullptr) {
            backtracking(root -> right, targetSum, sum);
        }
        sum -= root -> val; 
        path.pop_back();
        return; // 搜索完了这个节点的所有叶子,回溯
    }
};

注意,本题回溯需要回溯当前路径节点及当前路径总和 

如果回溯绕进去了,看看Day17的那道二叉树的所有路径的示意图,其实进入回溯函数就是相当于这个人到了一个城市,回溯函数的内容就是他需要做什么事情

106.从中序与后序遍历序列构造二叉树、105.从前序与中序遍历序列构造二叉树 

106.从中序与后序遍历序列构造二叉树 

连续做了几道回溯的题目,这道题又可以回到递归三部曲了。因为这道题可以用分解问题的思想

首先明确递归函数定义:递归函数能够从一棵树的中序遍历结果与后序遍历结果构造二叉树,并返回该二叉树的头节点

确定递归函数的参数与返回值: 因为递归函数需要从两个遍历结果构造二叉树,需要接收两个数组分别表示两个遍历,返回头节点

TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder);

确定终止条件: 当某个用于表示遍历结果的数组为空了,说明为空树,直接返回NULL

if (postorder.size() == 0) return NULL;

确定单层递归逻辑:递归函数需要从中序遍历结果与后序遍历结果构造二叉树,并返回二叉树的头节点。有以下几个步骤

  1. 后序数组的最后一个元素作为根节点的值
  2. 找到后序数组最后一个元素在中序数组的位置,作为切割点
  3. 切割中序数组,切成左子树的中序遍历和右子树的中序遍历
  4. 根据左子树的节点个数与右子树的节点个数切割后序数组,切成左子树的后序遍历和右子树的后序遍历
  5. 根据左子树的中序遍历和后序遍历,递归调用得到左子树;根据右子树的中序遍历和后序遍历,递归调用得到右子树
  6. 让根节点的指针域指向左右子树
/**
 * 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:
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        if (postorder.size() == 0)
            return nullptr; // 终止条件:如果某个遍历序中没有元素了,说明是空树,返回NULL
        
        TreeNode * root = new TreeNode;
        root -> val = postorder[postorder.size() - 1];  // 根节点的值应该是后序遍历末尾元素的值

        // 我们分别需要得到左子树的中序遍历和后序遍历,以及右子树的中序遍历和后序遍历
        vector<int> left_inorder, left_postorder, right_inorder, right_postorder;
        
        // 以根节点为分割点,在中序遍历中划分出左子树的中序遍历和右子树的中序遍历
        auto iter = find(inorder.begin(), inorder.end(), root -> val);
        copy(inorder.begin(), iter, back_inserter(left_inorder));
        copy(iter + 1, inorder.end(), back_inserter(right_inorder));

        // 根据左子树的节点个数和右子树的节点个数,在后序遍历中获取左子树的后序遍历与右子树的后序遍历
        for (int i = 0; i < left_inorder.size(); ++i)
        {
            left_postorder.push_back(postorder[i]);
        }
        for (int i = left_inorder.size(); i < postorder.size() - 1; ++i) {
            right_postorder.push_back(postorder[i]);
        }

        // 递归调用
        root -> left = buildTree(left_inorder, left_postorder);
        root -> right = buildTree(right_inorder, right_postorder);
        
        return root;
    }
};

105.从前序与中序遍历序列构造二叉树  

和上一道题一模一样的思路,下面只提一下不同的地方

定义递归函数作用换成:递归函数能够从一棵树的前序遍历结果与中序遍历结果构造二叉树,并返回该二叉树的头节点

单层递归逻辑中,根节点的值要从前序数组的第一个元素取

上代码

/**
 * 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:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        if (preorder.size() == 0)
            return nullptr; // 终止条件:如果某个遍历序中没有元素了,说明是空树,返回NULL
        
        TreeNode * root = new TreeNode;
        root -> val = preorder[0];  // 根节点的值应该是前序遍历中首元素的值

        // 我们分别需要得到左子树的中序遍历和前序遍历,以及右子树的中序遍历和前序遍历
        vector<int> left_inorder, left_preorder, right_inorder, right_preorder;
        
        // 以根节点为分割点,在中序遍历中划分出左子树的中序遍历和右子树的中序遍历
        auto iter = find(inorder.begin(), inorder.end(), root -> val);
        left_inorder.assign(inorder.begin(), iter);
        right_inorder.assign(iter + 1, inorder.end());

        // 根据左子树的节点个数和右子树的节点个数,在前序遍历中获取左子树的前序遍历与右子树的前序遍历
        for (int i = 1; i <= left_inorder.size(); ++i)
        {
            left_preorder.push_back(preorder[i]);
        }
        for (int i = left_inorder.size() + 1; i < preorder.size(); ++i) {
            right_preorder.push_back(preorder[i]);
        }

        // 递归调用
        root -> left = buildTree(left_preorder, left_inorder);
        root -> right = buildTree(right_preorder, right_inorder);
        
        return root;
    }
};

回顾总结 

什么时候用递归三部曲,什么时候用回溯:一种逆向思维,另一种是正向思维 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

林沐华

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

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

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

打赏作者

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

抵扣说明:

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

余额充值