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

article/2025/9/1 18:15:48

前言

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

//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为空,此时无非两种情况:


说明:

  1. 上图中只给出了必要的节点和边,其它的边和节点与讨论无关,不必画出。
  2. 你可能认为图a中最近保存节点算不得是根节点。如果你看过树、二叉树基础,使用扩充二叉树的概念,就可以解释。总之,不用纠结这个没有意义问题。
  3. 整个二叉树只有一个根节点的情况可以划到图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;
}<span style="font-family: 'Courier New'; ">  </span>

总结

思维和代码之间总是有巨大的鸿沟。通常是思维正确,清楚,但却不易写出正确的代码。要想越过这鸿沟,只有多尝试、多借鉴,别无它法。
以下几点是理解上述代码的关键:
  1. 所有的节点都可看做是父节点(叶子节点可看做是两个孩子为空的父节点)。
  2. 把同一算法的代码对比着看。在差异中往往可看到算法的本质。
  3. 根据自己的理解,尝试修改代码。写出自己理解下的代码。写成了,那就是真的掌握了。

转载请注明出处,本文地址:http://blog.csdn.net/zhangxiangdavaid/article/details/37115355

专栏目录:

  • 数据结构与算法目录
  • c指针




http://chatgpt.dhexx.cn/article/MdgSFKj1.shtml

相关文章

二叉树的后序遍历

二叉树文章系列&#xff1a; 二叉树的前序遍历二叉树的中序遍历二叉树的后序遍历二叉树的层序遍历二叉树的前序、中序、后序、层序遍历【解法完整版】 本文目录 一、解题思路&#xff1a;递归二、解题思路&#xff1a;迭代&#xff08;方法1&#xff09;三、解题思路&#xff…

C语言完整代码实现:二叉树的先序遍历、中序遍历、后序遍历

一、先序遍历原理 先序遍历就是&#xff1a;根、左、右&#xff0c;也就是先遍历根结点再遍历左结点最后再遍历右结点&#xff0c;注意&#xff1a;如果遍历到的结点不是叶子结点的话需要对该结点进行拆分&#xff0c;比如这棵二叉树&#xff1a; 先遍历A&#xff0c;然后是B&a…

数据结构——二叉树的先序遍历

二叉树的遍历分为 先序遍历&#xff0c;中序遍历&#xff0c;后序遍历&#xff0c;层次遍历 四种遍历。 这节要分享的是先序遍历 如图所示&#xff0c;这是一个普通的二叉树。他的先序遍历是&#xff1a;A B D E H C F G I J 为什么呢&#xff1f; 先序遍历的遍历规则是&am…

二叉树三种遍历顺序

三.二叉树的三种遍历方式 1.先序遍历&#xff1a;按照根节点->左子树->右子树的顺序访问二叉树 先序遍历&#xff1a;&#xff08;1&#xff09;访问根节点&#xff1b;&#xff08;2&#xff09;采用先序递归遍历左子树&#xff1b;&#xff08;3&#xff09;采用先序…

二叉树(Binary Tree):先序遍历、中序遍历、后序遍历和层次遍历

二叉树&#xff08;Binary Tree&#xff09;&#xff1a;先序遍历、中序遍历、后序遍历和层次遍历 树 Tree二叉树 Binary Tree先序遍历 Preorder Traversal中序遍历 Inoreder Traversal后序遍历 Postorder Traversal层次遍历 Level Traversal 树 Tree 根 Root&#xff1a;树顶部…

oracle awr监控报告,一个Oracle小白的AWR报告分析(一)

背景&#xff1a;某个类似准实时的数据分析系统&#xff0c;每15分钟从其他6个数据库中抽取五百张增量数据表&#xff0c;并进行15分钟粒度统计&#xff0c;同时有个前端门户进行查询。 该数据分析系统由数据抽取服务器、应用服务器、数据库服务器组成&#xff0c;全部为虚拟机…

oracle生成awr报告命令,Oracle AWR报告生成方法

1、登录Oracle程序所在的服务器&#xff0c;查找出awrrpt.sql文件所在位置 D:\oracle\product\10.2.0\db_1\RDBMS\ADMIN\awrrpt.sql 2、登录Oracle&#xff0c;以sysdba身份连接 3、执行命令 D:\oracle\product\10.2.0\db_1\RDBMS\ADMIN\awrrpt.sql 4、输入report_type报告类型…

oracle打印awr报告,oracle导出AWR报告步骤

1、进入数据库 sqlplus / as sysdba ps:如果出现用户密码错误&#xff0c; 计算机管理 > 组 > ora_dba组里的用户登陆操作系统&#xff0c;就可以无需输入用户和口令&#xff0c;直接以sysdba的身份连上数据库。 2、查看用户 show parameter db_name 3、开始压测后执行 e…

Oracle导出AWR报告

一、使用root用户登录Linux服务器 二、切换至oracle用户 执行命令&#xff1a;su – oracle&#xff0c;然后回车 三、使用管理员权限连接数据库 执行命令&#xff1a;sqlplus / as sysdba&#xff0c;然后回车 四、生成报告快照 执行脚本&#xff1a;exec DBMS_WORKLOAD_RE…

如何分析AWR 报告

Automatic Workload Repository是10g引入的一个重要组件。在里面存贮着近期一段时间内&#xff0c;默认是7天&#xff0c;数据库活动状态的详细信息。 AWR报告是对AWR视图进行查询而得到的一份自动生成的报告。可以通过下面的脚本手工得到一份AWR报告。 exec dbms_w…

oracle 取awr报告,Oracle生成awr报告

Oracle生成awr报告 达芬奇的梦 2018-04-22 21:28:32 Oracle 一、手工生成awr报告的方法 1、相应权限用户登录(sysdba)后,在$ORACLE_HOME/rdbms/admin 2、在sqlplus里执行@?/rdbms/admin/awrrpt.sql,按照提示操作。 3、生成AWR报告说明 单实例:@$ORACLE_HOME/rdbms/admin/aw…

Oracle SQL调优系列之AWR报告简介

文章目录 一、AWE报告生成步骤1.1 工具选择1.2 自动创建快照1.3 手工创建快照1.4 生成AWR报告 二、AWR报告分析2.1 AWR之DB Time2.2 AWR之load_profile2.3 AWR之efficiency percentages2.4 AWR之top 10 events2.5 AWR之SQL Statistics 一、AWE报告生成步骤 对于SQL调优&#x…

AWR报告解读

0 初步结论 ① 数据库CPU资源不够&#xff0c;CPU使用率较高&#xff0c;造成CPU等待时间较长&#xff0c;可适当提升CPU资源&#xff1b; ② 数据库I/O资源消耗不太大&#xff0c;不存在IO瓶颈&#xff1b; ③ 可适当调大SGA空间&#xff08;增加10G左右&#xff09;&#xf…

用sql统计vintage,滚动率,迁移率,逾期率

获取代码请移步&#xff1a;用sql统计vintage&#xff0c;滚动率&#xff0c;迁移率&#xff0c;逾期率

如何用R语言做Vintage分析

一、背景 Vintage一词源自葡萄酒业&#xff0c;意思是葡萄酒酿造年份。因为每年的天气、温度、湿度、病虫害等情况不同&#xff0c;而这些因素都会对葡萄酒的品质产生很大的影响&#xff0c;所以人们对葡萄酒以葡萄当年的采摘年份进行标识来加以品质区分。现在Vintage分析被广泛…

风控中必做的数据分析

大数据领域就没有不做数据分析的&#xff0c;大数据风控也不例外。 我的观点是风控和其他互联网业务都是互通的&#xff0c;本文介绍下风控中必做的数据分析&#xff0c;用以说明数据分析是一通百通的。 工欲善其事&#xff0c;必先利其器。先说下数据分析的工具。 分析工具…

Vintage、滚动率、迁移率的应用

更多风控建模、大数据分析等内容请关注公众号《bigdatafengkong》 BY 小石头 一、Vintage Vintage源于葡萄酒酿造&#xff0c;葡萄酒的品质会因葡萄生长的年份不同、气候不同而不同。Vintage分析是指评估不同年份的葡萄酒的品质随着窖藏时间的推移而发生的变化&#xff0c;并且…

窗口函数:vintage报表

0 前言 Vintage这个词原意是指酿造葡萄酒的酒窖。葡萄酒是讲究年份&#xff0c;哪年光景好&#xff0c;哪年光景不好&#xff0c;直接会影响到葡萄酒的品质。后来借用到信贷资产行业&#xff0c;指的是每个月贷款的资产质量情况&#xff0c;要直接跟每个相同时间段内的余额做比…

信贷风控中Vintage、滚动率、迁移率的理解

风控业务背景 信贷风险管理是一门艺术&#xff0c;更是一门科学。资产质量分析中常会涉及到三个理论&#xff1a; 账龄分析&#xff08;Vintage Analysis&#xff09;&#xff1a;用以分析账户成熟期、变化规律等。滚动率分析&#xff08;Roll Rate Analysis&#xff09;&#…

风控ML[9] | Vintage和Roll Rate 分析的详解

我们说了好几期的风控建模了&#xff0c;也有不少的同学私信我说一般来说我们需要怎么确定Y值呢&#xff1f;&#xff0c;到底多坏的逾期表现的客户可以被我们定义为坏客户呢&#xff1f;今天这篇文章&#xff0c;就给大家介绍一个大家既熟悉又陌生的分析工具——Vintage Analy…