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

二叉树的4种非递归遍历 二叉树的4种非递归遍历,你知道吗? 阅读详情

本文源地址:http://blog.youkuaiyun.com/zhangxiangdavaid/article/details/37115355

前言

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

//Binary Tree Node  
typedef struct node  
{  
     int data;  
     struct node* lchild;  //左孩子  
     struct node* rchild;  //右孩子  
 }BTNode;  

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

中序遍历

分析

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

BTNode* p = root;  //p指向树根  
stack<BTNode*> s;  //STL中的栈  
//一直遍历到左子树最下边,边遍历边保存根节点到栈中  
while (p)  
{  
    s.push(p);  
    p = p->lchild;  
}  

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

说明:

上图中只给出了必要的节点和边,其它的边和节点与讨论无关,不必画出。
你可能认为图a中最近保存节点算不得是根节点。如果你看过树、二叉树基础,使用扩充二叉树的概念,就可以解释。总之,不用纠结这个没有意义问题。
整个二叉树只有一个根节点的情况可以划到图a。

仔细想想,二叉树的左子树,最下边是不是上图两种情况?不管怎样,此时都要出栈,并访问该节点。这个节点就是中序序列的第一个节点。根据我们的思维,代码应该是这样:

p = s.top();  
s.pop();  
cout << p->data;  

我们的思维接着走,两图情形不同得区别对待:
1.图a中访问的是一个左孩子,按中序遍历顺序,接下来应访问它的根节点。也就是图a中的另一个节点,高兴的是它已被保存在栈中。我们只需这样的代码和上一步一样的代码:

p = s.top();  
s.pop();  
cout << p->data;  

左孩子和根都访问完了,接着就是右孩子了,对吧。接下来只需一句代码:p=p->rchild;在右子树中,又会新一轮的代码段(i)、代码段(ii)……直到栈空且p空。

2.再看图b,由于没有左孩子,根节点就是中序序列中第一个,然后直接是进入右子树:p=p->rchild;在右子树中,又会新一轮的代码段(i)、代码段(ii)……直到栈空且p空。
思维到这里,似乎很不清晰,真的要区分吗?根据图a接下来的代码段(ii)这样的:

p = s.top();  
s.pop();  
cout << p->data;  
p = s.top();  
s.pop();  
cout << p->data;  
p = p->rchild;  

根据图b,代码段(ii)又是这样的:

    p = s.top();  
    s.pop();  
    cout << p->data;  
    p = p->rchild;  

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

p = s.top();  
s.pop();  
cout << p->data;  
p = p->rchild;  

口说无凭,得经的过理论检验。
图a的代码段(ii)也可写成图b的理由是:由于是叶子节点,p=-=p->rchild;之后p肯定为空。为空,还需经过新一轮的代码段(i)吗?显然不需。(因为不满足循环条件)那就直接进入代码段(ii)。看!最后还是一样的吧。还是连续出栈两次。看到这里,要仔细想想哦!相信你一定会明白的。

这时写出遍历循环体就不难了:

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;  
    }  
}  

仔细想想,上述代码是不是根据我们的思维走向而写出来的呢?再加上边界条件的检测,中序遍历非递归形式的完整代码是这样的:
中序遍历代码一

//中序遍历  
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;  
        }  
    }  
}  

恭喜你,你已经完成了中序遍历非递归形式的代码了。回顾一下难吗?
接下来的这份代码,本质上是一样的,相信不用我解释,你也能看懂的。
中序遍历代码二

//中序遍历  
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;  
        }  
    }  
}  

前序遍历

分析
前序遍历的递归定义:先根节点,后左子树,再右子树。有了中序遍历的基础,不用我再像中序遍历那样引导了吧。
首先,我们遍历左子树,边遍历边打印,并把根节点存入栈中,以后需借助这些节点进入右子树开启新一轮的循环。还得重复一句:所有的节点都可看做是根节点。根据思维走向,写出代码段(i):
前序代码段(i)

//边遍历边打印,并存入栈中,以后需要借助这些根节点(不要怀疑这种说法哦)进入右子树  
while (p)  
{  
    cout << setw(4) << p->data;  
    s.push(p);  
    p = p->lchild;  
}  

接下来就是:出栈,根据栈顶节点进入右子树。
前序代码段(ii)

//当p为空时,说明根和左子树都遍历完了,该进入右子树了  
if (!s.empty())  
{  
    p = s.top();  
    s.pop();  
    p = p->rchild;  
}  

同样地,代码段(i)(ii)构成了一次完整的循环体。至此,不难写出完整的前序遍历的非递归写法。
前序遍历代码一

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;  
}  

下面给出,本质是一样的另一段代码:
前序遍历代码二

//前序遍历  
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;  
}  

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

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;  
}  

最后进入最难的后序遍历:
后序遍历
分析
后序遍历递归定义:先左子树,后右子树,再根节点。后序遍历的难点在于:需要判断上次访问的节点是位于左子树,还是右子树。若是位于左子树,则需跳过根节点,先进入右子树,再回头访问根节点;若是位于右子树,则直接访问根节点。直接看代码,代码中有详细的注释。
后序遍历代码一

//后序遍历  
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;  
}  

下面给出另一种思路下的代码。它的想法是:给每个节点附加一个标记(left,right)。如果该节点的左子树已被访问过则置标记为left;若右子树被访问过,则置标记为right。显然,只有当节点的标记位是right时,才可访问该节点;否则,必须先进入它的右子树。详细细节看代码中的注释。
后序遍历代码二

//定义枚举类型:Tag  
enum Tag{left,right};  
//自定义新的类型,把二叉树节点和标记封装在一起  
typedef struct  
{  
    BTNode* node;  
    Tag tag;  
}TagNode;      
//后序遍历    
void PostOrderWithoutRecursion2(BTNode* root)  
{  
    if (root == NULL)  
        return;  
    stack<TagNode> s;  
    TagNode tagnode;  
    BTNode* p = root;  
    while (!s.empty() || p)  
    {  
        while (p)  
        {  
            tagnode.node = p;  
            //该节点的左子树被访问过  
            tagnode.tag = Tag::left;  
            s.push(tagnode);  
            p = p->lchild;  
        }  
        tagnode = s.top();  
        s.pop();  
        //左子树被访问过,则还需进入右子树  
        if (tagnode.tag == Tag::left)  
        {  
            //置换标记  
            tagnode.tag = Tag::right;  
            //再次入栈  
            s.push(tagnode);  
            p = tagnode.node;  
            //进入右子树  
            p = p->rchild;  
        }  
        else//右子树已被访问过,则可访问当前节点  
        {  
            cout << setw(4) << (tagnode.node)->data;  
            //置空,再次出栈(这一步是理解的难点)  
            p = NULL;  
        }  
    }  
    cout << endl;  
}

总结
思维和代码之间总是有巨大的鸿沟。通常是思维正确,清楚,但却不易写出正确的代码。要想越过这鸿沟,只有多尝试、多借鉴,别无它法。
以下几点是理解上述代码的关键:

所有的节点都可看做是父节点(叶子节点可看做是两个孩子为空的父节点)。
把同一算法的代码对比着看。在差异中往往可看到算法的本质。
根据自己的理解,尝试修改代码。写出自己理解下的代码。写成了,那就是真的掌握了。
二叉树遍历的非递归算法 非递归的算法主要采用的是循环出栈入栈来实现对二叉树的遍历,下面是过程分析 阅读详情

相关推荐

GD32官方资料学习体系(主要包括MCU选型、原理图PCB设计、软件开发入门及用户手册等)

大部分还未更新,先占个坑,日后更新…… 目前国外芯片涨价涨的离谱(就是在说ST),国内最好的替代就是兆易创新出的系列芯片了。但是官方资料繁杂,小白上手不易,因此边入门边记录,也给后来者根据官方资料可以有一个较为完整的学习体系。

Eddie的博客 1万+

二叉树遍历之后序遍历(非递归、递归)入门详解

二叉树后序遍历不如先序遍历简单,是相对最复杂的一种遍历方法。访问结点的次序是:“左—>右—>根”,也就是首先访问左子树,之后访问右子树,最后访问树根。对于左、右子树而言,其访问的次序依然是“左—>右—>根”。也就是说,对于每一棵子树,都是最后访问树根。...

sunnyoldboy博客 3万+

24届-Python面经(华为OD)

【摘要】24届非科班生通过栗栗姐指导成功转行华为OD。从4月4日零基础准备,经历机考(4/18)、测评(4/19)、资格面(5/7)、技术面(5/9-11)到5月17日获offer。全程栗栗姐提供题库、面经指导和实时跟进。虽遇压力面和技术短板,但在专业辅导下10天走完全流程。案例证明转行需自身努力+优质HR支持,栗栗姐团队提供多岗位(Java/Python等)、多地域(北上深等)OD机会,含笔试资料和全流程攻略。

2401_86909848的博客 1188

二叉树的遍历(非递归)

遍历二叉树, 是指按一定的规则和顺序访问二叉树的所有结点, 使每一个结点都被访问一次, 而且只被访问一次.也可以是NRL, 因为这两种遍历方法都是先访问根结点, 所以没有区别, 掌握其中一种, 另一种也随之掌握.BFS: 是指沿着二叉树的宽度优先遍历二叉树的结点, 即从上到下从左到右逐层遍历二叉树的结点.由于二叉树是非线性结构, 因此, 二叉树的遍历实质上是将二叉树的各个结点排列成一个线性序列.层序遍历就是根据二叉树的结构从上到下从左到右逐层遍历二叉树的结点.给你一棵二叉树的根节点。

m0_61433144的博客 9119

别再只盯着CXL协议了,聊聊CXL.mem里那个能动态调优内存性能的‘QoS遥测’到底怎么用

本文深入解析CXL.mem协议中的QoS遥测功能,探讨如何通过DevLoad指标动态调优内存性能。文章详细介绍了QoS遥测的核心机制、多QoS类别设备的实战配置,以及AI训练、金融交易和视频处理场景中的优化案例,帮助数据中心实现高效内存资源管理。

weixin_33725722的博客 437

IC芯片验证 - 手把手教你搭建UVM验证环境

这是一个UVM的demo项目

howard789的博客 3万+

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

递归写法,只要理解思想,几行代码。可是非递归写法却很不容易。这里特地总结下,透彻解析它们的非递归写法。其中,中序遍历的非递归写法最简单,后序遍历最难。我们的讨论基础是这样的: ? 1 2 3 4 5 6 7 //Binary Tree Node typedef struct node

YanRu的专栏 2554

深入解析MTK DRM初始化流程:从KMS模块到组件框架

本文深入解析了联发科(MTK)平台上DRM显示框架的初始化流程。从内核模块加载、Component框架管理依赖,到核心的mtk_drm_bind函数构建KMS对象(CRTC、Plane、Encoder、Connector),最终完成DRM设备注册并创建用户空间接口。文章结合调试经验,为开发者梳理了从驱动启动到屏幕点亮的完整路径与关键排查点。

onion的博客 769
上一篇: win10交换Caps和Ctrl
下一篇: 位运算
Findway_
博客等级 码龄10年 36粉丝 117原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值