二叉树的Morris traversal是个很值得学习的算法,也是此系列重点想要记叙的一个算法。Morris traversal的一个亮点在于它是O(1)空间复杂度的。前面的递归和迭代都是需要O(n)空间复杂度的。那么这个O(1)空间复杂度怎样做到?这个借鉴了些线索二叉树的相关原理,Morris traversal通过在遍历时修改叶子节点的右孩子指针达到了回退到根节点的目的。Morris其实在原始论文中只给出了中序遍历的相关代码,但是依据这个思路,通过修改相关步骤,便可得到前序和后序的Morris traversal实现。以下将以前序,中序,后序的Morris traversal步骤来分别细述Morris traversal的具体实现。

中序遍历

 在中序遍历中,对于一个节点,我们都是处理它的左子树,再处理它,再处理它的右子树。一个节点的前驱节点就是它的左子树的最右节点(这里先假定所有节点都是有左孩子的,下段同)。

对于一个节点np,定义它的前驱节点为pre。显然,在遍历完np的左子树后还需要回到np。并且,依据中序遍历的定义,任意节点的前驱节点的右孩子必然是为空的。那么,这个前驱节点的右孩子指针便可以为之所用。在此可以让前驱节点的右孩子指针指向当前节点。这样,在遍历完左子树后便可以据这个指针再次返回到节点np.

那么当通过右孩子指针进入一个节点时,怎么区分它究竟是通过自己的父节点访问到的,还是通过自己的左子树最右节点(即前驱节点)访问到的呢?这个就很简单了,直接找到它的左子树最右节点,若通过这样的查找,再次访问到了该节点。不言而喻,该节点是通过自己的左子树最右节点访问到的。那么,就只需要输出它,再依据同样的步骤处理它的右指针即可。

前面假定了所有节点都是有左孩子的。那么对于没有左孩子的节点呢?这个更简单了!那它一定是被第一次访问到,这时只需要输出它并且,依据前面步骤处理它的右孩子节点即可。

这样,依据以上分析,很容易就可以写出Morris traversal中序遍历的具体步骤(下以伪码表示):

```
s1.if(!node) return;
s2.if(!node->left)
{put(node),node=node->right;goto s1;}
s3. findlrightmost(node->left)
s4. if(rightmost‘s right child==node)
{put(node),node=node->right;goto s1}
s5. {rightmost's right child=node,node=node->left,goto s1;}
```

依据以上分析,可以很轻松将如上伪码转换为c++代码如下:

  1. vector<int> inorderTraversal(TreeNode* root) {
  2. TreeNode *cur=root;
  3. vector<int> res;
  4. while(cur) {
  5. if(!cur->left) { //must first meet this node
  6. res.push_back(cur->val);
  7. cur=cur->right;
  8. }
  9. else {
  10. TreeNode *pre=cur->left;
  11. //find right most
  12. while(pre->right && pre->right!=cur)
  13. pre=pre->right;
  14. if(pre->right) {
  15. pre->right=NULL;
  16. res.push_back(cur->val);
  17. cur=cur->right;
  18. }
  19. else {
  20. pre->right=cur;
  21. cur=cur->left;
  22. }
  23. }
  24. }
  25. return res;
  26. }

前序遍历

前序遍历和中序遍历是很相似的。前序遍历和中序遍历的Morris traversal唯一不同在于,前序在遇到一个节点时,若它是第一次遇到,则输出,否则则不输出。容易看出,改一下输出语句的位置即可将其变成前序遍历。

  1. s1.if(!node) return;
  2. s2.if(!node->left)
  3. {put(node),node=node->right;goto s1;}
  4. s3. findlrightmost(node->left)
  5. s4. if(rightmosts right child==node)
  6. {node=node->right;goto s1}//删掉了put语句
  7. s5.{put(node),rightmost's right child=node,node=node->left,goto s1;}//添加了put语句

具体代码如下:

  1. vector<int> preorderTraversal(TreeNode* root) {
  2. TreeNode *cur=root;
  3. vector<int> res;
  4. while(cur) {
  5. if(!cur->left) { //must first meet this node
  6. res.push_back(cur->val);
  7. cur=cur->right;
  8. }
  9. else {
  10. TreeNode *pre=cur->left;
  11. while(pre->right && pre->right!=cur)
  12. pre=pre->right;
  13. if(pre->right) {
  14. pre->right=NULL;
  15. cur=cur->right;
  16. }
  17. else {
  18. res.push_back(cur->val);
  19. pre->right=cur;
  20. cur=cur->left;
  21. }
  22. }
  23. }
  24. return res;
  25. }

后序遍历

相比而言,后序遍历的思路就显得要复杂一些,也要多加一些操作。

依据前面的步骤,可以看到,不论是中序遍历还是前序遍历,对于中间节点的操作都是"用完即丟"的。即,当程序遍历完一个节点的左子树后,该节点的地址就会被我们丢弃不管。我们只需专心再处理它的右子树即可。然而,这在后序遍历中很明显是行不通的。后序遍历中,我们遍历完右子树后,才输出该中间节点。这样,怎样从右子树回到中间节点处就成了一个十分棘手的问题。而且,不像中序遍历,在后序遍历中,节点的输出是层层往上跳的。一个节点的前驱节点,为其自己的左孩子节点或右孩子。

这样,用其他地方来保存中间节点地址似乎也都不现实。所以,我们还是考虑继续使用leftchild's rightmost的right child指针来保存中间节点。

仔细看后序遍历的数字输出规律,层层往上跳这个规律似乎对我们很有利。很容易发现,在右孩子输出时,其输出顺序近似一层直线,我们输出该右孩子节点,接着又输出了该右孩子的父节点,再输出其父节点。那么,我们可以试着在碰到对于右孩子节点本身统统先不做输出。对于右孩子节点,我们可以在输出所有左孩子之后,从该子树的根节点开始逆序输出其到右孩子的路径即可。

而由于我们处理完后返回到的是中间节点,那么我们可以看做当返回中间节点后,其左孩子的所有左孩子节点都已处理完毕。接着,我们处理其左孩子节点及左子树的所有右孩子节点即可。如对于以下的树,当返回1时,我们据右孩子指针逆序输出5,2即可。

但这样,又引入了一个新的问题,root并不为任何节点的左孩子节点。解决办法很简单,我们引入一个节点,构造一棵左孩子为root的单边树,然后以该节点为root,从该节点开始处理。

这样我们就可以将所有节点都转换为这种情况,因为每个节点都必为某个节点的左子树的右孩子。这样,我们在每次访问一个节点时,若是第一次访问它,我们就去处理它的左孩子,如果是返回到了它,那么我们就该去处理它的左孩子的所有节点再接着去处理它的右孩子。若左孩子为空,那我们直接去处理它的右孩子即可。

依据以上分析,我们可以写出如下执行步骤(伪码表示)

  1. s0.先构造以root为左孩子的单变树,以该数根为root
  2. s1.if(!node) return;
  3. s2.if(!node->left)
  4. {node=node->right;goto s1}
  5. s3.findrightmost(node->left)
  6. s4. if(rightmost==node)
  7. {reverse_put(node->left,rightmost);node=node->right;goto s1}
  8. s5.{rightmost=node;node=node->left;goto s1}

至于逆序输出,我们可以把根节点到叶子节点的单路径先调转一遍,然后输出。在输出后,再调转回来。

将以上思路写作代码如下:

  1. vector<int> postorderTraversal(TreeNode* root) {
  2. vector<int> res;
  3. TreeNode head(0),*cur=&head;
  4. head.left=root;
  5. while(cur) {
  6. if(cur->left) {
  7. TreeNode *pre=cur->left;
  8. while(pre->right && pre->right!=cur)
  9. pre=pre->right;
  10. if(!pre->right) {
  11. pre->right=cur;
  12. cur=cur->left;
  13. }
  14. else {
  15. reverse_node(cur->left,pre,res);
  16. pre->right=NULL;
  17. cur=cur->right;
  18. }
  19. }
  20. else
  21. cur=cur->right;
  22. }
  23. return res;
  24. }
  25. void reverse(TreeNode *cur,TreeNode *pre) {
  26. if(cur==pre)
  27. return;
  28. TreeNode *follow=cur->right,*last=cur;
  29. do {
  30. TreeNode *temp=follow;
  31. follow=follow->right;
  32. temp->right=last;
  33. last=temp;
  34. } while(last!=pre);
  35. }
  36. void reverse_node(TreeNode *cur,TreeNode *pre,vector<int> &res) {
  37. reverse(cur,pre);
  38. TreeNode *p=pre;
  39. while(cur!=p) {
  40. res.push_back(p->val);
  41. p=p->right;
  42. }
  43. res.push_back(p->val);
  44. reverse(pre,cur);
  45. }

二叉树遍历之三(Moriis traversal)的更多相关文章

  1. 额外空间复杂度O(1) 的二叉树遍历 → Morris Traversal,你造吗?

    开心一刻 一天,有个粉丝遇到感情方面的问题,找我出出主意 粉丝:我女朋友吧,就是先天有点病,听不到人说话,也说不了话,现在我家里人又给我介绍了一个,我该怎么办 我:这个问题很难去解释,我觉得一个人活着 ...

  2. poj2255 (二叉树遍历)

    poj2255 二叉树遍历 Time Limit:3000MS     Memory Limit:0KB     64bit IO Format:%lld & %llu   Descripti ...

  3. D - 二叉树遍历(推荐)

    二叉树遍历问题 Description   Tree Recovery Little Valentine liked playing with binary trees very much. Her ...

  4. 二叉树遍历(非递归版)——python

    二叉树的遍历分为广度优先遍历和深度优先遍历 广度优先遍历(breadth first traversal):又称层次遍历,从树的根节点(root)开始,从上到下从从左到右遍历整个树的节点. 深度优先遍 ...

  5. C++ 二叉树遍历实现

    原文:http://blog.csdn.net/nuaazdh/article/details/7032226 //二叉树遍历 //作者:nuaazdh //时间:2011年12月1日 #includ ...

  6. python实现二叉树遍历算法

    说起二叉树的遍历,大学里讲的是递归算法,大多数人首先想到也是递归算法.但作为一个有理想有追求的程序员.也应该学学非递归算法实现二叉树遍历.二叉树的非递归算法需要用到辅助栈,算法着实巧妙,令人脑洞大开. ...

  7. 【二叉树遍历模版】前序遍历&&中序遍历&&后序遍历&&层次遍历&&Root->Right->Left遍历

    [二叉树遍历模版]前序遍历     1.递归实现 test.cpp: 12345678910111213141516171819202122232425262728293031323334353637 ...

  8. hdu 4605 线段树与二叉树遍历

    思路: 首先将所有的查询有一个vector保存起来.我们从1号点开始dfs这颗二叉树,用线段树记录到当前节点时,走左节点的有多少比要查询该节点的X值小的,有多少大的, 同样要记录走右节点的有多少比X小 ...

  9. 二叉树遍历 C#

    二叉树遍历 C# 什么是二叉树 二叉树是每个节点最多有两个子树的树结构 (1)完全二叉树——若设二叉树的高度为h,除第 h 层外,其它各层 (1-h-1) 的结点数都达到最大个数,第h层有叶子结点,并 ...

随机推荐

  1. linux下mycat自启动方法

    每次开机都要启动mycat,网上看了好多都是用shell脚本来实现mycat开机自启动,后来看到一种方法,直接修改系统文件来实现,已经实践过,方法有效. 1.修改脚本文件rc.local:vim /e ...

  2. PIL库学习及运用

    了解PIL以及安装. 个方面的功能: (1) 图像归档:对图像进行批处理.生产图像预览.图像格式转换等. (2) 图像处理:图像基本处理.像素处理.颜色处理等. 安装PIL在cmd中输入 pip in ...

  3. 显式Intent 和隐式 Intent 的区别

    显式 Intent : 在知道目标组件名称的前提下,去调用Intent.setComponent().Intent.setClassName()或Intent.setClass()方法或者在new I ...

  4. Day3 -4.9!受到毕设的突然袭击,一脸蒙蔽,学习暂时停止,明晚继续

    PS:啊啊啊啊,慌张的不行,不详的预感终于爆发了,第二次毕设评图好突然,没办法了,竹径和学习突然搁置,明晚健身/建模/补更Day3,感到崩溃 ————————————————————————————— ...

  5. Django 的认识,面试题

    Django 的认识,面试题 1. 对Django的认识? #1.Django是走大而全的方向,它最出名的是其全自动化的管理后台:只需要使用起ORM,做简单的对象定义,它就能自动生成数据库结构.以及全 ...

  6. python 代码求阶乘

    递归实现 1: #递归实现 def factorial(n): if n == 0: return 1 else: return n * factorial(n - 1)# 递归实现 递归实现 2: ...

  7. Qt4.8.6开发WinCE 5.0环境搭建

    Qt-Wince5.0开发环境介绍 1.Windows7SP1 64 2.vs2008,以及sp1补丁 3.编译qt-everywhere-opensource-src-4.8.6.zip 4.qt- ...

  8. tcp拥塞控制 tahoe reno new reno sack

    http://www.docin.com/p-812428366.html http://www.docin.com/p-812428366.html

  9. 探索未知种族之osg类生物---渲染遍历之Renderer::draw()简介

    我们今天进入上一节的遗留问题Renderer::draw()的探究. 1.从_drawQueue中取出其中一个sceneView对象.SceneView是对scene和view类的封装,通过他可以方便 ...

  10. Isight Linux 2016hf2 安装步骤

    把License文件整个拷进去,都给执行权限 把ABAQUS.lic 里的 this_host 改为psn004 27011 改为26011 (区别于2017hf2) 杀掉2017的server ./ ...