代码随想录DAY16 - 二叉树 - 08/15

找树左下角的值

题干

题目:给定一个二叉树的 根节点 root,请找出该二叉树的 最底层 最左边 节点的值。假设二叉树中至少有一个节点。

注意:最左边的结点不一定是左叶子结点!!

链接:. - 力扣(LeetCode)

思路和代码

递归法

将问题分解为找每棵子树最底层的最左边的叶子结点,同时还需要记录这个最左下结点的深度。

题目求深度最大的左下结点,就需要在递归的过程中不断更新结果 result 和最大深度 maxDepth。而这两者不能被递归调用栈所影响,所以将结果和最大深度都定义为全局变量。

  • 递归参数和返回值:参数为传入的结点和之前的深度;没有返回值。

  • 递归结束条件:当碰到叶子结点时,说明当前路径已经走到最底层,如果这个叶子结点深度最大,则记录结果;如果深度不是最大,要返回,回溯到父节点走别的路径继续找最左下结点。

  • 递归顺序:本质是前序遍历,根据 “中左右” 的顺序,当遍历到当前结点时,深度 depth + 1;之后先递归遍历左子树找叶子结点,再递归遍历右子树,因为同一层可能有多个叶子节点,但是我们要优先找到最左边的,所以每次都先遍历左子树,这样后续右子树如果也有相同最大深度的结点,就不会覆盖掉之前最左边的结点。

class Solution {
public:
    int maxDepth = INT_MIN; // 记录最大深度,全局变量
    int result = 0; // 记录最左下的节点值,即最终结果
    
    void findLeft(TreeNode* node, int depth){
        depth++;
        // 如果当前结点是叶子结点,需要返回
        if (node->left == nullptr && node->right == nullptr){
            if (depth > maxDepth){
                // 只能是大于号,不可以是大于等于号
                // 不然相同最大深度的后续结点会把前面的覆盖掉,这样就不是最左边结点了
                maxDepth = depth; // 更新最大深度
                result = node->val;
            }
            return;
        }
        // 遍历左子树找最左下结点
        if (node->left){
            findLeft(node->left,depth); // 隐含了回溯
            // 由于 depth 没有取地址,所以函数返回到这时 depth 并没有改变
        }
        // 遍历右子树找最左下结点
        if (node->right){
            findLeft(node->right,depth);
        }
    }
    
    int findBottomLeftValue(TreeNode* root) {
        findLeft(root,0);
        return result;
    }
};
迭代法:层序遍历

用层序遍历,遍历到最后一层时,最左侧的结点就是我们要找的答案。

class Solution {
public:
    int findBottomLeftValue(TreeNode* root) {
        TreeNode* cur;
        queue<TreeNode*> layer;
        layer.push(root);
        int count = 1;
        int result = 0; // 记录结果
        while (!layer.empty()){
            result = layer.front()->val; // 不断更新每层的最左边结点,也就是每一层队列的队头
            while (count--){
                cur = layer.front();
                layer.pop();
                if (cur->left)  layer.push(cur->left);
                if (cur->right) layer.push(cur->right);
            }
            count = layer.size();
        }
        return result;
    }
};

路径总和

题干

题目:给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。

链接:. - 力扣(LeetCode)

思路和代码

递归法:前序遍历,累加路径和

我们之前做过一道题,是求二叉树的所有路径,我们只需要在这道题的基础上,统计每条路径的和即可。

采用前序遍历,当遍历到叶子结点时表示采集到一条路径,在遍历的过程中不断更新路径和,到达叶子结点时,比较路径和与目标和是否相同,如果相同可以直接返回 true,如果不相同则要回溯到父节点换另一条路径。

  • 递归参数和返回值:参数是传入的结点、目标和、从根节点到当前结点的路径和 sum;返回值为是否找到符合条件的路径(bool 型)

  • 递归的结束条件:当遍历到叶子结点,找到路径和与目标和相同的情况时,直接返回 true,结束递归。

  • 递归顺序:根据 “中左右” 的顺序,先将当前结点加入路径和中,再遍历左子树继续向下寻找路径,遍历完左子树如果没有找到符合的路径,则继续遍历右子树找另外的路径。最后如果左右子树的路径都没有满足目标和的情况,则返回 false。

class Solution {
public:
    // 前序遍历
    bool pathSum(TreeNode* node, int targetSum, int sum){ // 注意这里的路径和 sum 不需要取地址,方便回溯
        sum += node->val;
        if (node->left == nullptr && node->right == nullptr){
            if (sum == targetSum)
                return true;
            else
                return false;
        }
        // 遍历左子树
        if (node->left){
            bool leftFlag = pathSum(node->left,targetSum,sum); 
            // 隐含了回溯,因为 sum 没有取地址,所以返回之后 sum 并没有被改变!!
            if (leftFlag) return true;
        }
        // 遍历右子树
        if (node->right){
            bool rightFlag = pathSum(node->right,targetSum,sum); // 隐含了回溯
            if (rightFlag) return true;
        }
        return false;
    }
    bool hasPathSum(TreeNode* root, int targetSum) {
        if (root == nullptr) return false;
        int sum = 0; // 记录路径和
        return pathSum(root,targetSum,sum);
    }
};
递归法:前序遍历,目标和递减

这个方法的本质思路和之前的方法是一样的,唯一的区别是我们不需要累加路径和了,递归的参数变成目标和,每次让目标和递减。

  • 递归参数和返回值:参数是传入的结点、目标和;返回值为是否找到符合条件的路径(bool 型)

  • 递归的结束条件:当遍历到叶子结点,并且目标和减少到 0,说明找到了满足条件的路径,直接返回 true,结束递归。

  • 递归顺序:根据 “中左右” 的顺序,先将目标和减去当前节点值,再遍历左子树继续向下寻找路径,遍历完左子树如果没有找到符合的路径,则继续遍历右子树找另外的路径。最后如果左右子树的路径都没有满足目标和的情况,则返回 false。

class Solution {
public:
    // 这段代码的整体框架和上一个方法是相同的
    bool pathSum(TreeNode* node, int targetSum){
        targetSum -= node->val; // 让 目标和 递减
        if (node->left == nullptr && node->right == nullptr){
            if (targetSum == 0)
                return true;
            else
                return false;
        }
        // 遍历左子树
        if (node->left){
            bool leftFlag = pathSum(node->left,targetSum); // 也是隐含了目标和的回溯
            if (leftFlag) return true;
        }
        // 遍历右子树
        if (node->right){
            bool rightFlag = pathSum(node->right,targetSum);
            if (rightFlag) return true;
        }
        return false;
    }
    bool hasPathSum(TreeNode* root, int targetSum) {
        if (root == nullptr) return false;
        return pathSum(root,targetSum);
    }
};

路径总和Ⅱ

题干

题目:给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

区别:这道题和上一题不同的地方在于要把满足 路径和 = 目标和 的所有路径返回。

链接:. - 力扣(LeetCode)

思路和代码

递归法:前序遍历

我们在做上一题的时候,只要找到了一条满足条件的路径就可返回 true,但是这道题必须要遍历完所有路径,所以这道题的递归方法并不需要返回值。

  • 递归参数和返回值:参数是传入的结点,一条记录结点值的路径,满足条件的路径结果集,目标和;没有返回值。

  • 递归的结束条件:当遇到叶子结点时返回,需要回溯到上一个结点,继续递归另外的路径;遍历完所有路径才完全终止。

  • 递归的顺序:根据 “中左右” 的顺序,先将目标和减去当前节点值,再先后遍历左、右子树向下寻找路径。在层层递归过程中,结果集参数由于取地址,所以会不断更新改变;而记录根节点到当前结点的路径 path,不需要取地址,方便后续的回溯。

class Solution {
public:
    void findPath(TreeNode* node, vector<int> path, vector<vector<int>> &result, int targetSum){
        targetSum -= node->val;
        path.push_back(node->val); 
        // path 在这里添加了当前结点,但由于没有取地址,所以返回到上一层时path并没有改变
        if (node->left == nullptr && node->right == nullptr){
            if (targetSum == 0){
                result.push_back(path);
            }
            return;
        }
        // 遍历左子树
        if (node->left){
            findPath(node->left,path,result,targetSum); 
            // 隐含了回溯,返回之后的 path 并没有被改变,相当于回溯了 
        }
        // 遍历右子树
        if (node->right){
            findPath(node->right,path,result,targetSum); // 隐含了回溯
        }
    }
    vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
        vector<vector<int>> result;
        if (root == nullptr) return result;
        vector<int> path; // 记录结点路径
        findPath(root,path,result,targetSum);
        return result;
    }
};

从中序和后序遍历构造二叉树

题干

题目:给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。

注意:你可以假设树中没有重复的元素。

链接:. - 力扣(LeetCode)

思路和代码

后序遍历是 “ 左右中 ”,因此后序遍历最靠后的元素,一定是二叉树的中间结点。以该中间结点作为分界,将中序序列分割为左子树的中序序列和右子树的中序序列。这样我们又可以知道左、右子树的结点个数,根据结点个数我们就知道后序遍历 “左右中” 里,前多少个结点属于左子树,后多少个结点属于右子树。(因为每个子树的结点个数肯定都是相同的!)到此为止,我们便知道了左子树、右子树的中序序列和后序序列,以此递归建立子树,最后返回子树的根节点。

递归法

将问题拆解为,先从中序和后序遍历中组建左右子树,最后由左右子树和根节点再拼成大树。

  • 递归参数和返回值:参数是中序序列和后序序列,返回值是树的根节点。

  • 递归的结束条件:当传入的数组为空,说明已经建完树,要返回空指针。

  • 递归顺序:根据后序遍历 “左右中” 的顺序,先以后序遍历的最后一个元素构建根节点;之后根据中序遍历 “左中右” 的顺序,以根节点为分界划分左右子树,分别获取左右子树的中序序列、后序序列,先后对左子树序列和右子树序列递归建树。

class Solution {
public:
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        int size = postorder.size(); // 结点数量
        if (size == 0) return nullptr; // 数组为空,返回空指针
        
        // 先找根结点,即后序序列的最后一个元素
        TreeNode* root = new TreeNode(postorder[size-1]);
        postorder.pop_back(); // 弹出最后一个结点
        
        // ...... 获取左子树、右子树的中序序列
        vector<int> leftInorder;// 左子树的中序序列
        vector<int> rightInorder;// 右子树的中序序列
        for (int i = 0; i < size; ++i) {
            if (inorder[i] == root->val){
                leftInorder.assign(inorder.begin(),inorder.begin()+i);
                rightInorder.assign(inorder.begin()+i+1,inorder.end());
                break;
            }
        }
        // ...... 获取左子树、右子树的后序序列
        int leftCount = leftInorder.size(); // 左子树的结点数量
        int rightCount = rightInorder.size(); // 右子树的结点数量
        vector<int> leftPostorder; // 左子树的后序序列
        leftPostorder.assign(postorder.begin(),postorder.begin()+leftCount);
        vector<int> rightPostorder; // 右子树的后序序列
        rightPostorder.assign(postorder.begin()+leftCount,postorder.end());
        
        // .... 获取完左右子树的中序、后序序列后,递归建立左子树、右子树,返回左右子树的根节点
        TreeNode* leftTree = buildTree(leftInorder,leftPostorder); // 左子树的根结点
        TreeNode* rightTree = buildTree(rightInorder,rightPostorder); // 右子树的根结点
        root->left = leftTree;
        root->right = rightTree;
        return root;
    }
};
递归法:优化

在上面的方法中,每次递归都要新建 vector,会浪费时间和空间,因此我们可以做些调整。我们新建vector是为了存储左、右子树的中序序列、后序序列,但其实只需要知道子树的中序序列、后序序列在原始 vector 中的起始下标、终止下标即可。(原始 vector 指最最开始的中序序列和后序序列)。至于递归的思路、整体框架和之前的方法是差不多的,主要修改的是递归参数。

  • 递归参数:原始中序序列、当前子树的中序序列起始下标 inorderStart 和 中序序列终止下标 inorderEnd; 原始后序序列、当前子树的后序序列起始下标 postorderStart 和后序序列终止下标 postorderEnd。

    • 左闭右开区间(设分割点的下标为 split):

      • 根结点 postorder [ postorderEnd-1 ]

      • 左子树中序序列 inorder [ inorderStart ~ split ]

      • 右子树中序序列 inorder [ split+1 ~ inorderEnd ]

    • 左闭右闭区间(设分割点的下标为 split):

      • 根结点 postorder [ postorderEnd ]

      • 左子树中序序列 inorder [ inorderStart ~ split-1 ]

      • 右子树中序序列 inorder [ split+1 ~ inorderEnd ]

  • 递归的结束条件和递归顺序和上题相同,不再赘述。

注意:题解中说到用索引下标的方法会优化性能,而在力扣运行中,区间选择左闭右开确实会快很多。但是如果区间是左闭右闭,则速度提升不大,暂时不知道为什么,但力扣本身的运行时间测试可能也不准确。这里给出的代码是左闭右闭区间。

以下为 2024/08/17 修正补充!!!

为什么之前说速度没有提升,是因为我在写代码的时候,传参时忘记给中序序列数组和后序序列数组取地址!!如果取地址,传进来的就是指针型变量,不需要新建数组;但如果不取地址,传参时会在栈空间中新建vector数组赋值,会很耗时!!

class Solution {
public:
    // !!!
    // 注意这里中序序列数组 inorder 和后序序列数组 postorder 一定要取地址,不然每次传参时都要新建数组赋值,会很耗时!
    TreeNode* build(vector<int> &inorder, int inorderStart, int inorderEnd,
                    vector<int> &postorder, int postorderStart, int postorderEnd)
    {
        int size = inorderEnd - inorderStart+1; // 结点数量
        if (size == 0) return nullptr;
        // 先找根结点
        TreeNode* root = new TreeNode(postorder[postorderEnd]);
        postorderEnd--; // 去掉根结点
        int i = 0; // 记录分割点的位置
        for (i = inorderStart; i <= inorderEnd; ++i) {
            if (inorder[i] == root->val){
                break;
            }
        }
        // 左、右子树的中序区间
        int leftInStart = inorderStart;
        int leftInEnd = i-1;
        int rightInStart = i+1;
        int rightInEnd = inorderEnd;

        int leftCount = i - inorderStart; // 左子树的结点数量
        // 左、右子树的后序区间
        int leftPostStart = postorderStart;
        int leftPostEnd = postorderStart+leftCount-1;
        int rightPostStart = postorderStart+leftCount;
        int rightPostEnd = postorderEnd;
		
        // 左子树的根结点
        TreeNode* leftTree = build(inorder,leftInStart,leftInEnd,postorder,leftPostStart,leftPostEnd); 			
        // 右子树的根结点
        TreeNode* rightTree = build(inorder,rightInStart,rightInEnd,postorder,rightPostStart,rightPostEnd); 
        // 插入左、右子树
        root->left = leftTree;
        root->right = rightTree;
        return root;
    }
    
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        if (inorder.size() == 0 || postorder.size() == 0) return nullptr;
        TreeNode* root = build(inorder,0,inorder.size()-1,postorder,0,postorder.size()-1);
        return root;
    }
};
取地址和不取地址的运行时间对比

从前序和中序遍历构造二叉树

题干

题目:给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

链接:. - 力扣(LeetCode)

思路和代码

递归法

这道题和上道题的思路本质上一样的。前序序列是 “中左右”,因此每次前序序列的第一个元素肯定就是根节点,找到根结点后再分割中序序列。不同的地方就是找根节点的位置不同罢了。

class Solution {
public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int size = inorder.size();
        if (size == 0) return nullptr;

        TreeNode* root = new TreeNode(preorder[0]);
        int i; // 寻找中序序列的切割点
        for (i = 0; i < size; ++i) {
            if (inorder[i] == root->val){
                break;
            }
        }
        // 左、右子树的中序序列
        vector<int> leftInorder(inorder.begin(),inorder.begin()+i);
        vector<int> rightInorder(inorder.begin()+i+1,inorder.end());
        // 左、右子树的前序序列
        vector<int> leftPreorder(preorder.begin()+1,preorder.begin()+1+leftInorder.size()); 
        vector<int> rightPreorder(preorder.begin()+1+leftInorder.size(),preorder.end());
        
        TreeNode* leftTree = buildTree(leftPreorder,leftInorder);
        TreeNode* rightTree = buildTree(rightPreorder,rightInorder);
        root->left = leftTree;
        root->right = rightTree;
        return root;
    }
};

小结

递归法什么时候需要返回值

递归函数什么时候需要返回值?什么时候不需要返回值?这里总结如下三点:

  • 如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(见上文的 “ 路径总和Ⅱ ” 那道题)

  • 如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (在后续236. 二叉树的最近公共祖先中介绍)

  • 如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(见上文的 “ 路径总和 ” 那道题)

给出哪两种序列可以确定一颗二叉树

前序 + 中序,后序 + 中序都可以确定一颗二叉树;但是前序 + 后序无法确定二叉树,因为如果缺少中序序列,则无法确定左右。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值