遍历二叉树(非递归)

来源:互联网 发布:淘宝电动车加热手套 编辑:程序博客网 时间:2024/06/05 11:11

遍历二叉树(非递归)

博客链接:递归遍历二叉树

语言 = C++;
博客摘要:

  1. 回顾层序遍历的遍历方式

  2. 采用非递归的方式遍历二叉树;
    主要三种遍历方式:
    @前序遍历
    @中序遍历
    @后序遍历


在上一篇博客中我们提到了遍历二叉树的四种方式:

  1. 前序遍历
  2. 中序遍历
  3. 后序遍历
  4. 层序遍历

其中前三种采用了递归的方式,而层序遍历采用的是非递归的方式,为我们本篇博客坐了铺垫;

我们先来回忆一下层序遍历的大体过程;

用队列辅助存储结点的方式遍历,(具体过程请阅读博客:二叉树的遍历(递归));

代码回顾:

层序遍历    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 );        }    }

既然层序遍历可以通过队列的辅助实现,那么,前三种方式是不是也可以借助一种容器实现呢?
回忆一下我们学习递归的时候经常说得,递归的过程其实和压栈的过程差不多,我们是不是可以利用栈来实现非递归的二叉树遍历呢?

来试一下:
还是以这棵二叉树为例:
这里写图片描述

  1. 前序遍历:
    依然按照先访问当前结点,再访问左孩子,最后访问右孩子的顺序来实现!

如果借助栈来实现的话我们需要先访问当前结点,然后当前结点压栈(因为最后访问右孩子的时候需要)再指向当前结点的左孩子,直到左孩子为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的情况;

后序遍历的演示过程就省略了,压栈过程借鉴前序遍历;

2 1
原创粉丝点击