大家好,今天还是我们的二叉树的章节,到这里或许一些朋友已经开始有松懈了,我可以告诉大家一句话:”如果你坚持不下去了,就想想自己为什么开始“,还是鼓励大家继续坚持下去,我也知道算法很难,但是我们还是要迎难而上,好了我们继续我们今天的题目。
这道题是什么意思呢?大家先搞清楚题目让我们求什么?
大家看我们需要求的不是树最左边的节点值,而是注意是最底层最左边的节点值,这个务必要注意,一定是最深的左边,它未必是二叉树的最左边,我们如何考虑呢?首先大家思考我们是否应该使用层序遍历,其实递归也可以但是我感觉层序遍历的迭代法可能更好理解,这样我把两种方法都给大家解释一遍。
首先我们来看第一种解法,迭代解法,我们应该是考虑拿到最后一层的第一个节点这其实就是题目要求我们去找的节点,大家是否还记得我们原来写过的一个模板,其实改一改就可以解决这道题目,我们其实就只需要记录最后一层地第一个元素,代码如下:
class Solution {
public:
int findBottomLeftValue(TreeNode* root) {
queue que;
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;
}
};
如果大家忘记了可以去看我前几天的博客或者是去看代码随想录网站上都会有解释,大家注意我这个其实是每一层更新一次result不是一次性直接就可以获得最后一层的第一个值,大家注意。
接下来是第二种方法递归法,我感觉递归其实比迭代要难理解,但我们还是要去试试如何写,其实大家还要知道其实这里还是会有回溯的逻辑,为什么呢?大家看其实我的结果并不见得一定是一直往左递归才能找到,换句话说不一定是某个节点的左孩子也有可能是右孩子,大家一定要区分好题目的定义,所以我们定义一个深度,我们给它初始化为int类型的最小值,这样的话我以后更新层数方便,我们的代码可以这样写:
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) {
depth++;
traversal(root->left, depth);
depth--; // 回溯
}
if (root->right) {
depth++;
traversal(root->right, depth);
depth--; // 回溯
}
return;
}
int findBottomLeftValue(TreeNode* root) {
traversal(root, 0);
return result;
}
};
这里一定注意递归隐藏在了depth--里面,这样我们才能回去,我们的递归参数与以往不同,我们需要传递节点还需要传递深度,当我遍历到叶子节点的时候我们如果此时的深度大于我初始化的深度我们就可以更新深度与当前的值,我们注意务必要先遍历左子节点再去遍历右子节点,因为其实题目有点不同以往,我如果最深层找不到左孩子我们还应该去考虑右孩子,最后我们返回结果就可以,当然千万不要忘记递归,还要注意一定是先左后右,其实题目前中后三种遍历方式都可以,因为没有对根节点的遍历逻辑,这道题目就讲解到这里,我们继续下一道题目。
这道题目拿到后我有点后背发凉,我天求路径总和,但其实看了一下题目发现不是我想的那样,它是给你一个数字让你判断有没有一条从根节点到叶子节点的路径的所有的节点的和等于题目给出的数字,这样比我起初想的似乎要明确一些,我们来看一下题目:
这道题目主要难在我不知道这条符合题目要求的路径究竟存在于哪里,可能一会左孩子一会右孩子,还是一直左孩子或者是一直右孩子,说不准,而且遍历顺序也不太容易确定,其实前中后都可以,其实我想先告诉大家我们其实未必需要遍历完整棵树的,就比如说我们题目中的示例一,我们会优先遍历5->4->11->7这条路线,我们会发现不是我们想要的,我们回溯结果就发现5->4->11->2就是符合题目要求的路线了,这样我就可以直接返回true结束了,这里我直接告诉大家我们设置一个计数器,我们把计数器的值设定为我们的目标值,我们一直减看看到最后能否成为0,如果可以变成0(同时这时候我访问到了叶子节点)就可以判断存在这样的一条路径,在这里我尽量把回溯的过程展示给大家看,因为我的算法功底还是太弱,所以目前就一步步理解,那代码可以如何写呢?
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);
}
};
大家不要嫌弃代码写的太冗余,其实是为了帮助大家理解,如果我将回溯的过程隐藏起来我自己或许都看不懂了,大家也可能看不懂了,大家看上面给出的代码,起初是递归的终止条件如果我到了叶子节点并且count也是零了我们就可以返回true了,如果遍历到了叶子节点但是count没有变成0就返回false就可以了,接下来是左孩子注意我们执行减去左孩子的值最后如果一番递归之后我的count可以变为0那我就返回true,否则返回false,但注意回溯的过程就体现在这里,我再加回去,同样我的右孩子也是一样的思路,还是要回溯,最后如果没有左右都没有返回true就返回false这样表示没有这样的路径,最后我调用但注意因为起初我的其实节点就是根节点所以我的计数器初始化为sum-root->val才对。这道题目我就尽我所能给大家解释了一番,希望大家可以理解。
今天的最后一题,其实这道题目很难,我估计可以算得上我们二叉树章节里面相当有难度的题目了,我会尽我所能给大家解释的清楚一些,首先我们还是来看一下题目:
给出我们中序遍历和后续遍历让我们返回这棵二叉树这个的确难,但我们也要来分析一下题目要求,可以先去看讲解视频再来自己想,好看完视频我其实大致明白了因为过去我也有一些基础,首先我们大致要了解这么几步,首先第一步如果后序遍历的数组长度为0的话这不就说明是一棵空树,直接return null就可以了,接下来我还是先从后序遍历下手,因为后续遍历的顺序是左右根,因此最后一个元素就是根节点,这样找到了根节点我就可以对中序遍历下手了,因为中序遍历是左根右,这样其实我就分开了左右子树,切割中序数组,切成中序左数组和中序右数组 ,切割中序数组,切成中序左数组和中序右数组 ,这样是可以确定一棵唯一的二叉树的,然后代码随想录还解释了一个问题我也一并告诉大家,就是给我前序遍历与后序遍历我可以确定有一棵唯一的二叉树吗?其实不可以的,因为前序遍历的顺序是根左右,那么我其实我的前后遍历的作用都是确定了根节点那我根本无法分割出左右子树啊,所以这根本无法构造出一棵唯一的二叉树,这个切割点是相当重要的,大家务必要想清楚,接下来我们就尝试写一下代码:
class Solution {
public:
TreeNode* traversal(vector& inorder, vector& postorder) {
if (postorder.size() == 0) return NULL;
//确定根节点的值
int rootValue = postorder[postorder.size() - 1];
//构造根节点
TreeNode* root = new TreeNode(rootValue);
//考虑根节点就是叶子结点的特殊情况
if (postorder.size() == 1) return root;
//找到中序遍历的分割点
int delimiterIndex;
for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) {
if (inorder[delimiterIndex] == rootValue) break;
}
//开始切割中序数组但一定要注意区间开闭情况
//我们统一左闭右开因为数组都是这样的
vector leftInorder(inorder.begin(), inorder.begin() + delimiterIndex);
vector rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end());
// postorder 舍弃末尾元素因为最后一个节点已经是根节点已经安家了
postorder.resize(postorder.size() - 1);
//切割后序数组
vector leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size());
vector rightPostorder(postorder.begin() + leftInorder.size(), postorder.end());
//递归构造左右子树
root->left = traversal(leftInorder, leftPostorder);
root->right = traversal(rightInorder, rightPostorder);
return root;
}
TreeNode* buildTree(vector& inorder, vector& postorder) {
if (inorder.size() == 0 || postorder.size() == 0) return NULL;
return traversal(inorder, postorder);
}
};
这道题代码很长,而且思维量说实话也一点不低,我其实在上面写了注释,我这里给大家强调几个地方,首先就是分割中序遍历与后序遍历顺序是一定不能乱的,大家务必注意,因为我后续找到了根节点我只有先去分中序遍历,这样我就找到了左子树与右子树,进而我才可以分和后序遍历,接着我递归创建左右子树,最后返回根节点就可以了,在调用函数的过程中我们考虑特殊情况就是两种遍历有一种长度是0的话就直接判断此树为空树。本题我就讲解到这里。
今天的题目其实还是很刺激的,尤其是最后一道题目一定要有一个清晰的逻辑,前面两道其实又用到了我们前面的知识,最后一道题分割两种遍历方式的数组的时候,我感觉又回到了梦开始的地方,区间的开闭其实我们在二分的时候就讲过,而且很重要,否则二分里面不注意开闭很容易写出死循环,好今天的讲解就到这里,我们明天的题目再见!