遍历二叉树(非递归)
博客链接:递归遍历二叉树
语言 = C++;
博客摘要:
回顾层序遍历的遍历方式
采用非递归的方式遍历二叉树;
主要三种遍历方式:
@前序遍历
@中序遍历
@后序遍历
在上一篇博客中我们提到了遍历二叉树的四种方式:
- 前序遍历
- 中序遍历
- 后序遍历
- 层序遍历
其中前三种采用了递归的方式,而层序遍历采用的是非递归的方式,为我们本篇博客坐了铺垫;
我们先来回忆一下层序遍历的大体过程;
用队列辅助存储结点的方式遍历,(具体过程请阅读博客:二叉树的遍历(递归));
代码回顾:
层序遍历
void LevelOrder1()
{
queue<Node> q; //队列(先进先出)
q.push (_root); //根结点先存入队列
while(!q.empty ())
{
Node top = q.front ();
cout<<top->_data <<" ";
q.pop ();
if(top->_left )
q.push (top->_left );
if(top->_right )
q.push (top->_right );
}
}
既然层序遍历可以通过队列的辅助实现,那么,前三种方式是不是也可以借助一种容器实现呢?
回忆一下我们学习递归的时候经常说得,递归的过程其实和压栈的过程差不多,我们是不是可以利用栈来实现非递归的二叉树遍历呢?
来试一下:
还是以这棵二叉树为例:
- 前序遍历:
依然按照先访问当前结点,再访问左孩子,最后访问右孩子的顺序来实现!
如果借助栈来实现的话我们需要先访问当前结点,然后当前结点压栈(因为最后访问右孩子的时候需要)再指向当前结点的左孩子,直到左孩子为NULL;
再从栈顶取出元素访问右孩子(为什么从栈顶取? 因为栈顶是最后一个压入栈的结点说明她的左孩子为NULL,当前结点也访问过了,就该访问右孩子了),注意:这里右孩子也有可能有左右孩子,就得注意这里的循环问题了!!!
具体实例分析过程: (还是上图的例子)
先看代码再看分析过程:
void PrevOrder2()//前序遍历非递归
{
stack<Node> s;//栈
Node cur = _root;//根结点
while(cur || !s.empty ())
{
while(cur)
{
cout<<cur->_data <<" ";
s.push (cur );
cur = cur->_left ;
}
Node tmp = s.top ();
s.pop();
cur = tmp->_right;
}
cout<<endl;
}
**首先我们有了一个栈:s
接着我们需要得到根节点cur = _root(根节点);**
正式开始:
如果当前的结点cur不为NULL或者栈不为NULL(需要用循环控制)我们就可以继续接下来的过程了;
为什么?
如果当前结点为NULL并且栈也为NULL的话,我们好像没有什么可以继续访问的不是吗?
如果 cur不为NULL的话(又是一个内循环),我们就先访问cur->_data( 1 )(先序遍历),然后指向 1 的节点指针入栈,然后让当前结点指向它的左孩子(2);cur = cur->_left;这就是前面设置循环条件为当前结点左孩子不为null的原因;
接下来直接用节点数据代表cur; 2不为NULL,2入栈,cur指向3;
3不为NULL,3入栈,cur指向3的左孩子即NULL;
cur指向NULL,可以访问栈顶元素的右孩子了,即3的右孩子,记得pop()掉栈顶元素,因为它的自身,左右孩子都访问过了;
cur = s.top()->_right;因为3的右孩子也可能有节点;
比如下图:
所以把3的右结点又当成是一个根节点来循环;
还是以第一个二叉树继续,cur为NULL,直接跳过内层循环,取栈顶元素2(3已经在上次循环时Pop()掉了);访问2的右孩子,pop栈顶元素,即又将2的右孩子当作根节点,cur = cur->_right;
cur指向4,进入内层循环,访问4;4入栈,cur = cur->_left;
cur为NULL,跳出内层循环,取栈顶元素4,pop栈顶元素,访问4的右孩子,cur = cur->_right;
cur为NULL,跳过内层循环,取栈顶元素1,cur指向它的右孩子,cur = cur->_right;
cur指向5,进入内层循环,访问5,5入栈,cur = cur->_left;
cur指向6,访问6,6入栈,出内层循环,取栈顶元素6,pop栈顶元素,cur = cur->_right;
cur为NULL, 跳过内层循环,取栈顶元素5,pop栈顶元素,cur = cur->_right;
cur为NULL并且栈为NULL, 结束!
以上就是走了一次前序遍历的非递归,中序遍历以此类推;
//中序遍历的非递归实现
void Inorder2()
{
stack<Node> s;
Node cur = _root;
while(cur || !s.empty ())
{
while(cur)
{
s.push (cur );
cur = cur->_left ;
}
Node tmp = s.top ();
cout<<tmp->_data <<" ";
s.pop();
cur = tmp->_right;
}
cout<<endl;
}
中序遍历可以以此类推,而后序遍历没有以此类推的原因是,后序遍历略微有坑;
下面我们就详细讲述一下后序遍历;
结合代码看解释更好理解!!!
//后序遍历非递归
void _PostOrder3(Node root)
{
stack<Node> s;
Node cur = _root; //保存当前结点
Node prev = NULL; //保存访问的前一个结点
while(cur || !s.empty ())
{
while(cur)
{
s.push (cur);
cur = cur->_left ;
}
Node top = s.top ();
//判断当前结点是否可以访问的两个限定条件
if(top->_right == NULL || top->_right == prev)
{
cout<<top->_data <<" ";
prev = top; //注意更新前一个访问的结点;
s.pop ();
}
//否则说明当前结点的右孩子还没有访问过;
else
cur = top->_right ;
}
}
后序遍历的规则在于,先左后右,最后当前结点;那么我们用栈存储结点访问时,就会出现一种情况,比如不知道当前结点是否可以访问,因为有两种情况可以退回当前结点,比如刚访问过它的左孩子,退回到当前结点,然后去访问它的右孩子,又会退回到当前结点,所有就会出现这个
矛盾;
具体点的比如,下图:
当访问过3后,退回到2,2不能访问,因为要访问2的右孩子,那么去访问4,又退回2,那么问题来了,编译器可不知道你刚才访问的是你的左孩子还是右孩子;
这就需要我们想一种方法让程序知道当前结点是否可以访问了,还是上图为例,如果我们知道当前结点访问的上一个结点是什么,再与要访问的下一个结点比较,如果和下一个结点相同的话,就代表当前结点可以访问了,当然,还有还得判断一种为NULL的情况;
比如:后序遍历访问到4的时候,前一个访问的是3,而4的右孩子为NULL,岂不是不能访问4了,所以,我们还得考虑右孩子为NULL的情况;
后序遍历的演示过程就省略了,压栈过程借鉴前序遍历;