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

前言

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

[cpp] view plain copy
  1. //Binary Tree Node  
  2. typedef struct node  
  3. {  
  4.     int data;  
  5.     struct node* lchild;  //左孩子  
  6.     struct node* rchild;  //右孩子  
  7. }BTNode;  

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

中序遍历

分析

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

中序代码段(i)    

[cpp] view plain copy
  1. BTNode* p = root;  //p指向树根  
  2. stack<BTNode*> s;  //STL中的栈  
  3. //一直遍历到左子树最下边,边遍历边保存根节点到栈中  
  4. while (p)  
  5. {  
  6.     s.push(p);  
  7.     p = p->lchild;  
  8. }  

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


说明:

  1. 上图中只给出了必要的节点和边,其它的边和节点与讨论无关,不必画出。
  2. 你可能认为图a中最近保存节点算不得是根节点。如果你看过树、二叉树基础,使用扩充二叉树的概念,就可以解释。总之,不用纠结这个没有意义问题。
  3. 整个二叉树只有一个根节点的情况可以划到图a。
仔细想想,二叉树的左子树,最下边是不是上图两种情况?不管怎样,此时都要出栈,并访问该节点。这个节点就是中序序列的第一个节点。根据我们的思维,代码应该是这样:  
[cpp] view plain copy
  1. p = s.top();  
  2. s.pop();  
  3. cout << p->data;  

我们的思维接着走,两图情形不同得区别对待:
1.图a中访问的是一个左孩子,按中序遍历顺序,接下来应访问它的根节点。也就是图a中的另一个节点,高兴的是它已被保存在栈中。我们只需这样的代码和上一步一样的代码:
[cpp] view plain copy
  1. p = s.top();  
  2. s.pop();  
  3. cout << p->data;  
  
左孩子和根都访问完了,接着就是右孩子了,对吧。接下来只需一句代码:p=p->rchild;在右子树中,又会新一轮的代码段(i)、代码段(ii)……直到栈空且p空。

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

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

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

中序代码段(ii)   

[cpp] view plain copy
  1. p = s.top();  
  2. s.pop();  
  3. cout << p->data;  
  4. p = p->rchild;  

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

这时写出遍历循环体就不难了:    
[cpp] view plain copy
  1. BTNode* p = root;  
  2. stack<BTNode*> s;  
  3. while (!s.empty() || p)  
  4. {  
  5.     //代码段(i)一直遍历到左子树最下边,边遍历边保存根节点到栈中  
  6.     while (p)  
  7.     {  
  8.         s.push(p);  
  9.         p = p->lchild;  
  10.     }  
  11.     //代码段(ii)当p为空时,说明已经到达左子树最下边,这时需要出栈了  
  12.     if (!s.empty())  
  13.     {  
  14.         p = s.top();  
  15.         s.pop();  
  16.         cout << setw(4) << p->data;  
  17.         //进入右子树,开始新的一轮左子树遍历(这是递归的自我实现)  
  18.         p = p->rchild;  
  19.     }  
  20. }  

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

中序遍历代码一          

[cpp] view plain copy
  1. //中序遍历  
  2. void InOrderWithoutRecursion1(BTNode* root)  
  3. {  
  4.     //空树  
  5.     if (root == NULL)  
  6.         return;  
  7.     //树非空  
  8.     BTNode* p = root;  
  9.     stack<BTNode*> s;  
  10.     while (!s.empty() || p)  
  11.     {  
  12.         //一直遍历到左子树最下边,边遍历边保存根节点到栈中  
  13.         while (p)  
  14.         {  
  15.             s.push(p);  
  16.             p = p->lchild;  
  17.         }  
  18.         //当p为空时,说明已经到达左子树最下边,这时需要出栈了  
  19.         if (!s.empty())  
  20.         {  
  21.             p = s.top();  
  22.             s.pop();  
  23.             cout << setw(4) << p->data;  
  24.             //进入右子树,开始新的一轮左子树遍历(这是递归的自我实现)  
  25.             p = p->rchild;  
  26.         }  
  27.     }  
  28. }  

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

中序遍历代码二   

[cpp] view plain copy
  1. //中序遍历  
  2. void InOrderWithoutRecursion2(BTNode* root)  
  3. {  
  4.     //空树  
  5.     if (root == NULL)  
  6.         return;  
  7.     //树非空  
  8.     BTNode* p = root;  
  9.     stack<BTNode*> s;  
  10.     while (!s.empty() || p)  
  11.     {  
  12.         if (p)  
  13.         {  
  14.             s.push(p);  
  15.             p = p->lchild;  
  16.         }  
  17.         else  
  18.         {  
  19.             p = s.top();  
  20.             s.pop();  
  21.             cout << setw(4) << p->data;  
  22.             p = p->rchild;  
  23.         }  
  24.     }  
  25. }  

前序遍历

分析

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

前序代码段(i)

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

接下来就是:出栈,根据栈顶节点进入右子树。

前序代码段(ii)   

[cpp] view plain copy
  1. //当p为空时,说明根和左子树都遍历完了,该进入右子树了  
  2. if (!s.empty())  
  3. {  
  4.     p = s.top();  
  5.     s.pop();  
  6.     p = p->rchild;  
  7. }  

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

前序遍历代码一   

[cpp] view plain copy
  1. void PreOrderWithoutRecursion1(BTNode* root)  
  2. {  
  3.     if (root == NULL)  
  4.         return;  
  5.     BTNode* p = root;  
  6.     stack<BTNode*> s;  
  7.     while (!s.empty() || p)  
  8.     {  
  9.         //边遍历边打印,并存入栈中,以后需要借助这些根节点(不要怀疑这种说法哦)进入右子树  
  10.         while (p)  
  11.         {  
  12.             cout << setw(4) << p->data;  
  13.             s.push(p);  
  14.             p = p->lchild;  
  15.         }  
  16.         //当p为空时,说明根和左子树都遍历完了,该进入右子树了  
  17.         if (!s.empty())  
  18.         {  
  19.             p = s.top();  
  20.             s.pop();  
  21.             p = p->rchild;  
  22.         }  
  23.     }  
  24.     cout << endl;  
  25. }  

下面给出,本质是一样的另一段代码:

前序遍历代码二    

[cpp] view plain copy
  1. //前序遍历  
  2. void PreOrderWithoutRecursion2(BTNode* root)  
  3. {  
  4.     if (root == NULL)  
  5.         return;  
  6.     BTNode* p = root;  
  7.     stack<BTNode*> s;  
  8.     while (!s.empty() || p)  
  9.     {  
  10.         if (p)  
  11.         {  
  12.             cout << setw(4) << p->data;  
  13.             s.push(p);  
  14.             p = p->lchild;  
  15.         }  
  16.         else  
  17.         {  
  18.             p = s.top();  
  19.             s.pop();  
  20.             p = p->rchild;  
  21.         }  
  22.     }  
  23.     cout << endl;  
  24. }  

在二叉树中使用的是这样的写法,略有差别,本质上也是一样的:

前序遍历代码三 

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

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

后序遍历

分析

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

后序遍历代码一   

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

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

后序遍历代码二

[cpp] view plain copy
  1. //定义枚举类型:Tag  
  2. enum Tag{left,right};  
  3. //自定义新的类型,把二叉树节点和标记封装在一起  
  4. typedef struct  
  5. {  
  6.     BTNode* node;  
  7.     Tag tag;  
  8. }TagNode;      
  9. //后序遍历    
  10. void PostOrderWithoutRecursion2(BTNode* root)  
  11. {  
  12.     if (root == NULL)  
  13.         return;  
  14.     stack<TagNode> s;  
  15.     TagNode tagnode;  
  16.     BTNode* p = root;  
  17.     while (!s.empty() || p)  
  18.     {  
  19.         while (p)  
  20.         {  
  21.             tagnode.node = p;  
  22.             //该节点的左子树被访问过  
  23.             tagnode.tag = Tag::left;  
  24.             s.push(tagnode);  
  25.             p = p->lchild;  
  26.         }  
  27.         tagnode = s.top();  
  28.         s.pop();  
  29.         //左子树被访问过,则还需进入右子树  
  30.         if (tagnode.tag == Tag::left)  
  31.         {  
  32.             //置换标记  
  33.             tagnode.tag = Tag::right;  
  34.             //再次入栈  
  35.             s.push(tagnode);  
  36.             p = tagnode.node;  
  37.             //进入右子树  
  38.             p = p->rchild;  
  39.         }  
  40.         else//右子树已被访问过,则可访问当前节点  
  41.         {  
  42.             cout << setw(4) << (tagnode.node)->data;  
  43.             //置空,再次出栈(这一步是理解的难点)  
  44.             p = NULL;  
  45.         }  
  46.     }  
  47.     cout << endl;  
  48. }<span style="font-family: 'Courier New'; ">  </span>  

总结

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/463800.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深夜看代码2

昨天的文章晚上读内核代码有人评论说好像说了一些什么&#xff0c;好像又没有说什么&#xff0c;所以我到底是在说什么呢&#xff1f;因为今天已经把内核修改好了&#xff0c;自己也测试了&#xff0c;所以这次好好说下&#xff0c;我到底是说了什么&#xff0c;又做了什么。—…

F#学习之路(2) 深刻理解函数(上)

函数在函数式编程语言中是一等公民&#xff0c;是函数式语言中最重要的基本组成元素&#xff0c;也是其名称的由来。 F# 中的函数之如C#中的类&#xff0c;是组织程序结构的最基本单元。是命令式编程语言中函数或OO编程语言中方法的超集。超集&#xff0c;有多强大&#xff1f…

C++ 线程安全的单例模式

转载&#xff1a;https://www.cnblogs.com/ccdev/archive/2012/12/19/2825355.html 废话不多说&#xff0c;常用的代码积淀下来。 一、懒汉模式&#xff1a;即第一次调用该类实例的时候才产生一个新的该类实例&#xff0c;并在以后仅返回此实例。 需要用锁&#xff0c;来保证其…

写代码多就牛逼?

最近遇到了一些人、一些事&#xff0c;然后就想着拿出来总结总结&#xff0c;并谈谈自己的一些看法&#xff0c;所以就有了这篇文章。首先&#xff0c;我们来看看下面遇到过的两种情景。情景1&#xff1a;在工作中经常会遇到这样一些人&#xff1a;要他们实现一些功能&#xff…

推荐12款非常有用的流行 jQuery 插件

jQuery 是一个非常优秀的 JavaScript 框架&#xff0c;在现在的 Web 开发项目中扮演着重要角色。jQuery 使用简单灵活&#xff0c;同时还有许多成熟的插件可供选择&#xff0c;它可以帮助你在项目中加入一些非常好的效果&#xff0c;让网站有更好的可用性和用户体验。今天这篇文…

Linux以及各大发行版介绍

什么是Linux&#xff1f; 也许很多人会不屑的说&#xff0c;Linux不就是个操作系统么。错&#xff01;Linux不是一个操作系统&#xff0c;严格来讲&#xff0c;Linux只是一个操作系统中的内核。内核是什么&#xff1f;内核建立了计算机软件与硬件之间通讯的平台&#xff0c;内核…

有人LeetCode第一题都做不出来

有一个这样的江湖传闻时间是8点30&#xff0c;我不信这个邪把力扣的第一题写一次——题目给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。你可以假设每种输入只会对应一个…

内存池技术介绍

看到一篇关于内存池技术的介绍文章&#xff0c;受益匪浅&#xff0c;转贴至此。 原贴地址&#xff1a;http://www.ibm.com/developerworks/cn/linux/l-cn-ppp/index6.html 6.1 自定义内存池性能优化的原理 如前所述&#xff0c;读者已经了解到"堆"和"栈"的…

Linux 应用开发——完整版思维导图

转自我朋友的公众号「嵌入式Linux系统开发」&#xff0c;总结的内容对大家学习是非常有帮助的。目录

你觉得好的代码可能并不是最优的解决方案

晚上我看到了JeffXie 写了一篇关于内存屏障的文章&#xff0c;后面又看到Linus对一次内存屏障修改的建议&#xff0c;所以就有了这篇文章。https://mp.weixin.qq.com/s/H7Pw8xCKcNu41UGaYB648w在我看来&#xff0c;内存屏障谁为了让计算机做更加正确的事情&#xff0c;不希望计…

oh,我这个大佬盆友教我整机器学习

这个项目是我一个盆友的毕业设计&#xff0c;他的设计在这项目基础上新增了功能&#xff0c;晚上我们在这部分讨论了很久&#xff0c;在机器学习领域这个项目不算高深&#xff0c;但对于我们初学者&#xff0c;想了解机器学习是个什么鬼东西的我们来说帮助很大。https://github…

MailMail升级到1.0.2.4

修正一处会导致异常的逻辑错误 手动添加收件人地址时&#xff0c;如果地址已存在&#xff0c;将获得提示。 增加收件人地址导入功能&#xff0c;可以从一个或多个文件中导入收件人地址。 增加收件人列表导出功能 为避免干扰滚动条的使用&#xff0c;双击打开添加附件对话框的功…

导师问我打开句柄fd和没有打开的差异在哪里?

大家好昨晚看到一个同学在群里提问&#xff0c;想简单回答这个问题&#xff0c;我的答案可能不是最全面的&#xff0c;文章最后的两篇技术文大家可以看看&#xff0c;大家也可以说下自己的看法。fd的发明我觉得是计算机的一个壮举&#xff0c;因为对于应用程序来说&#xff0c;…

C++之Boost准标准库配置

下载安装 进入官网下载地址&#xff1a;https://www.boost.org/users/download/ 本教程直接下载官方已编译库&#xff0c;不涉及源代码手动编译 点击官方编号好的链接&#xff0c;然后进入一个下载地址&#xff1a;https://sourceforge.net/projects/boost/files/boost-binarie…

利用HTML中的XML数据岛记录浏览

html文件&#xff1a;shop.html <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns"http://www.w3.org/1999/xhtml"> <head> <me…

测试一下你对IP地址的掌握水平(网管面试时会用到)

以下内容摘自《网管员面试宝典》一书。测试一下你对IP地址的理解能力&#xff0c;大家先不看题后的解答&#xff0c;看自己能做出多少题。网管面试时会用到的。面试题1&#xff1a;以下说法正确的是&#xff08; &#xff09;。A. C类地址就是局域网用的IP地址 B. A类地址的网…

中秋的秋

又是一年中秋中秋是比较特别的节日&#xff0c;因为每一年的中秋&#xff0c;我和小云总是能遇到各种事情而分开「当然今年不会」。去年的时候&#xff0c;我们因为要赶项目&#xff0c;所以中秋申请了加班&#xff0c;要申请加班的那天我还是挺不情愿的&#xff0c;然后旁边的…

工作和异地,都是生活的考验

12年毕业的我&#xff0c;应该没有人比我更懂异地恋了。12年毕业拿了一份上海的ARM底层开发offer&#xff0c;薪资不算高&#xff0c;不过我们那一年竟没有一个拿到比上一届师兄薪资好的offer&#xff0c;我那时心里郁郁发闷&#xff0c;女朋友那时候考公务员&#xff0c;我们没…

用临时表的GridView分页

本例子采用sql2000下的Nowthwind数据库中的[Order Details]表 下面是存储过程脚本 Code1ALTER PROC OrderDetailsPaging 2(PageIndex int,--页码 3 PageSize int,--页尺寸 4 RowsCount int output)--总行数 5AS 6BEGIN 7set nocount on 8declare PageLowerBound int 9declar…

HTML与CSS(图解6):超链接

动态的超链接&#xff1a; <html> <head> <title>动态超链接</title> <style> <!-- body{background:url(bg9.gif); /* 页面背景图片 */margin:0px; padding:0px;cursor:pointer; /*意思就是鼠标指针变成 手 的形状&#xff0c;和放到链…