二叉树的遍历
树的遍历即是:按照某种次序访问树中各个节点,并且每个节点恰好被访问一次。
遍历的方式有以下几种:
先序:V | L | R
中序:L | V | R
后序:L | R | V
层次/广度优先:自上而下,先左后右。
先序遍历
递归版本:
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};*/
void preorder(TreeNode*pnode,(*visit))
{
if(pnode)
{
visit(pnode->val);
preorder(pnode->left,visit);
preorder(pnode->right,visit);
}
}
迭代实现1:
在递归实现中,对左、右子树的递归遍历都类似于尾递归,我们可以以某种方式直接消除。
思路:二分递归 ——>迭代+单递归——>迭代+栈。
void preorder(TreeNode*pnode)
{
stack<TreeNode*> s;
if(pnode)
s.push(pnode);
while(s.empty())
{
TreeNode*p=s.top();
s.pop();
visit(p->val);
if(p->left)
s.push(p->left);
if(p->right)
s.push(p->right);
}
}
分析:每步迭代,都有一个节点出栈并被访问,每个节点入/出栈一次,每次迭代只需要O(1)。总的效率为O(n).
迭代分析2:
可以分析出:整个遍历过程可以划分为自上而下对左侧分支的访问,及随后自下而上对一系列右子树的遍历,不同右子树的遍历相互独立。
实现如下:
//自上而下访问左孩子
void VisitAlongLeftBranch(TreeNode*root,(*visit),stack<TreeNode*> &s)
{
while(root)
{
visit(root->val);
if(root->right)
s.push(root->right);
root=root->left;
}
}
void predorder(TreeNode*root,(*visit))
{
stack<TreeNode*> s;
while(true)
{
VisitAlongLeftBranch(root,visit,s);
if(s.empty()) break;
root=s.top();
s.pop();
}
}
中序遍历
递归实现:
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};*/
void inorder(TreeNode*pnode,(*visit))
{
if(pnode)
{
inorder(pnode->left,visit);
visit(pnode->val);
inorder(pnode->right,visit);
}
}
迭代实现1:
不同于先序遍历,尽管右子树的递归遍历是尾递归,但左子树绝对不是。实际上,实现迭代式中序遍历算法的难点正在于此。由先序遍历的版本2启发,我们找到第一个被访问的节点将其祖先用栈保存,这样,原问题就被分解为依次对若干个右子树遍历的问题。
分析:从根出发沿左分支下行,直到最深的那个节点,这个就是最先被访问的节点。
void AlongLeftBranch(TreeNode*root,stack<TreeNode*>&s)
{
//assert: 所有节点都会进栈,或早或迟;但不会重复进栈
while(root)
{
s.push(root);
root=root->left;
}
}
inorder(TreeNode*pnode)
{
stack<TreeNode*> s
while(true)
{
AlongLeftBranch(pnode,s);
if(s.empty())
break;
//assert: L‐subtree(pnode)为空,或已遍历(相当于空)
pnode=s.top();
s.pop()
visit(pnode->val);
pnode=pnode->right;
}
}
分析:每个节点出栈时其左子树(若存在)已经完全遍历,而右子树尚未入栈,于是,每当有节点出栈,只需要访问它,然后从右孩子出发…..如下图所示:
迭代实现2:
版本2只不过是版本1的等价形式,实现如下:
void inorder(TreeNode*pnode,(*visit))
{
stack<TreeNode*> s;
while(true)
{
if(pnode)
{
s.push(pnode); //根节点入栈
pnode=pnode->left; //深入遍历左子树
}
else if(!s.empty())
{
pnode=s.top(); //尚未访问的最低祖先节点出栈
s.pop();
visit(pnode->val);
pnode=pnode->right; //遍历该祖先的右子树
}
else
break;
}
}
效率分析:迭代版本与递归版本时间复杂度都为O(n),但迭代版本常数系数要远远小于递归版本的常数系数。
迭代实现3:
上面的两种迭代方式都需要使用辅助栈,尽管对算法的渐进时间复杂度没有实质的影响,但是所需要的辅助空间的规模线性正比于二叉树的高度,在最坏的情况下与节点总数相当。为此,我们实现第三个迭代版本,只需要使用O(1)的辅助空间。假设我们定义的树中有指向父节点的指针。
首先,我们要实现一个函数找出中序遍历中的下一个节点,在二叉树下一个节点中我们已经实现,为方便说明,在此还是列出该函数,如下:
struct TreeLinkNode {
int val;
struct TreeLinkNode *left;
struct TreeLinkNode *right;
struct TreeLinkNode *next;
TreeLinkNode(int x) :val(x), left(NULL), right(NULL), next(NULL) {
}
};
*/
TreeLinkNode *nextnode(TreeLinkNode *pnode)
{
TreeLinkNode *pnext=NULL;
if(!pnode)
return pnext;
if(pnode->right)
{
TreeLinkNode *pleft=pnode->right;
while(pleft->left)
pleft=pleft->left;
pnext=pleft;
}
//(1、若为节点A的左子树则下一个节点就为A,2、若为节点A的右子树则 沿着当前节点向上找直到找到一个节点B它是节点C的左子节点 则C就为下一个节点)
else if(pnode->next)
{
TreeLinkNode *pcurrent=pnode;
TreeLinkNode *pparent=pnode->next;
while(pparent&&pcurrent==pparent->right)
{
pcurrent=pparent;
pparent=pcurrent->next;
}
pnext=pparent;
}
return pnext;
}
借助于上面的函数我们可以实现我们的版本3,如下:
void indrder(TreeLinkNode*pnode,(*visit))
{
bool backtrack=false;//回溯标志,前一步是否从右子树回溯——省去栈
while(true)
{
if(!backtrack&&pnode&&pnode->left)
{
pnode=pnode->left;//有左子树且不是刚刚回溯,就深入遍历左子树
}
else//否则,无左子树或者刚刚回溯
{
visit(pnode->val);
if(pnode&&pnode->right)//右子树非空
{
pnod=pnode->right; //遍历右子树
backtrack=false; //关闭回溯标志
}
else
{
if(!pnode=nextnode(pnode)) //中序遍历中没有下一个节点时
break;
backtrack=true;
}
}
}
}
说明:这里相当于将原辅助栈替换成一个标志位backtrack。每当抵达一个节点,借助该标记位即可判断此前是否刚做过一次自下而上的回溯。若不是,则按照中序遍历的侧率优先遍历左子树。反之,若刚刚发生过回溯,则意味着当前节点的左子树已经遍历完成(或者等效的左子树为空),然后深入右子树继续遍历。
虽然属于就地算法,但是要反复的调用nextnode(),因此时间效率有所倒退,渐进的时间复杂度依然保持O(n)。
迭代实现4:
版本4是在版本3上的改进,无须借助标记位和辅助栈。
void indrder(TreeLinkNode*pnode,(*visit))
{
while(true)
{
if(pnode&&pnode->left)
{
pnode=pnode->left;//有左子树,就深入遍历左子树
}
else//否则
{
visit(pnode->val);
while(!pnode||!pnode->right)//在没有右分支处
{
if(!pnode=nextnode(pnode)) //中序遍历中没有下一个节点时 直接退出
return;
else
visit(pnode->val);
}
pnode=pnode->right; //直到有右子树,转向非空的右子树。
}
}
}
后续遍历
递归实现:
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};*/
void postorder(TreeNode*pnode,(*visit))
{
if(pnode)
{
postorder(pnode->left,visit);
postorder(pnode->right,visit);
visit(pnode->data);
}
}
迭代实现:
由于左、右子树的递归遍历,都不是尾递归。因此,我们很难消除递归。我们可以找到第一个被访问的节点将其祖先及其右兄弟用栈保存,这样原问题就被分解为依次对若干颗右子树的遍历问题。
在这里用下图解释说明:
首先我们要明白首先访问谁,有以下几点说明:
1、从根出发下行 尽可能沿左分支 实不得已,才沿右分支。
2、最后一个节点 必是叶子,而且 是从左侧可见的最高叶子(该叶子节点首先接受访问)
整个后续遍历也可以分解成几个片段,每一个片段,分别起始于通路上的一个节点,包括三步:访问当前节点,遍历以右兄弟为根的子树,以及向上回溯转入父节点,并且转入下一片段。
实现如下:
//在以s栈顶节点为根的子树中,找到从左侧可见的最高叶子节点
void gotoHLVFL(stack<TreeNode*> &s)
{
while(TreeNode* pnode=s.top())
{
if(pnode->left) //尽可能向左
{
if(pnode->right) //如果有右孩子,优先入栈
s.push(pnode->right);
s.push(pnode->left); //再转入左孩子
}
else//没有左孩子时,迫不得已
s.push(pnode->right);
}
s.pop();//返回之前,弹出栈顶的空节点
}
void postorder(TreeNode*pnode,(*visit))
{
stack<TreeNode*> s;
if(pnode)
s.push(pnode); //根节点首先入栈
while(!s.empty())
{
if(s.top()!=pnode->parent)//栈顶不是当前节点之父(则必为其右兄),此时要在遍历右兄为根的子树
gotoHLVFL(s);
pnode=s.top();
s.pop();
visit(pnode->val);
}
}
该算法都首先定位对应的最高左侧可见叶节点,并且在此过程中,利用辅助栈逆序保存沿途所经过的各个节点。
在自顶向下查找最高左侧可见叶节点时,始终都是尽可能向左,只有左子树为空的时候才向右。前一种情况下,要让右孩子和左孩子先后入栈,在转向左孩子。后一情况下,只要让右孩子入栈。因此,在主函数的每一步迭代中,如果当前节点的右兄弟存在,则该兄弟必然位于辅助栈栈顶。按照后续遍历次序,此时应转入以右兄弟为根的子树。
为了方便理解,下面举一个实例,演变过程如下:
层次遍历
在所谓层次遍历或广度优先遍历中,确定节点访问次序的原则可以概括为“先上后下,先左后右”。并且在打印二叉树一文中,已经介绍了层次遍历的应用,这里简要说明下。
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};*/
void levelorder(TreeNode*pnode)
{
deque<TreeNode*> s;
if(pnode)
s.push_back(pnode);
while(!s.empty())
{
pnode=s.front();
s.pop_front();
visit(pnode->val);
if(pnode->left)
s.push_back(pnode->left);
if(pnode->right)
s.push_back(pnode->right);
}
}
初始化时先令树根入队,随后进入循环。在每一步迭代中,首先取出队列的首节点,然后其左右孩子顺序入队。一旦发现队列为空遍历即完成。
满二叉树和完全二叉树
- 完全二叉树
完全二叉树的拓扑结构如下:
完全二叉树叶节点只能出现在最底部的两层,且最底层节点均处于此底层节点的左侧。因此,高度为h的完全二叉树,规模介于2^h至2^(h+1)-1之间,反之,规模为n的完全二叉树,高度h=O(logn)。并且叶节点虽然不至于少于内部节点,但是至多多出一个。
- 满二叉树
满二叉树的拓扑结构如下:
所有的叶节点同处于最底层。每一层的节点数达到饱和,因此高度为h的满二叉树由2^(h+1)-1个节点组成。叶节点恰好比内部节点多出一个。