代码随想录训练营Day18 | Leetcode 513、112、113、106、105
一、513 找树左下角的值
题目链接:513 找树左下角的值
核心:明确要求解的是最底层最左边的节点值(可能在左子树也可能在右子树)
(1)层序遍历:较为简单,只需记录最后一层的第一个节点值(实现时是记录每一层的第一个节点值,但下一层会覆盖前一层的节点值,因此最终记录的是最后一层的第一个节点值);
(2)递归法:最底层即最大深度所在层的最左边节点值,明确深度最大的叶子节点一定是最后一行,以及优先左边搜索时遇到的叶子节点即最左边的值,故使用前序遍历计算二叉树的深度。
第一,确定递归函数的参数和返回值:参数是树的根节点和记录最大深度的int变量;返回值为void,因为无需返回值;(主函数需返回最左边节点值,因此设置2个全局变量,其一记录最大深度,其二记录最大深度时最左边节点值;
第二,确定终止条件:遇到叶子节点则更新最大深度,并记录最大深度时的叶子节点值;
第三,确定单层递归的逻辑:递归时需使用回溯,即每次递归前需对深度加1,递归后需要回溯至上一层,故对深度减1.
int maxDepth=INT_MIN; //全局变量,记录二叉树的最大深度
int res=0; //全局变量,记录最大深度最左边的节点值
void getDepth(TreeNode* node,int depth)
{//前序遍历:中左右
if(!node->left && !node->right)
{//叶子节点【终止条件】
if(depth>maxDepth)
{
maxDepth=depth; //更新最大深度
res=node->val; //更新最大深度时的叶子节点值
}
return; //理解为何此处需要return?实际递归函数没有return也正确,为什么?
}
//【中】不存在处理
if(node->left)
{//左
depth++;
getDepth(node->left,depth);
depth--; //回溯至上一个节点
}
if(node->right)
{
depth++;
getDepth(node->right,depth);
depth--; //回溯至上一个节点
}
}
int findBottomLeftValue(TreeNode* root) {
//递归法:利用前序遍历求解二叉树最大深度
getDepth(root,1); //调用递归函数,根节点的深度为1
return res;
/*
//层序遍历:记录最后一层的第一个节点值
queue<TreeNode*> que;
int res=0;
if(root)
que.push(root);
while(!que.empty())
{
int size=que.size();
for(int i=0;i<size;++i)
{//逐层遍历
TreeNode* node=que.front(); //队头元素
que.pop();
if(i==0) //第一个元素,记录第二层时会覆盖第一层,最终保留最后一层的第一个元素
res=node->val;
if(node->left)
que.push(node->left);
if(node->right)
que.push(node->right);
}
}
return res;
*/
}
二、112 路径总和
题目链接:112 路径总和
核心:递归+回溯,判断是否存在满足路径之和为targetSum的一条路径
(1)递归:(前序遍历,对中无需处理)
第一,明确递归函数的参数和返回值:参数是树的根节点和路径之和sum,要求返回是否存在,故返回值是bool;
第二,明确终止条件:当前节点为叶子节点,且当前路径和sum被减至0(每经过一个节点,则将初始路径和sum减去该节点值,若存在一条路径满足条件,则此时sum=0);否则,即当前节点是叶子节点但sum不为0,则说明此条路径不符合条件。
第三,明确单层递归的逻辑:递归左子树或右子树时,sum需减去当前节点值,然后进行递归处理,若调用的递归函数返回true表示此路径满足条件,再次返回true,否则回溯至上一层,sum需加上当前节点值;
注意:递归函数的最后一次处理是返回false,如何理解?
bool traversal(TreeNode* node,int sum)
{//递归函数:返回true、false,sum是初始路径和,每次减去节点值,若最后为0且当前节点是叶子节点,说明满足条件
if(!node->left && !node->right && sum==0)
return true; //叶子节点且当前路径和被减至0
else if(!node->left && !node->right)
return false; //叶子节点但当前路径和不为0
if(node->left)
{//左
sum-=node->left->val; //递归,处理节点
if(traversal(node->left,sum))
return true; //这一步不太理解!
sum+=node->left->val; //回溯
}
if(node->right)
{//右
sum-=node->right->val;
if(traversal(node->right,sum))
return true;
sum+=node->right->val; //回溯
}
return false; //必须有此返回,为什么?
}
bool hasPathSum(TreeNode* root, int targetSum) {
//递归法:使用前序遍历,注意回溯
if(!root)
return false; //根节点为空时不可调用递归函数
return traversal(root,targetSum-root->val);
/*
//迭代法:使用栈模拟前序遍历
stack<pair<TreeNode*,int>> stk; //使用pair记录节点和路径之和
if(!root)
return false;
stk.push(pair<TreeNode*,int>(root,root->val));
while(!stk.empty())
{
pair<TreeNode*,int> node=stk.top(); //栈顶元素
stk.pop();
if(!node.first->left && !node.first->right && targetSum==node.second)
return true; //遇到叶子节点且当前节点路径之和与给定sum相等,则符合要求
if(node.first->right) //先压入right,在栈中弹出才是先left后right
stk.push(pair<TreeNode*,int>(node.first->right,node.second+node.first->right->val)); //入栈的是当前节点的右孩子,以及当前节点值与当前节点右孩子值这条路径之和
if(node.first->left)
stk.push(pair<TreeNode*,int>(node.first->left,node.second+node.first->left->val));
}
return false; //遍历二叉树所有节点都没有找到一条路径之和为targetSum
*/
}
扩展: 113 路径总和II
题目链接:113 路径总和II
核心:与112的思想相同,但区别是需要返回满足条件的路径组合
定义2个全局变量,其一是记录满足条件的所有路径(二维数组),其一是记录某一条满足条件的路径。
第一,明确递归函数的参数和返回值:参数是树的根节点和路径目标和sum,要返回的路劲用全局变量声明,因此递归函数无需返回任何变量;
第二,明确终止条件:当前节点为叶子节点且sum为0(每经过一条路径的节点sum都需要减去当前节点值),则该条路经(从根节点开始由各节点组成的数组)是满足条件的,需要记录到二维数组的所有路径中,并return;否则,即当前节点为叶子节点但sum不为0,说明该条路径不满足条件需要立即return;
第三,明确单层递归的逻辑:在递归处理左、右子树时,先将节点值push到当前路径,并在sum上减去当前节点值,然后调用递归函数记录是否存在满足条件的路径,最后需要回溯至上一层节点,即sum需加上当前节点值,并将当前节点从当前路径中弹出。
注意:对递归函数中三处return的含义理解不够清晰。
vector<vector<int>> res; //记录所有满足条件的路径
vector<int> path; //记录满足条件的一条路径
void traversal(TreeNode* node,int sum)
{
if(!node->left && !node->right && sum==0)
{//叶子节点且路径之和满足条件
res.push_back(path); //当前节点所在的路径push到res,并返回
return;
}
if(!node->left && !node->right)
return; //不满足条件直接返回
if(node->left)
{//左
path.push_back(node->left->val); //左孩子节点值加入到此路径
sum-=node->left->val;
traversal(node->left,sum);
sum+=node->left->val; //回溯
path.pop_back(); //回溯,将当前节点左孩子弹出
}
if(node->right)
{//右
path.push_back(node->right->val);
sum-=node->right->val;
traversal(node->right,sum);
sum+=node->right->val; //回溯
path.pop_back(); //回溯
}
return; //如何理解此处的return?
}
vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
//递归法:
res.clear();
path.clear();
if(!root)
return res;
path.push_back(root->val); //root不为空时需记录此节点
traversal(root,targetSum-root->val); //调用递归函数
return res;
}
三、106 从中序与后序遍历构造二叉树
题目链接:106 从中序和后序遍历构造二叉树
核心:明确利用中序和后序遍历构造的顺序,由后序遍历的最后一个节点确定每一层的root,然后利用该root对中序遍历分成左子树和右子树,进而对后序遍历分成左子树和右子树(除了此时的root),分别递归处理左、右子树,直到root为叶子节点
第一,明确递归函数的参数和返回值:参数是中序和后序遍历数组,返回值是根节点root,类型是TreeNode*;
第二,明确递归的终止条件:后序遍历数组为空时说明根节点为空;
第三,明确单层递归逻辑:
(1)获取后序遍历数组的最后一个节点,即当前层的root,并且判断此时root是否为叶子节点;
(2)由root节点值确定中序遍历数组的切割点;
(3)切割中序数组,得到左中序数组和右中序数组;
(4)切割后序数组,得到左后序数组和右后序数组,每次切割前需去除后序数组的最后一个节点,且后序数组切割后得到的左、右后序数组大小应与左、右中序数组大小相同,因此可根据左中序数组的长度来切割后序数组;
(5)递归处理左子树和右子树。
注意:切割时一定要保证循环不变量,即统一使用左闭右开或左闭右闭。
TreeNode* traversal(vector<int>& inorder,vector<int>& postorder)
{//递归函数:传入参数是已知的中、后序遍历数组,返回值是构造的二叉树,即root
if(postorder.size()==0)
return nullptr; //1. 后序遍历数组为空说明当前二叉树为空
//2. 获取后序遍历的最后一个节点,即root
int rootvalue=postorder[postorder.size()-1];
TreeNode* root=new TreeNode(rootvalue); //定义一个根节点并初始化
//3.该root可能为叶子节点,即无左右孩子,无需递归
if(postorder.size()==1)
return root;
//4.找到中序遍历的切割点(rootvalue)
int cutpoint;
for(cutpoint=0;cutpoint<inorder.size();++cutpoint)
{
if(inorder[cutpoint]==rootvalue)
break;
}
//5. 切割中序遍历数组,得到左中序和右中序
//left:[0,cutpoint),right:[cutpoint+1,end)
vector<int> leftInorder(inorder.begin(),inorder.begin()+cutpoint);
vector<int> rightInorder(inorder.begin()+cutpoint+1,inorder.end());
postorder.resize(postorder.size()-1); //去除后序遍历数组的最后一个元素,即root
//6. 根据左中序数组长度切割后序遍历数组
//left:[0,leftInorder.size()),right:[leftInorder.size(),end)
vector<int> leftPostorder(postorder.begin(),postorder.begin()+leftInorder.size());
vector<int> rightPostorder(postorder.begin()+leftInorder.size(),postorder.end());
//7. 递归处理左中序和左后序,右中序和右后序
root->left=traversal(leftInorder,leftPostorder);
root->right=traversal(rightInorder,rightPostorder);
return root; //返回根节点
}
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
//递归法:需定义一个递归函数
if(inorder.size()==0 || postorder.size()==0)
return NULL;
return traversal(inorder,postorder);
}
扩展:105 从前序和中序遍历构造二叉树
题目连接:105 从前序和中序遍历构造二叉树
核心:前序找根,中序分左右!二叉树遍历问题大概率考虑递归思想!!
第一,前序遍历的第一个元素一定是整个二叉树的根节点root;
第二,由根节点root在中序遍历的位置可以划分中序遍历左右子树;
第三,若要递归除了中序遍历的左右子树,仍需要前序遍历的左右子树,而左右子树的size不论什么遍历都是相同的,由此可确定前序遍历的左右子树;
第四,递归处理左右子树,确定二叉树每一层的根节点和其左右节点。
Q:何时结束递归?
A:左右子树的size=1时,即左右子树只有root,此时无需递归,直接return该root。
TreeNode* traversal(vector<int>& preorder,vector<int>& inorder)
{//递归函数,与106思想类似
if(preorder.size()==0)
return nullptr; //1. 前序遍历数组为空表示二叉树为空
//2. 获取前序遍历的第一个节点,即root
int rootvalue=preorder[0];
TreeNode* root=new TreeNode(rootvalue);
//3. 该root可能为叶子节点,无需递归处理
if(preorder.size()==1)
return root;
//4.确定中序遍历的切割点
int cutpoint;
for(cutpoint=0;cutpoint<inorder.size();++cutpoint)
{
if(inorder[cutpoint]==rootvalue)
break;
}
//5.切割中序遍历数组得到左、右中序
//left:[0,cutpoint),right:[cutpoint+1,end)
vector<int> leftInorder(inorder.begin(),inorder.begin()+cutpoint);
vector<int> rightInorder(inorder.begin()+cutpoint+1,inorder.end());
//6.切割前序遍历数组
//left:[1,1+leftInorder.size()),right:[1+leftInorder.size(),end)
//注意:前序遍历的第一个元素总是root,即已被使用,需跳过
vector<int> leftPreorder(preorder.begin()+1,preorder.begin()+1+leftInorder.size());
vector<int> rightPreorder(preorder.begin()+1+leftInorder.size(),preorder.end());
//7.递归处理
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);
}