5年刷题面试经历,遇到考题千千万,面试很难恰好遇到做过的题。但解题方法是有限的,将套路梳理清楚,面试时就能快速找到相关模板套入,顺利bug-free。
今天小编梳理树题型考点套路。点赞收藏,面试前看一遍,快速通关。
遍历
1.1 递归遍历
树的考题基本都是通过递归回溯来解决,将整个问题拆分为左子树,右子树的子问题来解决,相当借助了树的遍历解决问题。
树的递归遍历分为广度优先搜索和深度优先搜索。
广度优先搜索就是层次遍历。
深度优先搜索是分为前序、中序和后序遍历,前中后序的不同在于头节点是先访问还是中间访问,还是最后访问。
前序访问顺序:头节点,左节点,右节点;中序访问顺序:左节点,头节点,右节点;后序访问顺序:右节点,头节点,左节点。
层次遍历
二叉树层次遍历需要借助队列来保存子节点。
遍历过程想区分不同层,怎么实现?
-
使用一个queue,但是每个节点多用一个字段记录高度。能区分不同的层次。
-
二叉树的层次遍历中可以使用两个queue,区分不同层
vector<vector<int>> levelOrder(TreeNode *root) {
vector<vector<int>> ans;
queue<TreeNode *>qu1;
queue<TreeNode *>qu2;
qu1.push(root);
if (root==NULL)
return ans;
while(!qu1.empty()){
vector <int> items;
while (!qu1.empty()){
TreeNode * front = qu1.front();
items.push_back(front->val);
if (front->left!=NULL)
qu2.push(front->left);
if (front->right!=NULL)
qu2.push(front->right);
qu1.pop();
}
ans.push_back(items);
swap(qu1,qu2);
}
return ans;
1.2 非递归遍历
面试官也会要求使用非递归遍历实现树的遍历。
由于非递归后序遍历非常复杂,所以常考非递归实现前序,中序遍历。
这里提供一个非常好理解的实现方式。
首先,有一个功能函数putLeft,作用就是沿着root节点一路将左节点入栈。前序中序遍历的区别在于,前序遍历在putLeft过程中访问每个节点,而中序遍历是先将所有节点通过putLeft进栈,然后再依次弹出访问。
其余的代码是完全相同的,在栈弹出的过程中将右节点进行putLeft。
void putLeft(Node * root)
{
while(root!=NULL)
{
st.push(root);
root = root->left;
}
}
非递归前序遍历
在putLeft过程中访问每个节点。
stack<Node*>st;
vector<int>nums;
void putLeft(Node * root)
{
while(root!=NULL)
{
st.push(root);
nums.push_back(root->val);
root = root->left;
}
}
void solute(Node * root)
{
putLeft(root);
while(!st.empty())
{
Node * topNode = st.top();
st.pop();
if (topNode->right!=NULL)
{
putLeft(topNode->right);
}
}
}
非递归中序遍历
先将所有节点通过putLeft进栈,然后再依次弹出访问。
stack<Node*>st;
vector<int> nums;
void putLeft(Node * root)
{
while(root!=NULL)
{
st.push(root);
root = root->left;
}
}
void solute(Node * root)
{
putLeft(root);
while(!st.empty())
{
Node * topNode = st.top();
nums.push_back(topNode->val);
st.pop();
if (topNode->right!=NULL)
{
putLeft(topNode->right);
}
}
}
二叉搜索树
二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:
-
若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
-
若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
-
左、右子树也分别为二叉排序树
二叉搜索树需要掌握以下的基础知识:
-
由于二叉搜索树的性质,二叉搜索树的中序遍历是升序的。
-
Successor 代表的是中序遍历序列的下一个节点 (后继节点)。即比当前节点大的最小节点,简称后继节点。先取当前节点的右节点,然后一直取该节点的左节点,直到左节点为空,则最后指向的节点为后继节点。
3. Predecessor 代表的是中序遍历序列的前一个节点 (前驱节点)。即比当前节点小的最大节点,简称前驱节点。先取当前节点的左节点,然后取该节点的右节点,直到右节点为空,则最后指向的节点为前驱节点。
动态规划
树结构也会出现子树重复计算,需要用动态规划来优化冗余。
在刷题有术--动态规划 必备知识 中可知,动态规划思考时是从顶向下思考,思考大问题如果拆解为小问题。而在实现时是“从底向上”实现,先计算出子问题,存储下来,之后借此计算大问题。
在树结构中,借助后序遍历的方式实现“从底向上”,先将左右子树子问题计算出来,存储下来,再计算当前节点。
举个例题,打家劫舍III:
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
https://leetcode.cn/problems/house-robber-iii/
一开始遇到这个新题,考虑使用暴力搜索来解决。当A节点选择偷,则B,C节点就只能选择不偷。当A节点选择不偷,则B,C节点可以选择偷,也可以选择不偷。这里明显有重叠子问题,所以使用动态规划来优化。
使用两个map,分别记录节点不能偷 和 可以偷 两种情况下的最大收益,然后采用后序遍历的方式从底向上 将两个map 填满,最后比较root 节点两种选择的最大值即为答案。
map<TreeNode*,int> dp0;//记录节点不能偷时最大收益
map<TreeNode*,int> dp1;//记录节点可以偷时最大收益
void solute(TreeNode * root)
{
if(root==NULL)
return;
solute(root->left);
solute(root->right);
dp1[root] = max_(root->val +dp0[root->left]+dp0[root->right], dp1[root->left]+dp1[root->right]);
dp0[root] = dp1[root->left]+dp1[root->right];
}
int rob(TreeNode* root) {
dp0[NULL]=0;
dp1[NULL]=0;
solute(root);
return max_(dp0[root],dp1[root]);
}