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;
}
重点还是递归的解法,本题递归的解法不需要处理中结点,因为中结点的处理在题目中没有意义,只要找到符合条件的左下角结点再返回即可。如何找到最底层的结点,想到使用结点的深度,使用一个全局变量来保存现有的最大深度,在遍历的时候与当前结点的深度对比,再更新输出答案。
递归三部曲:
- 输入输出:输入一个是结点,一个是结点的深度,用来与最大深度比较;输出为
void
,不需要返回值; - 终止条件:结点是否为叶子结点,如果是叶子结点再判断是否是最左侧的叶子结点;
- 单层递归逻辑:本题因为不需要处理中结点,所以前中后序遍历都可以
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
。
递归三部曲:
- 输入输出:输入一个是结点不用说,另一个是统计路径和的变量,为
int
;输出是是否有符合条件的路径,为bool
; - 终止条件:当遍历到叶子结点时,说明已经遍历完一条路径了,此时判断是否符合条件;
- 单层递归逻辑:本题需要回溯,回溯在处理左右结点时的逻辑中,并且不需要处理中结点
完整代码如下:
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);
}
};
这里我在每一步的逻辑下都加了空行,没有加注释了。
总结
体会最深的就是递归函数中的返回类型和输入参数,想清楚输入输出是很重要的。其次是判断是否需要回溯,目前求路径肯定是需要回溯的,利用深度的题也是需要回溯的。
如果弄不清楚递归的过程,就自己写完之后去模拟一下,也可以加上标准输出来调试,找到问题出在哪里。