【数据结构与算法】二叉树题型经典面试题
- 1.根据二叉树创建字符串
- 2.二叉树的层序遍历
- 3.二叉树的最近公共祖先
- 4.二叉搜索树与双向链表
- 5.从前序与中序遍历序列构造二叉树
- 6.从中序与后序遍历序列构造二叉树
- 7.二叉树的前序遍历(非递归方法)
- 8.二叉树的中序遍历(非递归方法)
- 9.二叉树的后序遍历(非递归方法)
1.根据二叉树创建字符串
解题思路:
1.本质上考察的还是前序遍历,只不过要求加上括号,将子树括起来。
2.在递归到左子树之前要加上 ( 括号,在左子树递归结束后,要加上 )括号。
3.在递归右子树之前要加上 ( 括号,在右子树递归结束后,要加上 )括号。
4.题目要求空括号要省略,即为空结点点的位置就不需要再递归进去。(递归前后就将括号加上了)。所以只有不为空结点的位置才需要递归进去。
5.题目还有要求当左子树为空,而右子树不为空时,左子树为空的位置需要加上括号。
class Solution {
public:
string tree2str(TreeNode* root) {
if(root==nullptr)
return "";
//如果root为空,直接返回空字符串
string str=to_string(root->val);//要将结点里的数据变成string类型
//相当于前序遍历打印
if(root->left||root->right)//①当左子树存在时,需要打印括号② 当左子树不存在时,右子树存在,则需要打印
{
//递归之前加上(
str+='(';
str+=tree2str(root->left);
str+=')';
//递归之后加上)
}
//当第一个条件为假时才会判断第二个条件,所以当左子树为空,右子树不为空时,也需要打印
if(root->right)//当右子树存在时,需要打印
{
str+='(';
str+=tree2str(root->right);
str+=')';
}
return str;
}
};
2.二叉树的层序遍历
解题思路
1.二叉树的层序遍历需要使用到队列。
2.题目要求将每层的结点输入到一个vector<vector<.int>>> vv数组里
3.首先判断root是否为空,不为空时,直接将root结点入栈。
4.如何一行一行的获取各个结点呢?每pop掉一个结点,就要将它的左右结点入队列。比如pop掉根结点之后,就要将根节点的左右子树入队列。(当左孩子不为空时入队列,当右孩子不为空时再入队列)
4.根据队列的大小,来确定二叉树每层结点的个数。队列里有几个结点,就循环pop几次,每次pop完,要将pop掉的结点的左右子结点再入队列。循环几次,就会将结点的左右子树都入队列中来,所以这样就可以根据队列中元素的多少确定每层的结点个数。
5.每次pop前将队列里的结点值插入到vector<.int> v数组里,然后再将每层有的元素插入到vv里。
6.当队列为空时,则结束循环。
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> vv;
queue<TreeNode*> q;
if(root)
q.push(root);
//首先将根结点入栈
//这时栈里就不为空了
while(!q.empty())
{
vector<int> v;
//根据栈里元素的多少来确定每层的个数
int num=q.size();
for(int i=0;i<num;i++)
{
TreeNode* top=q.front();
v.push_back(top->val);
//每层的元素插入到v里
q.pop();
//将元素pop掉后就要将这个元素的左右孩子入栈
if(top->left)
q.push(top->left);
if(top->right)
q.push(top->right);
}
//将每层的v插入到vv里
vv.push_back(v);
}
return vv;
}
};
3.二叉树的最近公共祖先
解题思路①
1.如何判断某个结点是否是两个指定结点的最近公共祖先呢?
2.当指定的两个结点在当前结点的两侧时(左边和右边),就可以判断当前结点就是最近公共祖先。 或者有一个结点是根结点就可以判断该结点是公共祖先。
3.需要写一个查找FInd函数,用来判断结点是否在这颗树。
4.定义4个判断变量,分别是pinleft ,pinright, qinleft,qinright。用来查找p,q是否在左树还是在右树。
5.所以当两个在同一侧树时,公共祖先就不可能是另外一颗树。就直接递归到这一侧去找。比如同时在左边,则递归到左子树去找,同时在右边,则递归到右子树去找。当两个在两次时,直接返回该结点,该节点就是最近公共祖先。
6.时间复杂度O(N*2)
还有一种方法可以将时间复杂度提升到O(N)程度
解题思路②
1.利用栈将结点的路径(从根节点到该结点的路径)给找到并存起来,那么两个结点的路径中的交点,就是公共祖先。就相当于转化为链表相交问题了。
2.就是Find查找过程中将结点入栈。当不是所找结点的路径时就出栈。先入栈,再比较。
3.如果当前结点为空,直接返回false。
4.如果当前结点不为空,先入栈,然后进行比较。如果当前结点是所找结点,直接返回true。
5.走到这里说明,当前结点不是所找结点,那么就需要递归到左子树去找,如果在左子树里,那么直接返回true。
6.走到这里说明,所找结点不在左子树,那么就递归到右子树去找,如果在右子树,那么就直接返回true。
7.走到这里说明,所找结点也不知右子树,这说明当前结点和左右子树都没有,那么该结点肯定不是所找结点的路径,可以将其从栈中pop掉。然后返回false。
8.将两个结点的路径存到两个栈里后,根据链表相交解题原理,让长的路径先走长度差,这里直接pop长度差次。
9.然后比较栈里相同的结点就是公共祖先。
// 最近公共祖先--》 孩子在左右两侧的结点就是最近公共祖先
class Solution {
public:
// 查找函数,用来查找node结点是否在该树里
bool Find(TreeNode* root, TreeNode* node)
{
if(root==nullptr)
return false;
if(root==node)
return true;
return Find(root->left,node)||Find(root->right,node);
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(root==nullptr)
return nullptr;
//其中一个结点为根节点直接可以返回根节点,根据定义可以找到根节点就是公共祖先
if(root==p||root==q)
{
return root;
}
//到这里说明p 或 q不是根结点,所以要么在左子树或者在右子树
//需要写一个能查找p q结点是在左右子树那一步的函数
bool pinleft,pinright,qinleft,qinright;
pinleft=Find(root->left,p);//查找p是否在左子树,不在左子树就在右子树
pinright=!pinleft;
qinleft=Find(root->left,q);//查找q是否在左子树,不在左子树就在右子树
qinright =!qinleft;
//p,q两个都在左子树
if(pinleft&&qinleft)
{
return lowestCommonAncestor(root->left,p,q);
}
else if(pinright&&qinright)//p,q两个都在右子树
{
return lowestCommonAncestor(root->right,p,q);
}
else//p,q两个在两侧
{
return root;//直接返回该节点。
}
}
};
解题②:
//特殊树会怎么样? 三叉链 二叉搜索树?
//通过这个题目L:理解 通过Find查找这个过程,在加入一个栈, 可以将一个普通二叉树的路径搞出来
class Solution {
public:
//查找某个结点的同时将该结点的路径放入栈里
bool Path(TreeNode* root, TreeNode*node, stack<TreeNode*>& st)
{
if(root==nullptr)
return false;
//当前结点不为空,首先需要入栈
st.push(root);
//入栈以后再进行比较
if(root==node)
return true;
//走到这里表明,当前结点并不是所找的结点,那么就需要递归到左子树去找
if(Path(root->left,node,st))//如果为真,则说明该结点在左子树,如果为假的,就不走这里。递归到右子树去找
{
return true;
}
if(Path(root->right,node,st))//如果为真,则说明该结点在右子树,如果为假的,则说明该结点不在右子树
{
return true;
}
//能走到这里说明,当前结点的左右子树都没有所要找的结点,则说明该结点不可能是所找结点路径
st.pop();
return false;
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
stack<TreeNode*> pPath,qPath;
Path(root,p,pPath);
Path(root,q,qPath);
//因为不知道谁的路径长,所以这里两个都比较一下,只会走一个。
while(pPath.size()>qPath.size())
{
pPath.pop();
}
while(qPath.size()>pPath.size())
{
qPath.pop();
}
while(qPath.top()!=pPath.top())
{
pPath.pop();
qPath.pop();
}
return pPath.top();
}
};
4.二叉搜索树与双向链表
解题思路
1.题目要求二叉搜索树转化成一个排序好的双向链表,那么这里肯定要走中序,因为中序才可以有序。
2.题目要求只能在原树上改动,不能开辟空间。
3.那么就写一个特殊的中序遍历走完,就变成双向链表。
4.中序遍历,左 根 右,这里递归到左子树和递归到右子树是肯定的,要操作的地方就在于根这里
5.要求将左指针变成前驱,右指针变成后继,首先我们要明白,当前结点的右指针指向我们是无法明确的,就像我们不知道明天会发生什么,但左指针我们是可以明确的,就是前一个结点,所以我们可以先将各个结点的左指针改成前驱。要知道前一个结点的位置,我们就要在递归之前记录这个结点。这里我们再传一个参数prev,用来记录每个结点的前一个位置。一开始给nullptr。(注意这个参数需要给引用,因为想要让下一个结点知道上一个结点的改动,就需要用到引用)
6.所每递归到一个结点,就让这个结点的左指针指向prev(也就是前一个结点的位置)。
7.虽然我们不知道每个结点的右指针指向哪里,但我们知道每个结点前一个结点的右指针指向哪里呀,每一个结点的前一个结点的右指针就指向当前结点。我们可以穿越回去,让前一个结点的右指针自己。
8.所以左指针的指向可以在当前结点完成,而右指针的指向需要穿越到前一个结点完成,也就是当前结点能完成左指针的指向,而右指针的指向只能完成前一个结点。
9.题目要求获取双向链表的第一个结点,在二叉树里即最小结点,直接返回最左边结点即可。
class Solution {
public:
void Inoder(TreeNode* cur,TreeNode* &prev)//prev用来记录当前结点的前一个结点
{
if(cur==nullptr)
return ;
//走中序遍历
Inoder(cur->left,prev);
//根 ->有序
cur->left=prev;//当前结点的左指针我们是知道的,但右指针是不知道的
//当前结点是无法知道后一个指针指向那里的,但我可以穿越到前一个结点,将前一个结点的右指针指向我自己,那么前一个结点的右指针就链接上了
if(prev!=nullptr)
prev->right=cur;
//prev要求改变,在整个递归中只有以一个prev
//在每次递归之前,记录当前结点的位置
prev=cur;
//想要让下一次递归看到上一次的改变,就需要用引用,不然看不到
//左指针指向是在当前结点完成的,而右指针的指向是在后一个结点完成的。
Inoder(cur->right,prev);
}
TreeNode* Convert(TreeNode* pRootOfTree) {
TreeNode* prev=nullptr;
Inoder(pRootOfTree,prev);
TreeNode* head=pRootOfTree;
//返回二叉树中最左结点,这里还有要注意,head必须是不为空才可以走这个循环,不然head为空,就没有左指针
while(head&&head->left)
{
head=head->left;
}
return head;
}
};
5.从前序与中序遍历序列构造二叉树
解题思路
1.利用前序方式构建二叉树。即首先创建结点,然后递归左子树,递归右子树。
2.怎么创建呢?我们根据前序遍历可以确定根结点,根据中序遍历可以确定根结点的左右区间。一旦知道左右区间我们就可以递归了。
3.首先根据前序遍历创建结点,然后到中序遍历里找到该根节点,并确定其左右区间。
4.然后根据左区间,递归到左子树。根据右区间,递归到右子树。
5.题目给的函数参数不满足我们所需,所以创建一个子函数,我们需要前序数组的下标,需要中序数组的区间。前序的下标要给引用,因为在递归中一直走的都是这个数组。
class Solution {
public:
//原理:根据前序确定根,中序确定根的左右区间,利用前序遍历方式创建结点
//需要子函数,唯一我们还需要两个数组的下标
TreeNode* _buildTree(vector<int>& preorder, vector<int>& inorder,int& prei,int begin,int end)
{
//当还右一个结点也要创,当没有结点了,就返回
if(begin>end)
return nullptr;
//前序不断确定根节点,通过中序丘吉尔的左右区间
TreeNode* newnode=new TreeNode(preorder[prei]);//根据前序确定根,创建根结点
//再确定该结点在中序中的位置
int pos=begin;
while(pos<=end)
{
if(preorder[prei]==inorder[pos])
break;
else
++pos;
}
//中序确定左右子树区间
//[begin pos-1] pos [pos+1, end ]
++prei;
//确定左右区间后,就可以递归创建左右子树
newnode->left=_buildTree(preorder,inorder,prei,begin,pos-1);
newnode->right=_buildTree(preorder,inorder,prei,pos+1,end);
return newnode;
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
int x=0;
return _buildTree(preorder,inorder,x,0,inorder.size()-1);
}
};
6.从中序与后序遍历序列构造二叉树
解题思路
1.与上一题类似,只不过这里根据后序和中序来构建二叉树。
2.这里要注意的是后序根节点在哪呢?后序,左右根,最后一个才是根节点,并且根节点前一个是右子树。
3.所以我们在创建完结点后,先递归走的是右子树然后再递归左子树。
4.根据后序确定根节点,根据中序确定结点的左右区间。
5.类似于前一个,需要创建一个子函数,用来获取想要的参数,后序的下标,中序的区间。
后序从最后一个开始,依次往前走。
lass Solution {
public:
//后序遍历 :左 右 根
//后序确定根结点,中序确定左右区间, 创建完根,递归应该先递归创建右子树,再创建左子树
TreeNode* _buildTree(vector<int>& inorder, vector<int>& postorder,int& posi,int begin,int end)
{
if(begin>end)
return nullptr;
//根据后序最后一个先确定根结点
TreeNode* newnode =new TreeNode(postorder[posi]);
//从后序中确定根结点后,再从中序中找到这个结点,从而确定左右区间
int j=begin;
while(j<=end)
{
if(inorder[j]==postorder[posi])
break;
else
++j;
}
//这时结点的左右区间就分割出来了
//[begin j-1] j [j+1, end]
--posi;
//首先递归创建右子树
newnode->right=_buildTree(inorder,postorder,posi,j+1,end);
//然后再递归创建左子树
newnode->left=_buildTree(inorder,postorder,posi,begin,j-1);
return newnode;
}
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
int i=postorder.size()-1;
return _buildTree(inorder,postorder,i,0,inorder.size()-1);
}
};
7.二叉树的前序遍历(非递归方法)
解题思路
1.将一颗树看成两部分,左路结点和右子树。
2.前序遍历,我们可以确定最先被访问的是左路各个结点,然后就是从最下面的左路结点的右子树,访问完再访问上一个左路结点的右子树,依次类推。而要实现这样最下面的左路结点的右子树最先被访问,需要用到栈。
3.首先将左路结点全部入栈。(左路结点在入栈之后就已经被访问完了,然后就开始要访问右子树)栈里一旦有元素就说明有右子树要被访问。要访问右树之前先将左路结点pop掉。
4.怎么访问右子树呢?子问题转化,可以将右子树再看成由左路结点和右子树构成,每一个右子树都可以看成由左路结点和右树构成。
5.题目要求放入一个数组里,在访问完结点后,就将结点里的值放入数组里,而栈里的结点是用来找左路结点对应的右树的。
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> v;
TreeNode* cur=root;
//用cur表示要一开始要访问的树
//当栈里还有元素,表明还有右路需要访问
//在访问右子树之前就会将左路结点pop掉,如果不加上cur的话,最后一个左路结点的右树就不会被访问到,因为这时,栈已经空了。
while(cur||!st.empty())
{
//首先需要将左路结点不断入栈
while(cur)
{
st.push(cur);
v.push_back(cur->val);
//将访问完的结点值放入数组里
cur=cur->left;
}
//栈里结点是用来找右路的
//首先将栈里元素取出来,然后pop掉
TreeNode* Top =st.top();
st.pop();
//用子问题的方式去访问右路
cur =Top->right;
}
return v;
}
};
8.二叉树的中序遍历(非递归方法)
解题思路
1.与前序不同的是访问结点的时机不同,前序是根左子树右子树,而中序是先左子树再根然后右子树。
2.前序在入栈之后就访问完根结点了,而中序虽然同样是把左路结点都入栈,在入栈后并不是真正的访问根结点,只有当将左路结点从栈里取出来时,才是真正的访问到这个结点。而取出来这个结点说明它的左路已经被访问完了。比如最下面的左路结点一开始入栈并不是真正访问到,当出栈说明它的左路已经被访问完,它的左路就是空结点,直接可以访问到根结点了(因为左中右顺序)。而访问到根结点后,就可以将根节点值放入数组里。
3.前序中序后序其中本质上就是访问结点的时机不同而已。
4.当根结点访问完,就可以访问右子树了,右子树如何访问呢?子问题转化!
//先将左路入栈
//当栈里元素被取出,表明左路结点已经被访问。
//然后就访问左路结点的右树
//与前序本质就是访问左路结点的时机不同,前序是在入栈之前就访问了,而中序是在入栈取出之后才访问完。
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> v;
TreeNode* cur=root;
//用cur表示要一开始要访问的树
//当栈里还有元素,表明还有右路需要访问
while(cur||!st.empty())
{
//将左路全部入栈
while(cur)
{
st.push(cur);
cur=cur->left;
}
//首先将栈里元素取出来,然后pop掉,这时才访问完左路
TreeNode* Top =st.top();
st.pop();
v.push_back(Top->val);
//访问完根结点后就开始访问右树
//用子问题的方式去访问右路
cur =Top->right;
}
return v;
}
};
9.二叉树的后序遍历(非递归方法)
解题思路
1.与前序中序不同的是访问结点的时间不同,后序是左右根,根结点是最后再访问。
2 前序是将左路入栈之后就访问完了,中序将左路入栈后并不是真正的访问,而当左路被取出来后才是真正的访问到结点。而后序将左路入栈后不是真正的访问,将左路再取出来时,也不是真正的访问这个结点,而还需要再访问这个左路结点的右树后,回来才算真正的访问到这个结点。而如何判断右树是否被访问过了呢?当右树没有被访问时,前一个访问的结点是谁?(左路结点)当右树被访问完时,前一个访问的结点是谁?(右树结点),所以根据上一次访问的结点是否是右树结点来判断是否访问完。
3.当右树为空时,可以直接去访问这个结点了。当右数不为空时,那么先去访问右子树,如何访问右子树?子问题转化。
4.而当右子为空可以访问这个结点,或者当右树被访问完后,也就可以访问这个结点了。
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> v;
TreeNode* cur=root;
TreeNode*prev=nullptr;
while(cur||!st.empty())
{
//首先将左路全部入栈
while(cur)
{
st.push(cur);
cur=cur->left;
}
//当栈里元素取出来时,表明这个结点的左路已经访问完,而要访问这个结点需要判断右路如何
TreeNode* top=st.top();
//右路结点为空,或者上次访问的结点为右路时,则可以访问该结点
if(top->right==nullptr||prev==top->right)
{
v.push_back(top->val);
//要记录一下上一次访问的结点是哪一个
st.pop();
prev=top;
}
else
{
cur=top->right;
}
}
return v;
}
};