前面几篇文章我们学习了搜索二叉树,以及搜索二叉树的应用,包括性能分析,这篇文章,我们一起来做一些二叉树相关的面试题。
这些题目更适合使用C++完成,难度也更大一些
题目:link
大家可以自己先看一下题目
我们一起来分析一下
题目的要求是给我们一棵二叉树,让我们用前序遍历的方式把它转换成一个由整数和括号组成的字符串。
我们观察它给的用例会发现其实整数就是每个结点的值,括号其实是把每棵树的左右子树括起来。
另外还要求省略掉不必要的空括号对,但是又不能无脑的全部省略掉,省略后不能影响字符串与原始二叉树之间的一对一映射关系。
所以,解这道题之前,我们可以先来分析一下,哪些情况需要省略空括号,哪些情况不能省略
那对照着图我们很容易得出,括号的处理应该是这样的:
- 首先不为空的情况对应子树的括号肯定不省略
- 左右子树都为空,左右子树的括号都要省略
- 右为空但左不为空,右子树的括号省略
- 右不为空但左为空,左子树的括号不省略
那理清思路,我们来写一下代码
我们可以先不考虑省略括号的问题,把完整的搞出来看一下
其实很简单,前序遍历二叉树,把结点对应的字符放到字符串里面,同时遍历每一个左右子树前后,加一对括号进去就行。
看一下结果
那现在我们把括号省略一下:
首先我们来看左子树,根据我们上面的分析
对于左子树来说:
- 如果左子树不为空,肯定不省略
- 如果左子树为空,那左子树的括号是否省略就要看右子树了:
右子树也为空,那左子树省略了
右子树不为空,那左子树就不能省略了
所以这样写就行了
那右子树就好处理了:
AC代码:
class Solution {
public:
string tree2str(TreeNode* root) {
if(root==nullptr)
return "";
string ret=to_string(root->val);
if(root->left||root->right)
{
ret+='(';
ret+=tree2str(root->left);
ret+=')';
}
if(root->right)
{
ret+='(';
ret+=tree2str(root->right);
ret+=')';
}
return ret;
}
};
题目:link
其实二叉树的层序遍历我们在二叉树初阶是讲过的:
借助一个队列就可以搞
先让根结点入队列,然后如果队列不为空,就出对头数据,并把对头数据的孩子结点带入队列,然后继续出对头数据,再将其孩子带入队列,依次循环往复,直到队列为空,就遍历完成了。
最终出队列的顺序就是层序遍历的顺序。
但是我们当时只是层序遍历打印结点的值,而这道题目的要求有些不同:
所以这道题的关键不在于如何进行层序遍历,而是如何控制把每一层的遍历结果放在不同的数组里面,最后放到一个二维数组里面。
那我们来分析一下
首先第一种思路我们可以借助两个队列来搞:
一个队列就是去放结点的指针,利用队列的先进先出,上一层带下一层,完成二叉树的层序遍历。
另外一个队列用来存放对应结点的层数,比如根结点入队列时存一个1,根结点出去把他的孩子带进队列,带进几个孩子,就存几个2(上一层的层数+1)
那这样我们就能区分不同结点的层数,从而把不同层的结点按照层序遍历的顺序放到不同的是数组里面。
不过呢,其实我们用一个队列也可以搞定
一个队列怎么搞呢?
一个队列的话需要我们再增加一个变量,去记录每一层结点的个数,每出一个,就
- -
一次,一层出完,继续记录下一层的个数,这样确保每一层的结点能放到不同的数组中。
比如
以这棵树为例,root不为空,让根结点入队列,第一层的个数是1,这是确定的,然后根结点出队列,把它的孩子9和20带进去。
那第一层出完,第二层的所有结点就都进入队列了,那此时第二层的个数怎么获取?
,此时队列的大小是不是就是当前层的个数,我们让那个记录的变量更新为队列的大小就行了。
那后面也是这样,依次循环往复,就可以实现分层。
那我们来写一下代码,这里我就只写第二种思路的代码了
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*> q;
int levelSize=0;
if(root)
{
q.push(root);
levelSize=1;
}
vector<vector<int>> ret;
while(!q.empty())
{
//tmp保存每一层的结果
vector<int> tmp;
//通过levelSize控制一层一层出
while(levelSize--)
{
TreeNode* front=q.front();
q.pop();
tmp.push_back(front->val);
//带入当前出队列结点的孩子
if(front->left)
{
q.push(front->left);
}
if(front->right)
{
q.push(front->right);
}
}
ret.push_back(tmp);
levelSize=q.size();
}
return ret;
}
};
题目:link
跟上一题差不多,但是这次要求我们从下往上去进行层序遍历
这道题呢乍一看好像还挺不好搞呢,的从下往上进行层序遍历,再把每一层分开。
但是,其实有个很简单的方法:
我们还是从上往下遍历,最后把得到的二维的vector逆置一下不就行了。
STL的算法库里面是有reverse函数的,这个我们之前也提到过
所以,其实把上一题代码拷贝一下,最后把得到的二维vector逆置一下就了
class Solution {
public:
vector<vector<int>> levelOrderBottom(TreeNode* root) {
queue<TreeNode*> q;
int levelSize=0;
if(root)
{
q.push(root);
levelSize=1;
}
vector<vector<int>> ret;
while(!q.empty())
{
//tmp保存每一层的结果
vector<int> tmp;
//通过levelSize控制一层一层出
while(levelSize--)
{
TreeNode* front=q.front();
q.pop();
tmp.push_back(front->val);
//带入当前出队列结点的孩子
if(front->left)
{
q.push(front->left);
}
if(front->right)
{
q.push(front->right);
}
}
ret.push_back(tmp);
levelSize=q.size();
}
reverse(ret.begin(),ret.end());
return ret;
}
};
题目给我们一个搜索二叉树,要求我们转换为一个排序的双向链表,并作出了以下要求
1.要求不能创建任何新的结点,只能调整树中结点指针的指向。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继
2.返回链表中的第一个节点的指针
3.函数返回的TreeNode,有左右指针,其实可以看成一个双向链表的数据结构
4.你不用输出双向链表,程序会根据你的返回值自动打印输出
5.空间复杂度O(1)(即在原树上操作),时间复杂度 O(n)
那这道题要怎么解呢?
首先有一种比较简单的方法是:
我们可以中序遍历搜索二叉树(中序遍历的结果就是升序嘛),按照遍历的顺序把所有结点放到一个容器里比如vector就可以。
然后再遍历vector去依次改变指针的指向将它转换成双向链表。
但是呢:
题目要求空间复杂度为O(1),不过OJ一般不太能限制这个空间复杂度,所以如果这样写的话应该也可以通过。
那我们这里就不写这种实现了
那在原树上进行操作,怎么搞呢?
单独写一个递归函数,在中序遍历的同时直接改变指针的指向。
那中间这个链接的过程怎么写呢?
在遍历的过程中我们记录一下前驱结点prev,prev初始值为空nullptr
中序遍历的话第一个结点是4,我们用cur记录每次递归的结点。
那此时怎么按照双向链表的形式去链接呢?
我们让cur的left指针指向prev
然后让prev=cur
,更新一下prev的值
然后继续继续递归到下一个有效结点时6(4的左右都为空就会往上返回),那这一层递归的cur就是6
那此时又会执行cur->left=prev
那现在只处理了left指针,那right指针呢?
,那从现在开始(第二个结点)其实就要处理了,怎么搞呢?
我们让prev->right=cur
那大家看,此时4和6两个结点是不是就链接好了啊。
注意我们第一次是不是没有处理right指针啊,因为不用处理:
第一次的prev是空指针,并不是有效结点,不需要管,而且你对空指针解引用也会出问题的。
所以,对于prev,当它不为空的时候再去链接。
那后续就接着递归走就行了,这样中序遍历完毕,就把二叉树成功转换成一个双向链表了。
所以我们的递归转换的函数是这样的:
那题目要求我们返回转换后链表中第一个结点的指针:
所以在题目给的原始函数接口中,我们只需要拿到转换之后链表的第一个结点指针,即二叉树中序遍历的第一个结点的指针。
所以完整的代码是这样的:
如果大家感觉还不是特别明白转换的过程,建议自己画一下函数调用的递归展开图,这个对理解递归过程很有帮助。
class Solution {
public:
void InOrderConvert(TreeNode* cur,TreeNode*& prev)
{
if(cur==nullptr)
return;
InOrderConvert(cur->left, prev);
cur->left=prev;
if(prev)
prev->right=cur;
prev=cur;
InOrderConvert(cur->right, prev);
}
TreeNode* Convert(TreeNode* pRootOfTree) {
if(pRootOfTree==nullptr)
return nullptr;
TreeNode* prev=nullptr;
InOrderConvert(pRootOfTree,prev);
TreeNode* head=pRootOfTree;
while(head->left)
head=head->left;
return head;
}
};
这道题目呢让我们根据一棵二叉树的前序和中序遍历序列构建这棵二叉树,返回其根结点。
那我们先来分析一下,有了前序和中序序列,我们能得到什么?
就以这棵二叉树为例
大家想前序遍历是怎么走的:根、左子树、右子树
所以,有了前序序列我们能确定什么?
,是不是可以确定根啊,前序遍历的第一个结点是不是就是整棵树的根结点啊。
当然我们只能确定一个,它里面的子树的根我们是不能够确定的(所以后面要递归处理它的子树)
那在这里它的根结点就是3。
然后中序遍历我们可以确定什么?
我们知道了根,是不是可以用过中序遍历确定左右子树的区间啊,因为中序是左子树、根、右子树
所以:
那能够确定根结点和左右子树区间,我们就可以走一个前序递归去创建这棵树了:
首先构建根结点,然后再传左右子树区间去递归构建左子树和右子树,左右子树递归的时候,同样划分为根和左右子树分别进行处理。
最后把构建好的左右子树链接到根结点上就好了。
那我们来写一下代码:
由于我们这里要去递归,所以这里还是要写一个子函数:
解释一下新增的这三个参数:
因为要根据前序序列去构建,所以要有一个下标表示当前遍历到哪个位置了,因为每次递归构建的子树是不同的,对应的前序序列是不同的,所以prei
用来做这个下标(注意这里应该传引用,在不同的递归层里面我们应该一直改变的是同一个下标)。
然后我们递归左右子树的时候要划分对应的区间,所以这里inbegin inend
标识每次递归对应的区间。
注意:
prei
是遍历前序序列的下标,而区间是根据中序序列划分的区间。在递归过程中,我们是拿到对应的由中序序列确定的子树区间,然后按照当前子树对应的前序遍历的顺序构建的。
那我们来实现函数体:
每次递归的时候,上来我们可以先把根结点构建了。
那根结点构建之后,我们要递归构建左右子树,所以我们要先在中序序列中将对应左右子树的区间划分出来
那要分割的话我们的先把中序序列中的根结点找出来,通过根结点分割
那区间划分好,我们就去递归创建左右子树并连接到根结点上
,然后还有关键的一步,我们的递归是不是还没有结束条件啊。
那什么时候结束呢?
如果递归的区间不存在是不就要停止了啊(其实就是左右子树递归到空的情况)
比如像这样:
那我们把递归结束条件加上
然后,下面调用一下就可以了
class Solution {
public:
TreeNode* _buildTree(vector<int>& preorder, vector<int>& inorder, int& prei, int inbegin, int inend) {
//递归结束条件
if(inend<inbegin)
return nullptr;
//构建根
TreeNode* root=new TreeNode(preorder[prei]);
//分割左右子树区间
int rooti=inbegin;
while(rooti<inend)
{
if(inorder[rooti]!=preorder[prei])
rooti++;
else
break;
}
//[inbegin, rooti-1] rooti [rooti+1, inend]
//递归构建左右子树
prei++;
root->left = _buildTree(preorder, inorder, prei, inbegin, rooti-1);
root->right = _buildTree(preorder, inorder, prei, rooti+1, inend);
return root;
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
int i=0;
return _buildTree(preorder,inorder,i,0,inorder.size()-1);
}
};
题目链接: link
上一道题是通过前序遍历和中序遍历序列构建二叉树,这道题是通过中序遍历和后序遍历构建:
那把上一道题做会之后,再来看这个就很简单了:
其实思路是差不多的。
这次给的是中序和后序序列,那我们可以通过后序遍历确定根,后序遍历是左子树、右子树、根,所以后序遍历的最后一个就是根结点。
然后还是利用中序遍历分隔左右子树区间。
那这次我们的构建顺序是什么?
首先肯定还是先构建根,所以这次我们倒着遍历后序序列,倒数第一个元素就是整棵大二叉树的根结点。
然后构建完根之后呢?
构建完根之后我们应该先构建右子树,然后构建左子树
那代码其实也没有多大变化,简单修改一下就行了
class Solution {
public:
TreeNode* _buildTree(vector<int>& inorder, vector<int>& postorder, int& posti, int inbegin, int inend) {
//递归结束条件
if(inend<inbegin)
return nullptr;
//构建根
TreeNode* root=new TreeNode(postorder[posti]);
//分割左右子树区间
int rooti=inbegin;
while(rooti<inend)
{
if(inorder[rooti]!=postorder[posti])
rooti++;
else
break;
}
//[inbegin, rooti-1] rooti [rooti+1, inend]
//递归构建左右子树
posti--;
//先右子树
root->right = _buildTree(inorder, postorder, posti, rooti+1, inend);
//后左子树
root->left = _buildTree(inorder, postorder, posti, inbegin, rooti-1);
return root;
}
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
int i=postorder.size()-1;
return _buildTree(inorder,postorder,i,0,inorder.size()-1);
}
};