代码随想录算法训练营DAY18|C++二叉树Part.5|513.找树左下角的值、112. 路径总和、113.路径总和II、106\105.从中(前)序与后(中)序遍历序列构造二叉树

513.找树左下角的值

力扣题目链接

文章讲解:513.找树左下角的值

视频讲解 :怎么找二叉树的左下角? 递归中又带回溯了,怎么办?| LeetCode:513.找二叉树左下角的值

状态:最直观的一个方法,用层序遍历(迭代法)找出最底层左边的值,到最后一层第一个结点的值就是我们的结果。
第二种办法就是使用递归了,这里的递归如何保证优先左侧遍历的呢?我们对左子树的递归调用优先于右子树。这就意味着,对于每个深度层级,最左侧的节点总是最先被访问和处理的。并且我们保证同一深度我们只做一次值的存储。

层序-迭代遍历

class Solution {
public:
    int findBottomLeftValue(TreeNode* root) {
        queue<TreeNode*> que;
        if (root != NULL) que.push(root);
        int result = 0;
        while (!que.empty()) {
            int size = que.size();
            for (int i = 0; i < size; i++) {
                TreeNode* node = que.front();
                que.pop();
                if (i == 0) result = node->val; // 记录最后一行第一个元素
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
        }
        return result;
    }
};

前中后序-递归遍历

那么用递归的方式如何求左下角的值呢?

经典错误:

这里我们要防止一个误区,如果我们一直向左遍历,然后最左边的那个值就是左下角的值吗?显然是错误的。

题目中要求的是树的最后一行找到最左边的值。所以左下角的值完全可能在右子树。

思路

还记得我们关于二叉树深度的定义吗?

本题中深度最大的肯定就在最后一行,所以我们先找到深度最大的那一行;然后在这一行找到最靠左的值

如果能够实现以上两个目的,题目就解出来了。

首先确定遍历顺序,其实中左右都可以,因为我们没有对中结点的处理逻辑,只要先遍历左就可以。即然前中后序都是先遍历左边,所以只要到深度最大一行,我们找到的那个结点肯定就是最靠左的结点。

伪代码

先定义一个全局变量maxDepth,用来记录二叉树中的最大深度。我们单反递归到一个深度,就要记录该值。

int maxDepth = INT_MIN; //INT里的最小值,方便比较
int result; //每次更新深度,我们都要跟新该深度最左侧的值
  • 递归三部曲第一步:确定返回值和参数。depth来记录我们当前遍历的深度,由于已经定义了全局变量,我们的返回值void
void traversal(root, depth);
  • 递归三部曲第二部:确定终止条件。遍历到叶子结点我们就记录一下深度即可,到时候还要比较这是不是最大深度
if (root->left == NULL && root->right == NULL){
  if (depth > maxDepth){	//depth是当前深度,MaxDdepth是最大深度
    maxDepth = depth;		
    result = root->val;
  }
}
  • 单层递归逻辑:无论是前中后序都是先遍历左,再遍历右。再结合第二部,我没没有判断root是否为NULL,所以在我们不能让空指针入递归。
    • 关于回溯:这是由我们的代码目的决定的,因为左结点可能不是最大深度,我们还要回退去找右边的深度能不能到最下一层
if(root->left){
  depth++;
  traversal(root->left, depth);
  depth--; //回溯
}
if (root->right){
  depth++;
  traversal(root->right, depth);
  depth--;
}
return;

回溯代码简化:我们传参depth+1就是对depth++,同时该函数体内我们并没有做depth++traversal(root->left, depth+1)代码段的后面相当于又一次depth--

if (root->left) {
    traversal(root->left, depth + 1); // 隐藏着回溯
}
if (root->right) {
    traversal(root->right, depth + 1); // 隐藏着回溯
}
return;

CPP代码

class Solution {
public:
    int maxDepth = INT_MIN;
    int result;
    void traversal(TreeNode* root, int depth) {
        if (root->left == NULL && root->right == NULL) {
            if (depth > maxDepth) {
                maxDepth = depth;
                result = root->val;
            }
            return;
        }
        if (root->left) {
            traversal(root->left, depth + 1); // 隐藏着回溯
        }
        if (root->right) {
            traversal(root->right, depth + 1); // 隐藏着回溯
        }
        return;
    }
    int findBottomLeftValue(TreeNode* root) {
        traversal(root, 0);
        return result;
    }
};

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

力扣题目链接

文章链接:112. 路径总和、113.路径总和II

视频链接:拿不准的遍历顺序,搞不清的回溯过程,我太难了! | LeetCode:112. 路径总和

状态:觉得好难!主要是没想到回溯回溯还是回溯!

112.路径总和

思路

首先确定遍历方式,这里我们是不需要处理中结点的,所以前中后序都可以。这里明显要采用回溯算法,如下图所示:

2021020316051216

也就是说,如果当前路径不符合,我们就从倒数第二天路还是回溯,还是不行就回溯倒数第三、倒数第四条路,总有一条能符合规矩!

伪代码实现

  • 递归三部曲第一部:确定参数和返回值

    • 参数:需要二叉树的根节点,还需要一个计数器,这个计数器用来计算二叉树的一条边之和是否正好是目标和,计数器为int型。
    • 返回值:我们应该是只要找一条符合条件的路径,递归函数就需要及时返回,那么返回类型是bool
    bool traversal(TreeNode* cur, int count){
      
    }
    
  • 确定终止条件:

    • 首先计数器如何统计这一条路径的和呢?

      不要去累加然后判断是否等于目标和,那么代码比较麻烦,可以用递减,让计数器count初始为目标和,然后每次减去遍历路径节点上的数值。

    if (!cur->left && !cur->right && count == 0) return true; // 遇到叶子节点,并且计数为0
    if (!cur->left && !cur->right) return false; // 遇到叶子节点而没有找到合适的边,直接返回
    
  • 确定单层递归的逻辑

    • 终止条件是判断叶子节点,所以递归的过程中就不要让空节点进入递归
    • 递归函数是有返回值的,如果递归函数返回true,说明找到了合适的路径,应该立刻返回
    if (cur->left) { // 左
        count -= cur->left->val; // 递归,处理节点;
        if (traversal(cur->left, count)) return true;
        count += cur->left->val; // 回溯,撤销处理结果
    }
    if (cur->right) { // 右
        count -= cur->right->val;
        if (traversal(cur->right, count)) return true;
        count += cur->right->val;
    }
    return false;
    

    精简回溯的代码:

    if (cur->left) { // 左 (空节点不遍历)
        // 遇到叶子节点返回true,则直接返回true
        if (traversal(cur->left, count - cur->left->val)) return true; // 注意这里有回溯的逻辑
    }
    if (cur->right) { // 右 (空节点不遍历)
        // 遇到叶子节点返回true,则直接返回true
        if (traversal(cur->right, count - cur->right->val)) return true; // 注意这里有回溯的逻辑
    }
    return false;
    

CPP代码

class Solution {
private:
    bool traversal(TreeNode* cur, int count) {
        if (!cur->left && !cur->right && count == 0) return true; // 遇到叶子节点,并且计数为0
        if (!cur->left && !cur->right) return false; // 遇到叶子节点直接返回

        if (cur->left) { // 左
            count -= cur->left->val; // 递归,处理节点;
            if (traversal(cur->left, count)) return true;
            count += cur->left->val; // 回溯,撤销处理结果
        }
        if (cur->right) { // 右
            count -= cur->right->val; // 递归,处理节点;
            if (traversal(cur->right, count)) return true;
            count += cur->right->val; // 回溯,撤销处理结果
        }
        return false;
    }

public:
    bool hasPathSum(TreeNode* root, int sum) {
        if (root == NULL) return false;
        return traversal(root, sum - root->val);
    }
};

递归法

递归法在逻辑上面还是显得非常简单。

我们只要每次存入节点的时候,把它跟之前节点的和求一下就可以了,这个时候 pair<TreeNode*, int>就能派上用场。

定义为: pair<TreeNode*, int> pair<节点指针,路径数值数值和>


113.路径总和II

力扣题目链接

思路

与112.路径总和不同,113.路径总和II要遍历整个树,找到所有路径,所以递归函数不要返回值

伪代码实现

本题要求我们遍历整棵树,那么我们肯定是要一个数组记录路径,另一个数组记录所有满足条件的路径,在这里我们定义全局变量

vector<vector<int>> result;
vector<int> path;
  • 递归三部曲之第一部:确定参数和返回值

    • 参数跟之前一样,还是当前遍历的结点和我们的count
    • 返回值为void,因为我们要遍历整个树,然后记录路径,所以递归函数要不返回值
    void traversal(TreeNode* node, int count){
    }
    
  • 递归三部曲之第二部:确定终止条件,直接跟之前一样即可

if (!cur->left && !cur->right && count == 0) return;	//遍历到叶子结点并且count=0
if (!cur->left && !cur->right) return;  遇到叶子节点而没有找到合适的边,直接返回
  • 递归三部曲之第三部:确定单层递归逻辑
if (cur->left) { // 左 (空节点不遍历)
    path.push_back(cur->left->val);
    count -= cur->left->val;
    traversal(cur->left, count);    // 递归
    count += cur->left->val;        // 回溯
    path.pop_back();                // 回溯
}
if (cur->right) { // 右 (空节点不遍历)
    path.push_back(cur->right->val);
    count -= cur->right->val;
    traversal(cur->right, count);   // 递归
    count += cur->right->val;       // 回溯
    path.pop_back();                // 回溯
}
return ;

CPP代码实现

class solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    // 递归函数不需要返回值,因为我们要遍历整个树
    void traversal(TreeNode* cur, int count) {
        if (!cur->left && !cur->right && count == 0) { // 遇到了叶子节点且找到了和为sum的路径
            result.push_back(path);
            return;
        }

        if (!cur->left && !cur->right) return ; // 遇到叶子节点而没有找到合适的边,直接返回

        if (cur->left) { // 左 (空节点不遍历)
            path.push_back(cur->left->val);
            count -= cur->left->val;
            traversal(cur->left, count);    // 递归
            count += cur->left->val;        // 回溯
            path.pop_back();                // 回溯
        }
        if (cur->right) { // 右 (空节点不遍历)
            path.push_back(cur->right->val);
            count -= cur->right->val;
            traversal(cur->right, count);   // 递归
            count += cur->right->val;       // 回溯
            path.pop_back();                // 回溯
        }
        return ;
    }

public:
    vector<vector<int>> pathSum(TreeNode* root, int sum) {
        result.clear();
        path.clear();
        if (root == NULL) return result;
        path.push_back(root->val); // 把根节点放进路径
        traversal(root, sum - root->val);
        return result;
    }
};

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

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

力扣题目链接

文章讲解:106.从中序与后序遍历序列构造二叉树

视频讲解:坑很多!来看看你掉过几次坑 | LeetCode:106.从中序与后序遍历序列构造二叉树

状态:其实切割思路还是比较简单的,难就难在我们应该以怎样的流程完成节点的构造呢?其实很简单,我们通过后序遍历的数组很容易得确定中间节点是哪个,并且我们也是通过中间节点来切割中序遍历数组,再反过来切割后序遍历,来确定新一轮的中间节点。那么思路就清晰了,我们的整个递归顺序是中、左、右!

整体思路

后序数组最后一个元素切割点先切中序数组,根据中序数组,反过来再切后序数组。一层一层切下去,每次后序数组最后一个元素就是节点元素。(因为后序是左右中)

20210203154249860

把步骤量化出来:

  • (第一步:如果数组大小为零的话,说明是空节点了。)
  • 第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
  • 第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
  • 第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组
  • 第五步:切割后序数组,切成后序左数组和后序右数组
  • 第六步:递归处理左区间和右区间

伪代码

搭建整体代码框架:

TreeNode* traversal (vector<int>& inorder, vector<int>& postorder) {

    // 第一步
    if (postorder.size() == 0) return NULL;

    // 第二步:后序遍历数组最后一个元素,就是当前的中间节点
    int rootValue = postorder.back();
    TreeNode* root = new TreeNode(rootValue);

    // 如果当前递归条件下后序数组只有一个结点,那当前处理的根结点就是叶子节点
    if (postorder.size() == 1) return root;

    // 第三步:找切割点
    auto it = std::find(inorder.cbegin(), inorder.cend(), rootValue);
    int delimiterIndex = std::distance(inorder.cbegin(), it);

    // 第四步:切割中序数组,得到 中序左数组和中序右数组
    // 第五步:切割后序数组,得到 后序左数组和后序右数组
    //		  为了后序数组的切割方便,可以先把后序数组被处理过的元素拿掉,直接resize即可

    // 第六步
    root->left = traversal(中序左数组, 后序左数组);
    root->right = traversal(中序右数组, 后序右数组);

    return root;
}

现在马上进入,切割环节,关于切割一定要坚守循环不变量。我们这里一律左闭右开

先切中序


// 找到中序遍历的切割点
auto it = std::find(inorder.cbegin(), inorder.cend(), rootValue);
int delimiterIndex = std::distance(inorder.cbegin(), it);

// 左闭右开区间:[0, delimiterIndex)
vector<int> leftInorder(inorder.begin(), inorder.begin() + delimiterIndex);
// [delimiterIndex + 1, end)
vector<int> rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end() );

再切后序


此时有一个很重的点,就是中序数组大小一定是和后序数组的大小相同的(这是必然)。

中序数组我们都切成了左中序数组和右中序数组了,那么后序数组就可以按照左中序数组的大小来切割,切成左后序数组和右后序数组。

// postorder 舍弃末尾元素,因为这个元素就是中间节点,已经用过了
postorder.resize(postorder.size() - 1);

// 左闭右开,注意这里使用了左中序数组大小作为切割点:[0, leftInorder.size)
vector<int> leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size());
// [leftInorder.size(), end)
vector<int> rightPostorder(postorder.begin() + leftInorder.size(), postorder.end());

开始递归!

root->left = traversal(leftInorder, leftPostorder);
root->right = traversal(rightInorder, rightPostorder);

CPP实现

class Solution {
private:
    TreeNode* traversal (vector<int>& inorder, vector<int>& postorder) {
        if (postorder.size() == 0) return NULL;

        // 后序遍历数组最后一个元素,就是当前的中间节点
        int rootValue = postorder[postorder.size() - 1];
        TreeNode* root = new TreeNode(rootValue);

        // 叶子节点
        if (postorder.size() == 1) return root;

        // 找到中序遍历的切割点
		auto it = std::find(inorder.cbegin(), inorder.cend(), rootValue);
		int delimiterIndex = std::distance(inorder.cbegin(), it);

        // 切割中序数组
        // 左闭右开区间:[0, delimiterIndex)
        vector<int> leftInorder(inorder.begin(), inorder.begin() + delimiterIndex);
        // [delimiterIndex + 1, end)
        vector<int> rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end() );

        // postorder 舍弃末尾元素
        postorder.resize(postorder.size() - 1);

        // 切割后序数组
        // 依然左闭右开,注意这里使用了左中序数组大小作为切割点
        // [0, leftInorder.size)
        vector<int> leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size());
        // [leftInorder.size(), end)
        vector<int> rightPostorder(postorder.begin() + leftInorder.size(), postorder.end());

        root->left = traversal(leftInorder, leftPostorder);
        root->right = traversal(rightInorder, rightPostorder);

        return root;
    }
public:
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        if (inorder.size() == 0 || postorder.size() == 0) return NULL;
        return traversal(inorder, postorder);
    }
};

值得我们注意的是,每一次递归我们都在定义新的数组,代码性能并不好,所以一个非常直观的优化方法就是:使用索引。

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

力扣题目链接

思路

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

CPP实现

日志版本请移步代码随想录18. 106.从中序与后序遍历序列构造二叉树----相关题目推荐

首先我先写一下最基础的版本,随后是使用索引的版本:

//基础版本
class Solution {
public:
    TreeNode* traversal(vector<int>& preorder, vector<int>& inorder) {
        if (preorder.size() == 0) return nullptr;

        int rootValue = preorder.front();
        TreeNode* root = new TreeNode(rootValue);
        if (preorder.size() == 1) return root;

        // 开始切割
        auto it = std::find(inorder.cbegin(), inorder.cend(), rootValue);
        int delimiter = std::distance(inorder.cbegin(), it);

        vector<int> leftInorder(inorder.cbegin(), inorder.cbegin() + delimiter);
        vector<int> rightInorder(inorder.cbegin() + delimiter + 1, inorder.cend());

        vector<int> leftPreorder(preorder.cbegin() + 1, preorder.cbegin() + 1 + leftInorder.size());
        vector<int> rightPreorder(preorder.cbegin() + 1 + leftInorder.size(), preorder.cend());
        
        // 开始递归构造
        root->left = traversal(leftPreorder, leftInorder);
        root->right = traversal(rightPreorder, rightInorder);

        return root;
    }

    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        if (preorder.size() == 0 || inorder.size() == 0) return nullptr;
        return traversal(preorder, inorder);
    }
};
class Solution {
private:
        TreeNode* traversal (vector<int>& inorder, int inorderBegin, int inorderEnd, vector<int>& preorder, int preorderBegin, int preorderEnd) {
        if (preorderBegin == preorderEnd) return NULL;

        int rootValue = preorder[preorderBegin]; // 注意用preorderBegin 不要用0
        TreeNode* root = new TreeNode(rootValue);

        if (preorderEnd - preorderBegin == 1) return root;

        int delimiterIndex;
        for (delimiterIndex = inorderBegin; delimiterIndex < inorderEnd; delimiterIndex++) {
            if (inorder[delimiterIndex] == rootValue) break;
        }
        // 切割中序数组
        // 中序左区间,左闭右开[leftInorderBegin, leftInorderEnd)
        int leftInorderBegin = inorderBegin;
        int leftInorderEnd = delimiterIndex;
        // 中序右区间,左闭右开[rightInorderBegin, rightInorderEnd)
        int rightInorderBegin = delimiterIndex + 1;
        int rightInorderEnd = inorderEnd;

        // 切割前序数组
        // 前序左区间,左闭右开[leftPreorderBegin, leftPreorderEnd)
        int leftPreorderBegin =  preorderBegin + 1;
        int leftPreorderEnd = preorderBegin + 1 + delimiterIndex - inorderBegin; // 终止位置是起始位置加上中序左区间的大小size
        // 前序右区间, 左闭右开[rightPreorderBegin, rightPreorderEnd)
        int rightPreorderBegin = preorderBegin + 1 + (delimiterIndex - inorderBegin);
        int rightPreorderEnd = preorderEnd;

        root->left = traversal(inorder, leftInorderBegin, leftInorderEnd,  preorder, leftPreorderBegin, leftPreorderEnd);
        root->right = traversal(inorder, rightInorderBegin, rightInorderEnd, preorder, rightPreorderBegin, rightPreorderEnd);

        return root;
    }

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

        // 参数坚持左闭右开的原则
        return traversal(inorder, 0, inorder.size(), preorder, 0, preorder.size());
    }
};
  • 25
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值