二叉树的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++代码如下:

vector<int> inorderTraversal(TreeNode* root) {
TreeNode *cur=root;
vector<int> res;
while(cur) {
if(!cur->left) { //must first meet this node
res.push_back(cur->val);
cur=cur->right;
}
else {
TreeNode *pre=cur->left;
//find right most
while(pre->right && pre->right!=cur)
pre=pre->right;
if(pre->right) {
pre->right=NULL;
res.push_back(cur->val);
cur=cur->right;
}
else {
pre->right=cur;
cur=cur->left;
}
}
}
return res;
}

前序遍历

前序遍历和中序遍历是很相似的。前序遍历和中序遍历的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)
{node=node->right;goto s1}//删掉了put语句
s5.{put(node),rightmost's right child=node,node=node->left,goto s1;}//添加了put语句

具体代码如下:

vector<int> preorderTraversal(TreeNode* root) {
TreeNode *cur=root;
vector<int> res;
while(cur) {
if(!cur->left) { //must first meet this node
res.push_back(cur->val);
cur=cur->right;
}
else {
TreeNode *pre=cur->left;
while(pre->right && pre->right!=cur)
pre=pre->right;
if(pre->right) {
pre->right=NULL;
cur=cur->right;
}
else {
res.push_back(cur->val);
pre->right=cur;
cur=cur->left;
}
}
}
return res;
}

后序遍历

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

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

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

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

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

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

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

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

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

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

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

vector<int> postorderTraversal(TreeNode* root) {
vector<int> res;
TreeNode head(0),*cur=&head;
head.left=root;
while(cur) {
if(cur->left) {
TreeNode *pre=cur->left;
while(pre->right && pre->right!=cur)
pre=pre->right;
if(!pre->right) {
pre->right=cur;
cur=cur->left;
}
else {
reverse_node(cur->left,pre,res);
pre->right=NULL;
cur=cur->right;
}
}
else
cur=cur->right;
}
return res;
} void reverse(TreeNode *cur,TreeNode *pre) {
if(cur==pre)
return;
TreeNode *follow=cur->right,*last=cur;
do {
TreeNode *temp=follow;
follow=follow->right;
temp->right=last;
last=temp;
} while(last!=pre);
}
void reverse_node(TreeNode *cur,TreeNode *pre,vector<int> &res) {
reverse(cur,pre);
TreeNode *p=pre;
while(cur!=p) {
res.push_back(p->val);
p=p->right;
}
res.push_back(p->val);
reverse(pre,cur);
}

二叉树遍历之三(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. JVM性能优化读后笔记

    java性能优化权威指南读后笔记 三重境界 1.花似雾中看:对于遇到的额问题还看不清,不知道真真假假,是是非非. 2.悠然见南山:虽然刚开始对这个领域还不清楚,但随着时间推移,你对它有许多自己的见解, ...

  2. 使用idea的springboot项目出现org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)

    参考: https://www.cnblogs.com/lfm601508022/p/InvalidBoundStatement.html https://blog.csdn.net/xsggsx/a ...

  3. python读取数据库出txt报表

    python出报表使用到了数据库访问,文件读写,字符串切片处理.还可以扩展到电子邮件的发送,异常处理以及定时批任务. 总之在学习中发现还是有蛮多乐趣在其中. #coding=utf-8 ' impor ...

  4. Kafka 性能测试报告

    Producer command: kafka-producer-perf-test --topic _perf-test --num-records 10000000 --record-size 1 ...

  5. web自动化上传附件 2

    当我们进行某一项web自动化脚本编写时,有上传附件操作,点击附件直接打开了windows窗口,而有的点击添加附件打开一个小窗体,再点击‘浏览’才打开windows窗口, 中间多了这么一个小窗体的操作, ...

  6. UVa156

    #include <bits/stdc++.h> using namespace std; map<string,int> cnt; vector<string> ...

  7. OO课程第三次总结QWQ

    调研,然后总结介绍规格化设计的大致发展历史和为什么得到了人们的重视 emmm为这个问题翻遍百度谷歌知乎也没有得到答案,那我就把自己认为最重要的两点简要说明一下吧,欢迎大家补充~ 1.便于完成代码的重用 ...

  8. python学习心得--编码格式篇

    计算机容量单位: 1位 = 1bit: 8bit = 1byte = 1字节 : 1024bytes = 1kbytes =1KB: 1024KB = 1Million Bytes = 1MB = 1 ...

  9. 第二阶段第三次spring会议

    昨天我对便签加上了清空回收站功能 private void 清空回收站ToolStripMenuItem_Click(object sender, EventArgs e) { DelectText( ...

  10. pip使用国内镜像安装各种库

    1. 指定阿里云镜像, 安装requirements.txt中的所有 pip install -i http://mirrors.aliyun.com/pypi/simple/ --trusted-h ...