找树左下角的值
题干
题目:给定一个二叉树的 根节点 root,请找出该二叉树的 最底层 最左边 节点的值。假设二叉树中至少有一个节点。
注意:最左边的结点不一定是左叶子结点!!
思路和代码
递归法
将问题分解为找每棵子树最底层的最左边的叶子结点,同时还需要记录这个最左下结点的深度。
题目求深度最大的左下结点,就需要在递归的过程中不断更新结果 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 。
思路和代码
递归法:前序遍历,累加路径和
我们之前做过一道题,是求二叉树的所有路径,我们只需要在这道题的基础上,统计每条路径的和即可。
采用前序遍历,当遍历到叶子结点时表示采集到一条路径,在遍历的过程中不断更新路径和,到达叶子结点时,比较路径和与目标和是否相同,如果相同可以直接返回 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 ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
区别:这道题和上一题不同的地方在于要把满足 路径和 = 目标和 的所有路径返回。
思路和代码
递归法:前序遍历
我们在做上一题的时候,只要找到了一条满足条件的路径就可返回 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 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。
注意:你可以假设树中没有重复的元素。
思路和代码
后序遍历是 “ 左右中 ”,因此后序遍历最靠后的元素,一定是二叉树的中间结点。以该中间结点作为分界,将中序序列分割为左子树的中序序列和右子树的中序序列。这样我们又可以知道左、右子树的结点个数,根据结点个数我们就知道后序遍历 “左右中” 里,前多少个结点属于左子树,后多少个结点属于右子树。(因为每个子树的结点个数肯定都是相同的!)到此为止,我们便知道了左子树、右子树的中序序列和后序序列,以此递归建立子树,最后返回子树的根节点。
递归法
将问题拆解为,先从中序和后序遍历中组建左右子树,最后由左右子树和根节点再拼成大树。
-
递归参数和返回值:参数是中序序列和后序序列,返回值是树的根节点。
-
递归的结束条件:当传入的数组为空,说明已经建完树,要返回空指针。
-
递归顺序:根据后序遍历 “左右中” 的顺序,先以后序遍历的最后一个元素构建根节点;之后根据中序遍历 “左中右” 的顺序,以根节点为分界划分左右子树,分别获取左右子树的中序序列、后序序列,先后对左子树序列和右子树序列递归建树。
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 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
思路和代码
递归法
这道题和上道题的思路本质上一样的。前序序列是 “中左右”,因此每次前序序列的第一个元素肯定就是根节点,找到根结点后再分割中序序列。不同的地方就是找根节点的位置不同罢了。
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. 二叉树的最近公共祖先中介绍)
-
如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(见上文的 “ 路径总和 ” 那道题)
给出哪两种序列可以确定一颗二叉树
前序 + 中序,后序 + 中序都可以确定一颗二叉树;但是前序 + 后序无法确定二叉树,因为如果缺少中序序列,则无法确定左右。