代码随想录第十八天 LeetCode513、112、113、106、105

513.找树左下角的值

本题中的左下角的值是指二叉树的 最底层 最左边 节点的值
既然是找最底层的值,很明显用层序遍历是最容易想到的,层序遍历没什么难度,下面直接给出代码:

int findBottomLeftValue(TreeNode* root) {
        queue<TreeNode*> que;
        int ans;
        que.push(root);
        while(!que.empty()){
            int size = que.size();
            ans = que.front()->val;
            while(size--){
                TreeNode* cur = que.front();
                que.pop();
                if(cur->left) que.push(cur->left);
                if(cur->right) que.push(cur->right);
            }
            
        }
        return ans;
    }

重点还是递归的解法,本题递归的解法不需要处理中结点,因为中结点的处理在题目中没有意义,只要找到符合条件的左下角结点再返回即可。如何找到最底层的结点,想到使用结点的深度,使用一个全局变量来保存现有的最大深度,在遍历的时候与当前结点的深度对比,再更新输出答案。
递归三部曲:

  1. 输入输出:输入一个是结点,一个是结点的深度,用来与最大深度比较;输出为void,不需要返回值;
  2. 终止条件:结点是否为叶子结点,如果是叶子结点再判断是否是最左侧的叶子结点;
  3. 单层递归逻辑:本题因为不需要处理中结点,所以前中后序遍历都可以
void getdep(TreeNode* cur , int depth){
        if(cur->left == NULL && cur->right == NULL){
            if(depth > maxdepth){
                ans = cur->val;
                maxdepth = depth;
            }
        }
        if(cur->left) getdep(cur->left, depth + 1);
        if(cur->right) getdep(cur->right, depth + 1);
    }

这里在处理左右的时候,也是有回溯的逻辑的:getdep(cur->left, depth + 1),通过回溯在递归返回上一层的时候得到父节点正确的深度。
下面是完整代码:

	int maxdepth = INT_MIN; //两个全局变量
    int ans;
    void getdep(TreeNode* cur , int depth){
        if(cur->left == NULL && cur->right == NULL){
            if(depth > maxdepth){
                ans = cur->val;
                maxdepth = depth;
            }
        }
        if(cur->left) getdep(cur->left, depth + 1);
        if(cur->right) getdep(cur->right, depth + 1);
    }
    int findBottomLeftValue(TreeNode* root) {
        //前序遍历
        getdep(root, 1);
        return ans;
    }

我认为本题的关键点在于想清楚递归函数的返回类型和输入参数,和之前做过的找二叉树最大深度或最小高度不同,这里虽然也是利用深度,但是返回类型为void,借助全局变量更新答案。然后确定代码中是否有回溯,有的回溯应该在哪里添加。

112. 路径总和

不知道这题为什么也是简单。思路看起来很简单,写起来挺麻烦的,至少我自己写的上来没AC出来。
这题和之前的257. 二叉树的所有路径类似,所以明显需要回溯。
本题使用递归来写,因为不需要处理中结点,所以不区分前中后序。题目的要求是如果有一条符合条件的路径,就返回true,所以当找到符合条件的路径后就及时返回,这一点在处理代码的时候很重要。
下面给出卡哥对于递归中是否需要返回值的总结:

  • 如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(这种情况就是本文下半部分介绍的113.路径总和ii)
  • 如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (这种情况我们在236. 二叉树的最近公共祖先 (opens new window)中介绍)
  • 如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(本题的情况)

所以本题是需要返回值的,返回类型为bool
递归三部曲:

  1. 输入输出:输入一个是结点不用说,另一个是统计路径和的变量,为int;输出是是否有符合条件的路径,为bool
  2. 终止条件:当遍历到叶子结点时,说明已经遍历完一条路径了,此时判断是否符合条件;
  3. 单层递归逻辑:本题需要回溯,回溯在处理左右结点时的逻辑中,并且不需要处理中结点

完整代码如下:

	bool hasPathSum(TreeNode* root, int targetSum) {
        if(root == NULL) return false;
        return travesal(root , targetSum - root->val);
    }
    bool travesal(TreeNode* cur , int sum){
        if(cur == NULL) return false;
        if(!cur->left && !cur->right && sum == 0) return true;
        if(!cur->left && !cur->right && sum != 0) return false;
        if(cur->left){
            sum -= cur->left->val;
            if(travesal(cur->left, sum + cur->left->val)) return true;
        }
        if(cur->right){
            sum -= cur->right->val;
            if(travesal(cur->right, sum + cur->right->val)) return true;
        }
        return false;

    }

这里和257. 二叉树的所有路径对比一下:

  • 当前节点的处理:本题是放在处理左右节点逻辑之前,并且求和的是cur->left->val不是cur->val;而求所有路径中,递归函数中第一行代码就是对当前节点值的输入;这和根节点的处理有关,本题在主函数的调用中travesal(root , targetSum - root->val),不是直接输入targetSum,而是先把根节点的值处理了,再去考虑他的左右子树,这也会影响处理后序节点的结果。自己调试一下就知道区别了。
  • 257需要遍历整个二叉树,并且不需要处理节点,所以返回类型为void;本题只要找到一条符合条件的路径,所以返回类型为bool

另外,本题使用的是遇到递减的方式,这样如果最后count == 0,同时到了叶子节点的话,说明找到了目标和。

113. 路径总和 II

本题是112和257的缝合怪,既需要判断是否符合条件,也要进行输出答案。实际上如果掌握了112和257,本题还是很简单的。
下面说一下需要注意的点:

  • 不需要处理节点,并且需要遍历整个二叉树,所以返回类型为void
  • 需要处理两个回溯,分别是对输出数组的回溯和路径和的回溯
  • 输入参数需要传递地址,才能在内存里真正将答案赋值

直接给出代码:

class Solution {
public:
    vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
        vector<vector<int>> ans;
        if(root == NULL) return ans;
        vector<int> path;
        path.push_back(root->val);
        travesal(root, path, ans, targetSum - root->val);
        return ans;

    }
private:
    void travesal(TreeNode* cur , vector<int>& path , vector<vector<int>>& ans , int sum){
        if(!cur->left && !cur->right && sum == 0) ans.push_back(path);
        if(cur->left){
            path.push_back(cur->left->val);
            travesal(cur->left, path, ans, sum - cur->left->val);
            path.pop_back();
        }
        if(cur->right){
            path.push_back(cur->right->val);
            travesal(cur->right, path, ans, sum - cur->right->val);
            path.pop_back();
        }
    }
};

对了,主函数中不要忘了把根节点提前放到path中:path.push_back(root->val);

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

在这里插入图片描述
题目给出对一颗二叉树的中序和后序遍历的数组,然后要求根据这两个数组构建出二叉树。
首先需要找到中序和后序遍历数组中的规律:

  • 后序数组中的最后一个节点必然是根节点(子树的根节点)
  • 中序是左中右,所以中序数组的左右半区是靠中节点分割的
  • 后序是左右中,并且左子树和右子树的节点个数和中序数组中是对应相等的

综上,本题的思路是首先找到根节点,然后利用根节点分割中序数组,再根据分割后中序数组左右半区的长度分割后序数组。
采用递归的方式,先定义根节点,再添加根节点的左右孩子。返回类型为节点,输入参数分别为中序数组和后序数组。
代码如下:

TreeNode* traversal (vector<int>& inorder, vector<int>& postorder) {
        if(postorder.size() == 0) return NULL;
        int nodeval = postorder[postorder.size() - 1];
        TreeNode* node = new TreeNode(nodeval);
        if(postorder.size() == 1) return node;
        //利用中序数组找到中结点
        int index = 0;
        for(index = 0; index < inorder.size(); index++){
            if(inorder[index] == nodeval) break;
        }
        //利用index切割中序数组
        vector<int> leftinorder(inorder.begin() , inorder.begin() + index);
        vector<int> rightinorder(inorder.begin() + index + 1, inorder.end());
        //利用切割后的中序数组切割后序数组
        postorder.resize(postorder.size() - 1);
        int leftsize = leftinorder.size();
        int rightsize = rightinorder.size();
        vector<int> leftpostorder(postorder.begin() , postorder.begin() + leftsize);
        vector<int> rightpostorder(postorder.begin() + leftsize , postorder.end());
        node->left = buildTree(leftinorder , leftpostorder);
        node->right = buildTree(rightinorder , rightpostorder);
        return node;
    }
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        if (inorder.size() == 0 || postorder.size() == 0) return NULL;
        return traversal(inorder, postorder);
    }

下面是一些细节:

  • 这里涉及到数组区间的问题,使用迭代器来构造新数组,所以一律采用左闭右开的方式;
  • 根节点的下标需要提前定义,因为后面需要用到
  • 后序数组中舍弃最后一个根节点使用postorder.resize(postorder.size() - 1);的方式,注意这里不能使用postorder.erase(postorder.end());
  • 当后序数组的size为1时,说明这个节点为叶子节点,所以直接返回即可

最后,卡哥给出的优化版本就是不改变原数组,直接在原数组上使用下标进行操作,这样可以减少内存的使用。

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

本题和上一题思路一模一样。
首先需要找到中序和前序遍历数组中的规律:

  • 前序数组中的第一个节点必然是根节点(子树的根节点)
  • 中序是左中右,所以中序数组的左右半区是靠中节点分割的
  • 前是中左右,并且左子树和右子树的节点个数和中序数组中是对应相等的

只不过是从后序中找根节点和从前序中找根节点的区别。
代码如下:

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

        int nodeval = preorder[0];
        TreeNode* node = new TreeNode(nodeval);

        if(preorder.size() == 1) return node;

        int index = 0;
        for(index = 0; index < inorder.size(); index++){
            if(inorder[index] == nodeval) break;
        }

        vector<int> leftinorder(inorder.begin() , inorder.begin() + index);
        vector<int> rightinorder(inorder.begin() + index + 1 , inorder.end());

        preorder.erase(preorder.begin());

        vector<int> leftpreorder(preorder.begin() , preorder.begin() + leftinorder.size());
        vector<int> rightpreorder(preorder.begin() + leftinorder.size() , preorder.end());

        node->left = travesal(leftpreorder , leftinorder);
        node->right = travesal(rightpreorder , rightinorder);
        return node;

    }
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        return travesal(preorder, inorder);
    }
};

这里我在每一步的逻辑下都加了空行,没有加注释了。

总结

体会最深的就是递归函数中的返回类型和输入参数,想清楚输入输出是很重要的。其次是判断是否需要回溯,目前求路径肯定是需要回溯的,利用深度的题也是需要回溯的。
如果弄不清楚递归的过程,就自己写完之后去模拟一下,也可以加上标准输出来调试,找到问题出在哪里。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值