刘晶ID:Tomsdinary
5775次访问,排名15962(2)好友0人,关注者1
我是一个喜欢C++热爱底层编程及算法设计的学生。
Tomsdinary的文章
原创 22 篇
翻译 1 篇
转载 0 篇
评论 1 篇
最近评论
agjyfm:wow gold
文章分类
收藏
    相册
    存档
    软件项目交易
    订阅我的博客
    XML聚合  FeedSky
    订阅到鲜果
    订阅到Google
    订阅到抓虾
    订阅到BlogLines
    订阅到Yahoo
    订阅到GouGou
    订阅到飞鸽
    订阅到Rojo
    订阅到newsgator
    订阅到netvibes

    原创  二叉树后序遍历讨论收藏

    新一篇: 关于我 | 旧一篇: Windows程序的简单包装

    遍历的初步探讨

    由于树结构不再线性,所以对于它的遍历不再如线性结构直观简明。根据树结构特点有三种遍历策略:由于树存在单前驱多后继有一种层次关系,我们可以逐层遍历每一个节点,靠近根的节点层先访问而越深的节点越后访问。此外,我们可以对于树中的每一颗子树,递归的先访问根节点后子树,或者先子树后根节点的策略遍历完整棵树。

    将以上后两种策略映射到我讨论的二叉搜索树(以下简称树)我们得到三种遍历方案:前序遍历,中序遍历和后序遍历。后序遍历是这样一种遍历:对于树中每一个节点我们从根节点开始,递归地先遍历左右孩子树才遍历根节点。也就是说节点在访问子树后 后访问,即“后序”的意思。前序遍历和中序遍历大同小异,在这我们不细讨论。

     

    直观的递归算法

    根据以上对后序遍历的初步讨论,我们很容易的依据其定义写出后序遍历的第一个算法。

     

         算法一:

         后序遍历

         如果当前节点不是空节点

             后序遍历当前节点的左子树

             后序遍历当前节点的右子树

             访问当前节点

     

    C++代码描述:

    //递归后序

    void PostOrder (Tree t, Visit visitor)

    {

           assert(visitor!=NULL);

     

           if ( t!=NULL )

           {

                  PostOrder(t->left, visitor);

                  PostOrder(t->right, visitor);

                  visitor(t->data);

           }

    }

     

    非递归后序遍历算法

    后序递归算法很简单,但非递归的算法却没那么直接简单。非递归后序遍历算法的核心思想是通过显式运用堆栈和控制手段把原本由系统实现的动态堆栈调用模拟出来。其关键在于对显式堆栈的操作上,具体说来就是对树中各节点进栈,出栈和访问的控制上。基于以上朴素的思想我们得到以下算法:

     

    算法二:(自然语言描述)

    bleft //控制标志  当且仅当前一个操作有在当前节点左支活动时为真

    bdelete //控制标志 当且仅当前一个操作有删除动作时为真

    开始

        根为空退出算法

        根节点入栈

        当栈不为空时循环

            取得栈顶元素副本 称当前元素

            如果前一次操作是右支回溯  //A

                 访问 删除当前栈顶元素,修改控制标志,重新开始循环

            如果当前元素为叶子节点  //B

                访问 删除当前栈顶元素,修改控制标志

            否则 如果当前节点有左子树并且不是从左支回溯过来 //C 

               当前节点左子树根入栈,修改控制标志

            否则  //D

                当前节点没有右子树  //DA

                    访问 删除当前栈顶元素,修改控制标志

                否则  //DB

                    右子树根节点入栈 修改控制标志

       结束

     

    我们不妨举几个简单的例子

                       

    对于图一,我们来验证一下我们朴素的愿望。首先我们初始化bleftbdelete分别为真和假,这意味着我们是从上一步左支开始并且没有删除动作。根节点5先入栈。进入住循环,进行条件判断。此时条件C满足,节点4,入栈,控制变量重置;很明显它与我们开始初始化时相同。进入第二次循环,同样的情况发生:条件C满足,节点3入栈;开始新的循环,栈顶节点3有左子树,条件C满足,节点2入栈,新的循环开始。节点1同样可以入栈,此时循环重新开始,由于节点1已是叶子,条件B满足,于是我们访问节点1,删除栈顶节点1。由于我们刚才删除的节点是现在栈顶节点2 的左节点,此时bleftbdelete都置为真,这样在接下来的循环中,条件DA满足,访问节点2并删除之;很明显此时情况和删除节点1时相同,这样如此循环直到所有栈中节点被删除完,退出住循环,算法结束。于是我们得到如是访问序列:1 2 3 4 5

    我们来看看图二的算法流程是怎样进行的。

                 

    同样地,我们将bleftbdelete 设置成真和假。首先节点1入栈,进入主循环。此时取节点1的副本进行控制流选择,条件DB满足,节点1的右节点入栈,控制变量bleftbdelete都设置为假,因为我们进入了当前子树的右支且没有删除行为。节点2入栈后情况与节点1时入出一辙,只有条件DB满足。很容易发现以后的情况都几乎一样,直到节点5入栈以后。现在35都如节点1时的情况处理,它们先后入栈。循环继续,现在发现节点5是叶子,条件B满足,访问节点5,删除栈顶节点5bleftbdelet置为假和真,无疑此控制设置是最强的,它标志着下一步应该右支回溯,于是接下来的循环控制流立即进入条件A的处理过程。访问栈顶节点4,删除之,重置控制变量。而此时情况与节点5时是一样的,于是继续回溯。不难发现接下来的控制流一直在回溯,直到访问过所有节点,算法结束。我们得到如是访问序列:5 4 3 2 1

    我们刚才讨论了对于特殊二叉树算法的工作的具体控制流向。它工作的非常好,没有用到递归函数却有后序遍历的结果。那么,对于一颗普通二叉树情况又如何呢?让我们来讨论一下图三。

             

    同样的控制变量初始化如以上两例。根节点50入栈,开始循环。

    现在容易看出条件C满足,节点25入栈;继续,条件C满足,节点10入栈。很明显节点10为叶子,条件B满足,访问节点10,删除栈顶的10,调整控制变量。我们在当前新栈顶表示的左子树删除了一个节点,bleft为真bdelete为假。现在栈顶元素为25,而控制流满足DB,节点40入栈。继续循环,节点30入栈;继续节点28入栈,节点28是叶子满足条件B,访问28并删除栈顶的28,重置控制变量说明刚才从左支回溯,于是控制流在接下来的循环中进入条件DB控制流操作,节点38入栈,紧接着便访问删除之,最强条件满足,右支回溯。现在栈顶元素为节点30,访问之并出栈,进入下一次循环。因为刚才我们在当前节点表示的子树左支有删除动作,现在控制进入DA。访问节点40,删除栈顶元素40,并右支回溯。又回到栈顶元素25,此时条件A满足,访问之,删除之,更新控制变量,进入下一轮循环。现在根节点50的左支都遍历过了,栈顶元素是50,控制变量标识出应该进行右支遍历了——条件DB满足。于是节点75入栈,在控制变量和当前节点状态控制下节点9080先后入栈。当节点80是栈顶元素时,条件B满足,访问80并删除它;之后条件DA满足,访问90并删除它;右支回溯访问75并删除它,最后再次回到节点50——栈内唯一元素,访问并删除后,栈为空,遍历结束。访问序列依次为:10 28 38 30 40 25 80 90 75 50

       经过上面一番讨论,我们看到算法虽未递归,但达到了递归后序遍历的效果。其核心就是通过每次记录下我们前次的操作来实现的。具体的说就是通过设置控制变量bleftbdelete 的真假来告诉算法我们接下来的循环中控制流应该往哪里走。通过这种控制结合堆栈来模拟递归后序遍历的访问路线。

       算法很明了,易于理解。它完全依据后序遍历的步骤一步一步执行。但或许有一个不清楚的地方:每一个判断依据依据的是什么?修改控制标志如何进行?通过遍历具体的实例,很显然判断依据的依据是当前节点的状态和修改标志的状态;而修改标志操作标识前一个操作的动作。

       以下用C++语言重写该算法:

     

    //非递归后序实现一

    void postorder (Tree t, Visit visitor)

    {

        assert(t!=NULL);

        assert(visitor!=NULL);

     

        std::stack<LPTreeNode> work;

        LPTreeNode tmp=NULL;  //当前访问指针

        LPTreeNode top=NULL;  //栈顶指针

        //控制标志

        bool bleft=true;      //是否刚才在左操作

        bool bdelete=false;   //是否刚才有删除动作

        work.push(t); //根入栈

        while ( !work.empty() )

        {

                tmp=work.top(); //当前访问节点

            //处理刚刚删除右节点的情况——右支回溯

            if ( !bleft && bdelete )

            {

                work.pop();

                visitor(tmp->data); //访问

                //修改控制标志

                if ( work.empty() )

                {

                    break;

                }

                top=work.top();

                bleft=(top->left==tmp ? true : false);

                bdelete=true;

                continue;

            }

            //当前节点是叶子节点

            if ( tmp->left==NULL && tmp->right==NULL )

            {

                work.pop();

                visitor(tmp->data);  //访问

                //修改控制标志

                if ( work.empty() )

                {

                    break;

                }

                top=work.top();

                bleft=(top->left==tmp ? true : false);

                bdelete=true;

            }

            //存在左子树且刚才没有从左回溯

            else if ( tmp->left!=NULL && !( bleft && bdelete ) )

            {

                work.push(tmp->left);

                bleft=true; bdelete=false;

            }

            else //右子树情况

            {

                //不存在右子树

                if ( tmp->right==NULL )

                {

                    work.pop();

                    visitor(tmp->data);  //访问

                    //修改控制标志

                    if ( work.empty() )

                    {

                        break;

                    }

                    top=work.top();

                    bleft=(top->left==tmp ? true : false);

                    bdelete=true;

                }

                else //存在右子树

                {

                    work.push(tmp->right);

                    bleft=false; bdelete=false;

                }

            }//end of else

        }// end of while

    }

     

    从以上自然语言描述和C++语言描述可以看出此算法实现的思想很单纯,也很有利于初学者理解。但它的代码太冗长,控制流语句概括性不强;虽有利于初学者理解但较难实现,因为以上控制语句要严格按以上顺序且控制变量修改起来很麻烦,对效率也有影响。

     

    于是我们会想有没有更好的改进方式了?答案是有的,且更简洁。

     

    算法三:(自然语言描述)

    node //刚删除节点地址

    root //当前节点

    开始:

      root先被赋予树根地址

      node=root;

      root和栈都不为空时循环

        如果root不为空

           root入栈  root指向它的左子树

        否则

           root取栈顶元素副本

           如果root的右孩子为空或者前一个删除的是它右孩子

              访问root  删除栈顶元素  node=root root置空

           否则

              root指向它的右子树

    结束

     

    以下是C++语言描述

    //非递归后序遍历实现二

    void postOrder2(Tree root, Visit visitor)

    {

        assert(visitor!=NULL);

     

        LPTreeNode node=root; //记录最后删除的节点地址核心思想

        std::stack<LPTreeNode> work;

     

        while (root || !work.empty())

        {

            if ( root!=NULL )

            {

                work.push(root);

                root=root->left;

            }

            else

            {

                root=work.top();

                //此句概括性极强

                if (root->right==NULL || root->right==node)

                {

                    visitor(root->data);

                    work.pop();

                    node=root;

                    root=NULL;

                }

                else

                {

                    root=root->right;

                }

            }//end of else

        }//end of while

    }

     

    与第一个非递归后序遍历算法比较之,此算法简洁,概括性极强。那些需要第一个算法用多条控制流和控制变量判断的语句,在这里只是简单的判断前一删除的是不是当前节点的右孩子就搞定了,而辅助变量只有一个而已。但万变不离其宗,此算法如此比第一个算法强也只是形式不同罢了,他们所实现的也只是一种对后序遍历的简单模拟。算法思想相同。如果你根据算法遍历以上各图,你会发现它们进栈和出栈情况很相似,唯一不同的是算法二依赖空指针,空指针也会频繁的进出栈。

     

    接下来的算法四却以不同的实现流得到了相同的解。

     

    算法四:(自然语言描述)

    开始

       根不为空入栈,否则退出

       当栈不为空时循环

          取得栈顶元素副本  称当前元素