二叉树前序、中序、后序遍历非递归写法的透彻解析

二叉树前序、中序、后序遍历非递归写法的透彻解析


前言

在前两篇文章二叉树和二叉搜索树中已经涉及到了二叉树的三种遍历。递归写法,只要理解思想,几行代码。可是非递归写法却很不容易。这里特地总结下,透彻解析它们的非递归写法。其中,中序遍历的非递归写法最简单,后序遍历最难。我们的讨论基础是这样的:

?
1
2
3
4
5
6
7
//Binary Tree Node
typedef struct node
{
     int data;
     struct node* lchild;  //左孩子
     struct node* rchild;  //右孩子
}BTNode;

首先,有一点是明确的:非递归写法一定会用到栈,这个应该不用太多的解释。我们先看中序遍历:

中序遍历

分析

中序遍历的递归定义:先左子树,后根节点,再右子树。如何写非递归代码呢?一句话:让代码跟着思维走。我们的思维是什么?思维就是中序遍历的路径。假设,你面前有一棵二叉树,现要求你写出它的中序遍历序列。如果你对中序遍历理解透彻的话,你肯定先找到左子树的最下边的节点。那么下面的代码就是理所当然的:

中序代码段(i)
?
1
2
3
4
5
6
7
8
BTNode* p = root;  //p指向树根
stack<btnode*> s;  //STL中的栈
//一直遍历到左子树最下边,边遍历边保存根节点到栈中
while (p)
{
     s.push(p);
     p = p->lchild;
}</btnode*>

保存一路走过的根节点的理由是:中序遍历的需要,遍历完左子树后,需要借助根节点进入右子树。代码走到这里,指针p为空,此时无非两种情况:

\

说明:<喎�"http://www.2cto.com/kf/ware/vc/" target="_blank" class="keylink">vcD4KPHA+PC9wPgoKyc/NvNbQ1ru4+LP2wcux2NKqtcS92rXjus2x36OsxuTL/LXEsd+6zb3atePT68zWwtvO3rnYo6yyu7HYu62z9qGjxOO/ycTcyM/Oqs28YdbQ1+69/LGjtOa92rXjy+Oyu7XDyse4+b3ateOho8jnufvE47+0uf3K96Gitv6y5sr3u/m0oaOsyrnTw8Cps+S2/rLmyve1xLjFxO6jrL7Nv8nS1L3iys2ho9fc1q6jrLK708O+wL3h1eK49sO709DS4tLlzsrM4qGjPGJyPgrV+7j2tv6y5sr31rvT0NK7uPa4+b3ateO1xMfpv/a/ydLUu661vc28YaGjCtfQz7jP68/ro6y2/rLmyve1xNfz19PK96Os1+7PwrHfyseyu8rHyc/NvMG91tbH6b/2o7+yu7nc1PXR+aOstMvKsba80qqz9tW7o6yyorfDzsq4w73ateOho9XiuPa92rXjvs3Kx9bQ0PLQ8sHQtcS12tK7uPa92rXjoaO4+b7dztLDx7XEy7zOrKOstPrC69OmuMPKx9Xi0fmjuiAgCjxwcmUgY2xhc3M9"brush:java;">p = s.top(); s.pop(); cout << p->data;
我们的思维接着走,两图情形不同得区别对待: 1.图a中访问的是一个左孩子,按中序遍历顺序,接下来应访问它的根节点。也就是图a中的另一个节点,高兴的是它已被保存在栈中。我们只需这样的代码和上一步一样的代码:

?
1
2
3
p = s.top();
s.pop();
cout << p->data;
左孩子和根都访问完了,接着就是右孩子了,对吧。接下来只需一句代码:p=p->rchild;在右子树中,又会新一轮的代码段(i)、代码段(ii)……直到栈空且p空。 
2.再看图b,由于没有左孩子,根节点就是中序序列中第一个,然后直接是进入右子树:p=p->rchild;在右子树中,又会新一轮的代码段(i)、代码段(ii)……直到栈空且p空。 思维到这里,似乎很不清晰,真的要区分吗?根据图a接下来的代码段(ii)这样的:
?
1
2
3
4
5
6
7
p = s.top();
s.pop();
cout << p->data;
p = s.top();
s.pop();
cout << p->data;
p = p->rchild;

根据图b,代码段(ii)又是这样的:
?
1
2
3
4
p = s.top();
s.pop();
cout << p->data;
p = p->rchild;

我们可小结下:遍历过程是个循环,并且按代码段(i)、代码段(ii)构成一次循环体,循环直到栈空且p空为止。 不同的处理方法很让人抓狂,可统一处理吗?真的是可以的!回顾扩充二叉树,是不是每个节点都可以看成是根节点呢?那么,代码只需统一写成图b的这种形式。也就是说代码段(ii)统一是这样的:
中序代码段(ii)
?
1
2
3
4
p = s.top();
s.pop();
cout << p->data;
p = p->rchild;

口说无凭,得经的过理论检验。 图a的代码段(ii)也可写成图b的理由是:由于是叶子节点,p=-=p->rchild;之后p肯定为空。为空,还需经过新一轮的代码段(i)吗?显然不需。(因为不满足循环条件)那就直接进入代码段(ii)。看!最后还是一样的吧。还是连续出栈两次。看到这里,要仔细想想哦!相信你一定会明白的。 
这时写出遍历循环体就不难了:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
BTNode* p = root;
stack<btnode*> s;
while (!s.empty() || p)
{
     //代码段(i)一直遍历到左子树最下边,边遍历边保存根节点到栈中
     while (p)
     {
         s.push(p);
         p = p->lchild;
     }
     //代码段(ii)当p为空时,说明已经到达左子树最下边,这时需要出栈了
     if (!s.empty())
     {
         p = s.top();
         s.pop();
         cout << setw( 4 ) << p->data;
         //进入右子树,开始新的一轮左子树遍历(这是递归的自我实现)
         p = p->rchild;
     }
}</btnode*>

仔细想想,上述代码是不是根据我们的思维走向而写出来的呢?再加上边界条件的检测,中序遍历非递归形式的完整代码是这样的:
中序遍历代码一
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//中序遍历
void InOrderWithoutRecursion1(BTNode* root)
{
     //空树
     if (root == NULL)
         return ;
     //树非空
     BTNode* p = root;
     stack<btnode*> s;
     while (!s.empty() || p)
     {
         //一直遍历到左子树最下边,边遍历边保存根节点到栈中
         while (p)
         {
             s.push(p);
             p = p->lchild;
         }
         //当p为空时,说明已经到达左子树最下边,这时需要出栈了
         if (!s.empty())
         {
             p = s.top();
             s.pop();
             cout << setw( 4 ) << p->data;
             //进入右子树,开始新的一轮左子树遍历(这是递归的自我实现)
             p = p->rchild;
         }
     }
}</btnode*>

恭喜你,你已经完成了中序遍历非递归形式的代码了。回顾一下难吗? 接下来的这份代码,本质上是一样的,相信不用我解释,你也能看懂的。
中序遍历代码二
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//中序遍历
void InOrderWithoutRecursion2(BTNode* root)
{
     //空树
     if (root == NULL)
         return ;
     //树非空
     BTNode* p = root;
     stack<btnode*> s;
     while (!s.empty() || p)
     {
         if (p)
         {
             s.push(p);
             p = p->lchild;
         }
         else
         {
             p = s.top();
             s.pop();
             cout << setw( 4 ) << p->data;
             p = p->rchild;
         }
     }
}</btnode*>

前序遍历

分析
前序遍历的递归定义:先根节点,后左子树,再右子树。有了中序遍历的基础,不用我再像中序遍历那样引导了吧。 首先,我们遍历左子树,边遍历边打印,并把根节点存入栈中,以后需借助这些节点进入右子树开启新一轮的循环。还得重复一句:所有的节点都可看做是根节点。根据思维走向,写出代码段(i):
前序代码段(i)
?
1
2
3
4
5
6
7
//边遍历边打印,并存入栈中,以后需要借助这些根节点(不要怀疑这种说法哦)进入右子树
while (p)
{
     cout << setw( 4 ) << p->data;
     s.push(p);
     p = p->lchild;
}

接下来就是:出栈,根据栈顶节点进入右子树。
前序代码段(ii)
?
1
2
3
4
5
6
7
//当p为空时,说明根和左子树都遍历完了,该进入右子树了
if (!s.empty())
{
     p = s.top();
     s.pop();
     p = p->rchild;
}

同样地,代码段(i)(ii)构成了一次完整的循环体。至此,不难写出完整的前序遍历的非递归写法。
前序遍历代码一
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void PreOrderWithoutRecursion1(BTNode* root)
{
     if (root == NULL)
         return ;
     BTNode* p = root;
     stack<btnode*> s;
     while (!s.empty() || p)
     {
         //边遍历边打印,并存入栈中,以后需要借助这些根节点(不要怀疑这种说法哦)进入右子树
         while (p)
         {
             cout << setw( 4 ) << p->data;
             s.push(p);
             p = p->lchild;
         }
         //当p为空时,说明根和左子树都遍历完了,该进入右子树了
         if (!s.empty())
         {
             p = s.top();
             s.pop();
             p = p->rchild;
         }
     }
     cout << endl;
}</btnode*>

下面给出,本质是一样的另一段代码:
前序遍历代码二
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//前序遍历
void PreOrderWithoutRecursion2(BTNode* root)
{
     if (root == NULL)
         return ;
     BTNode* p = root;
     stack<btnode*> s;
     while (!s.empty() || p)
     {
         if (p)
         {
             cout << setw( 4 ) << p->data;
             s.push(p);
             p = p->lchild;
         }
         else
         {
             p = s.top();
             s.pop();
             p = p->rchild;
         }
     }
     cout << endl;
}</btnode*>

在二叉树中使用的是这样的写法,略有差别,本质上也是一样的:
前序遍历代码三

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void PreOrderWithoutRecursion3(BTNode* root)
{
     if (root == NULL)
         return ;
     stack<btnode*> s;
     BTNode* p = root;
     s.push(root);
     while (!s.empty())  //循环结束条件与前两种不一样
     {
         //这句表明p在循环中总是非空的
         cout << setw( 4 ) << p->data;
         /*
         栈的特点:先进后出
         先被访问的根节点的右子树后被访问
         */
         if (p->rchild)
             s.push(p->rchild);
         if (p->lchild)
             p = p->lchild;
         else
         { //左子树访问完了,访问右子树
             p = s.top();
             s.pop();
         }
     }
     cout << endl;
}</btnode*>

最后进入最难的后序遍历:

后序遍历

分析
后序遍历递归定义:先左子树,后右子树,再根节点。后序遍历的难点在于:需要判断上次访问的节点是位于左子树,还是右子树。若是位于左子树,则需跳过根节点,先进入右子树,再回头访问根节点;若是位于右子树,则直接访问根节点。直接看代码,代码中有详细的注释。
后序遍历代码
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//后序遍历
void PostOrderWithoutRecursion(BTNode* root)
{
     if (root == NULL)
         return ;
     stack<btnode*> s;
     //pCur:当前访问节点,pLastVisit:上次访问节点
     BTNode* pCur, *pLastVisit;
     //pCur = root;
     pCur = root;
     pLastVisit = NULL;
     //先把pCur移动到左子树最下边
     while (pCur)
     {
         s.push(pCur);
         pCur = pCur->lchild;
     }
     while (!s.empty())
     {
         //走到这里,pCur都是空,并已经遍历到左子树底端(看成扩充二叉树,则空,亦是某棵树的左孩子)
         pCur = s.top();
         s.pop();
         //一个根节点被访问的前提是:无右子树或右子树已被访问过
         if (pCur->rchild == NULL || pCur->rchild == pLastVisit)
         {
             cout << setw( 4 ) << pCur->data;
             //修改最近被访问的节点
             pLastVisit = pCur;
         }
         /*这里的else语句可换成带条件的else if:
         else if (pCur->lchild == pLastVisit)//若左子树刚被访问过,则需先进入右子树(根节点需再次入栈)
         因为:上面的条件没通过就一定是下面的条件满足。仔细想想!
         */
         else
         {
             //根节点再次入栈
             s.push(pCur);
             //进入右子树,且可肯定右子树一定不为空
             pCur = pCur->rchild;
             while (pCur)
             {
                 s.push(pCur);
                 pCur = pCur->lchild;
             }
         }
     }
     cout << endl;
}</btnode*>

总结

思维和代码之间总是有巨大的鸿沟。通常是思维正确,清楚,但却不易写出正确的代码。要想越过这鸿沟,只有多尝试、多借鉴,别无它法。 
转载请注明出处,本文地址:http://blog.csdn.net/zhangxiangdavaid/article/details/37115355 

专栏目录:数据结构与算法目

1.先序遍历非递归算法#define maxsize 100typedef struct{ Bitree Elem[maxsize]; int top;}SqStack;void PreOrderUnrec(Bitree t){ SqStack s; StackInit(s); p=t; while (p!=null || !StackEmpty(s)) { while (p!=null) //遍历左子树 { visite(p->data); push(s,p); p=p->lchild; }//endwhile if (!StackEmpty(s)) //通过下一次循环中的内嵌while实现右子树遍历 { p=pop(s); p=p->rchild; }//endif }//endwhile }//PreOrderUnrec2.中序遍历非递归算法#define maxsize 100typedef struct{ Bitree Elem[maxsize]; int top;}SqStack;void InOrderUnrec(Bitree t){ SqStack s; StackInit(s); p=t; while (p!=null || !StackEmpty(s)) { while (p!=null) //遍历左子树 { push(s,p); p=p->lchild; }//endwhile if (!StackEmpty(s)) { p=pop(s); visite(p->data); //访问根结点 p=p->rchild; //通过下一次循环实现右子树遍历 }//endif }//endwhile}//InOrderUnrec3.后序遍历非递归算法#define maxsize 100typedef enum{L,R} tagtype;typedef struct { Bitree ptr; tagtype tag;}stacknode;typedef struct{ stacknode Elem[maxsize]; int top;}SqStack;void PostOrderUnrec(Bitree t){ SqStack s; stacknode x; StackInit(s); p=t; do { while (p!=null) //遍历左子树 { x.ptr = p; x.tag = L; //标记为左子树 push(s,x); p=p->lchild; } while (!StackEmpty(s) && s.Elem[s.top].tag==R) { x = pop(s); p = x.ptr; visite(p->data); //tag为R,表示右子树访问完毕,故访问根结点 } if (!StackEmpty(s)) { s.Elem[s.top].tag =R; //遍历右子树 p=s.Elem[s.top].ptr->rchild; } }while (!StackEmpty(s));}//PostOrderUnrec
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值