目录
一、根据二叉树创建字符串
题目概述:
给你二叉树的根节点 root ,请你采用前序遍历的方式,将二叉树转化为一个由括号和整数组成的字符串,返回构造出的字符串。
空节点使用一对空括号对 "()" 表示,转化后需要省略所有不影响字符串与原始二叉树之间的一对一映射关系的空括号对。
这道题目就是简单的前序遍历,我们先不考虑省略所有不必要的空括号对。如果不省略,就是每次遍历根,然后添加括号和左子树,然后是右子树,这样看来我们会得到包含空括号对的解。然后对需要去除的情况进行分析发现:
1、如果没有左子树并且没有右子树就不需要添加括号
2、如果没有左子树但是有右子树,则空左子树也需要添加括号
class Solution {
public:
string tree2str(TreeNode* root)
{
//根 左子树 右子树
//一般来说就是根 左子树 右子树
if(root==nullptr)
return string();
string ans;
ans+=to_string(root->val);
//不必要的空括号,如果右为空,就没必要加了,如果左为空,但是右不为空,就要加
//
if(root->left)
{
ans+='(';
ans+= tree2str(root->left);
ans+=')';
}
else if(root->right)//左为空,但是右不为空
{
ans+="()";
}
if(root->right)
{
ans+='(';
ans+=tree2str(root->right);
ans+=')';
}
return ans;
}
};
二、二叉树的层序遍历
题目概述:
给你二叉树的根节点 root
,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
二叉树的层序遍历,借助队列将每一层的节点存储到队列中。比如第一层是3.当队列不空的时候,取队列对头的节点,值存入数组中,如果队头节点有左右孩子,将它的左右孩子节点存入队列。问题是要我们返回一个二维数组,每一个数组都代表一行的节点,怎么监测是在这一行呢?我们假设一个levelSize来记录每一行节点的个数,比如第一个行只有一个根节点,levelSize==1,当访问过根节点levelSize就--,当levelSize减为0的时候,此时队列的大小就是下一行节点的个数!
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root)
{
if(root==nullptr)
return vector<vector<int>>();
vector<vector<int>> vv;
queue<TreeNode*> q;
//记录每一层的个数
int levelSize=1;
q.push(root);
while(!q.empty())
{
vector<int> v;
while(levelSize--)
{
//取头
TreeNode* front=q.front();
q.pop();
v.push_back(front->val);
if(front->left)
{
q.push(front->left);
}
if(front->right)
{
q.push(front->right);
}
}
levelSize=q.size();
vv.push_back(v);
}
return vv;
}
};
三、二叉树的层序遍历Ⅱ
题目概述:
给你二叉树的根节点 root
,返回其节点值 自底向上的层序遍历 。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
理解了上一道题目,这道题目就是小意思了,我们按照正序的方式存储下来后,再翻转一下就可以了。
class Solution {
public:
vector<vector<int>> levelOrderBottom(TreeNode* root)
{
if(root==nullptr)
return vector<vector<int>>();
vector<vector<int>> vv;
queue<TreeNode*> q;
q.push(root);
int levelSize=1;
while(!q.empty())
{
vector<int> levelv;
while(levelSize--)
{
TreeNode* front=q.front();
q.pop();
levelv.push_back(front->val);
if(front->left)
q.push(front->left);
if(front->right)
q.push(front->right);
}
vv.push_back(levelv);
levelSize=q.size();
}
reverse(vv.begin(),vv.end());
return vv;
}
};
四、二叉树的最近公共祖先
题目概述:
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
①递归求解
通过分析我们可以列举出所有的情况。
很轻易就写出递归代码:
class Solution {
public:
bool judgeTree(TreeNode* root,TreeNode* x)
{
if(root==nullptr)
return false;
return root==x|| judgeTree(root->left,x)
||judgeTree(root->right,x);
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
{
if(root==nullptr)
return nullptr;
if(root==p||root==q)
return root;
bool pInleft=judgeTree(root->left,p);
bool pInright=!pInleft;
bool qInleft=judgeTree(root->left,q);
bool qInright=!qInleft;
//是否在左边
if(pInleft&&qInright||pInright&&qInleft)
{
return root;
}
if(pInleft&&qInleft)
return lowestCommonAncestor(root->left,p,q);
else
return lowestCommonAncestor(root->right,p,q);
}
};
这种方式,比较好理解,但是对于极端情况:
对于这样的情况,它的时间复杂度就退化为O(N^2)。
②回溯求解
对于p和q节点,我们是否能求出他们的路径呢?
比如:
p节点也就是7的路径是:3 5 2 7
q节点也就是0的路径是:3 1 0
他们的路径我们用栈来存储,得到路径后就转化为链表相交问题。
求解路径的过程就是回溯的过程。比如我们要找7,先存入3,然后到3的左子树,存入5,5不是我们要找的节点,存入6,然后发现6的左右孩子都为空,那么说明6的左右子树找不到,返回false,同时将6节点 pop出栈。然后去5的右子树查找。(如果5的右子树也找不到,5也要被pop出栈)
代码:
class Solution {
public:
bool GetPath(TreeNode* root,TreeNode* x,stack<TreeNode*>& path)
{
if(root==nullptr)
return false;
//先入栈
path.push(root);
if(root==x)
return true;//如果是就返回true
//不是,去左子树记录路径
if(GetPath(root->left,x,path))//在左子树是否能找到
return true;
if(GetPath(root->right,x,path))
return true;//去右子树查找
//都找不到,就pop掉
path.pop();
return false;
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
{
//记录路径
stack<TreeNode*> pPath;//p的路径
stack<TreeNode*> qPath;//q的路径
GetPath(root,p,pPath);
GetPath(root,q,qPath);//q的路径
while(qPath.size()!=pPath.size())
{
if(qPath.size()>pPath.size())
qPath.pop();
else
pPath.pop();
}
//链表相交
while(qPath.top()!=pPath.top())
{
qPath.pop();
pPath.pop();
}
return pPath.top();
}
};
五、二叉搜索树和双向链表
题目概述:
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。如下图所示
比较暴力的一种解法,就是创建一个vector,将每个节点拆下来,然后一个一个去改变指向,这样一来这题目就变成了一个简单题。不过如果加限制条件空间复杂度为O(1)呢?
我们对于每个节点,我们无法预知之后的节点,但是我们可以记录改变之前遍历过的节点!设一个prev前驱指针,cur当前指针。每次cur遍历,prev的右指针指向cur,cur的左指针指向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;
}
};
六、根据一棵树的前序遍历与中序遍历构造二叉树
题目概述:
前序遍历是根、左子树和右子树。中序遍历是左子树、根和右子树。这道题目采用分治的思想,前序遍历可以确定根,根据根去中序遍历里面查找,找到根以后,根的左区间为它的左子树,根的右区间为它的右子树。这样我们采用递归的思想,就可以进一步转换为子问题。前序遍历仅供我们查找根,中序遍历用于确定区间。
代码:
class Solution {
public:
TreeNode* _buildTree(vector<int>& preorder, vector<int>& inorder,int inbegin,int inend,int& prei)
{
if(inbegin>inend)
return nullptr;
int rooti=inbegin;
while(rooti<=inend)
{
if(preorder[prei]==inorder[rooti])
break;
++rooti;
}
TreeNode* root=new TreeNode(preorder[prei++]);
root->left=_buildTree(preorder,inorder,inbegin,rooti-1,prei);
root->right=_buildTree(preorder,inorder,rooti+1,inend,prei);
return root;
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder)
{
int pi=0;
return _buildTree(preorder,inorder,0,inorder.size()-1,pi);
}
};
七、从中序与后序遍历序列构造二叉树
题目概述:
后序遍历:左子树、右子树、根。中序遍历:左子树、根、右子树
后序遍历倒着查找根,因为我们倒着查找根,并且后序遍历的次序是左子树、右子树、根,所以我们重建二叉树的次序应该是根、右子树、左子树,和上题不同的是,先构建右子树再构建左子树。
代码:
class Solution {
public:
TreeNode* _buildTree(vector<int>& inorder,vector<int>& postorder,int inbegin,int inend,int& rooti)
{
if(inbegin>inend)
return nullptr;
int exi=inbegin;
while(exi<=inend)
{
if(postorder[rooti]==inorder[exi])
break;
++exi;
}
TreeNode* root=new TreeNode(postorder[rooti--]);
root->right=_buildTree(inorder,postorder,exi+1,inend,rooti);
root->left=_buildTree(inorder,postorder,inbegin,exi-1,rooti);
return root;
}
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder)
{
//中序遍历:左子树、根、右子树
//后序遍历:左子树 右子树 根
int pi=postorder.size()-1;
return _buildTree(inorder,postorder,0,inorder.size()-1,pi);
}
};
八、二叉树的任一遍历,非递归迭代实现
这个图比较不容易分析,我们换一张图:
向左深度遍历,用栈存储左子树节点,以方便我们取到右子树节点。因为是前序遍历,所以每遍历到一个节点,都相当于它所在树的根,都要存到数组中。这样我们就很容易实现它的非递归实现。
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root)
{
if(root==nullptr)
{
return vector<int>();
}
vector<int> v;
stack<TreeNode*> st;
//3 9 7
//因为是根、左子树、右子树
//所以要先把根存入vector
TreeNode* cur=root;
while(cur||!st.empty())
{
while(cur)
{
st.push(cur);
v.push_back(cur->val);
cur=cur->left;
}
//取st中的节点
TreeNode* top=st.top();
st.pop();
cur=top->right;
}
return v;
}
};
中序遍历也很简单,只是先访问左子树,再访问根。
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root)
{
if(root==nullptr)
return vector<int>();
vector<int> v;
stack<TreeNode*> st;
TreeNode* cur=root;
while(cur || !st.empty())
{
while(cur)
{
st.push(cur);
cur=cur->left;
}
//3 5 6
TreeNode* top=st.top();
st.pop();
v.push_back(top->val);
cur=top->right;
}
return v;
}
};
后序遍历的顺序是:左子树、右子树、根。我们如果还采用上面的办法,需要解决一个问题,那就是根的访问和从栈中pop出节点的时机,这是十分重要的。
比如上图,我们向左子树dfs遍历,栈中存下3、5、6.这时我们取栈顶元素6,取完后可以pop出栈吗?可以!因为6的右子树为空。 我们再取5,5可以访问存入数组吗?不可以,因为5的右子树还没访问,所以我们需要访问完5的右子树才能pop出5.也就是说这时将根存入数组和pop出栈顶元素的条件是:该节点的右子树为空,或者上次访问的节点是该节点的右孩子!我们可以设prev为上次访问节点。
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root)
{
if(root==nullptr)
return vector<int>();
vector<int> v;
stack<TreeNode*> st;
TreeNode* cur=root;
TreeNode* prev=nullptr;
//左子树 右子树 根
while(cur || !st.empty())
{
while(cur)
{
st.push(cur);
cur=cur->left;
}
//3 5 6
// 访问右子树的条件是右子树为空
TreeNode* top=st.top();
//暂时不能pop
//pop的前提是访问过右子树才能pop
if(top->right==nullptr||prev==top->right)
{
st.pop();
v.push_back(top->val);
prev=top;
}
else
{
cur=top->right;
}
}
return v;
}
};