二叉树遍历之三(Moriis traversal)
二叉树的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)的更多相关文章
- 额外空间复杂度O(1) 的二叉树遍历 → Morris Traversal,你造吗?
开心一刻 一天,有个粉丝遇到感情方面的问题,找我出出主意 粉丝:我女朋友吧,就是先天有点病,听不到人说话,也说不了话,现在我家里人又给我介绍了一个,我该怎么办 我:这个问题很难去解释,我觉得一个人活着 ...
- poj2255 (二叉树遍历)
poj2255 二叉树遍历 Time Limit:3000MS Memory Limit:0KB 64bit IO Format:%lld & %llu Descripti ...
- D - 二叉树遍历(推荐)
二叉树遍历问题 Description Tree Recovery Little Valentine liked playing with binary trees very much. Her ...
- 二叉树遍历(非递归版)——python
二叉树的遍历分为广度优先遍历和深度优先遍历 广度优先遍历(breadth first traversal):又称层次遍历,从树的根节点(root)开始,从上到下从从左到右遍历整个树的节点. 深度优先遍 ...
- C++ 二叉树遍历实现
原文:http://blog.csdn.net/nuaazdh/article/details/7032226 //二叉树遍历 //作者:nuaazdh //时间:2011年12月1日 #includ ...
- python实现二叉树遍历算法
说起二叉树的遍历,大学里讲的是递归算法,大多数人首先想到也是递归算法.但作为一个有理想有追求的程序员.也应该学学非递归算法实现二叉树遍历.二叉树的非递归算法需要用到辅助栈,算法着实巧妙,令人脑洞大开. ...
- 【二叉树遍历模版】前序遍历&&中序遍历&&后序遍历&&层次遍历&&Root->Right->Left遍历
[二叉树遍历模版]前序遍历 1.递归实现 test.cpp: 12345678910111213141516171819202122232425262728293031323334353637 ...
- hdu 4605 线段树与二叉树遍历
思路: 首先将所有的查询有一个vector保存起来.我们从1号点开始dfs这颗二叉树,用线段树记录到当前节点时,走左节点的有多少比要查询该节点的X值小的,有多少大的, 同样要记录走右节点的有多少比X小 ...
- 二叉树遍历 C#
二叉树遍历 C# 什么是二叉树 二叉树是每个节点最多有两个子树的树结构 (1)完全二叉树——若设二叉树的高度为h,除第 h 层外,其它各层 (1-h-1) 的结点数都达到最大个数,第h层有叶子结点,并 ...
随机推荐
- bind和on的区别
bind方法与on方法都是事件绑定,但是两者却又有着一个大区别:事件委托 jquery文档中bind和on函数绑定事件的用法: .bind(events [,eventData], handler) ...
- Mac编译RocketMQ 4.1.0
参考:https://my.oschina.net/jayronwang/blog/861396 1. 前提先安装并设置好maven,jdk,git,这个网上有很多教程,就不讲了 2. 下载rocke ...
- yum 安装时遇到“UnicodeDecodeError: 'ascii' codec”的问题
今天新安装了一个6.9系统,配置好本地yum源后,用yum安装时报了以上的错误信息,在/etc/yum.repos.d/目录下多出了TTT的一个目录 (手动问号),在百度上查了一些文档. 解决方法:1 ...
- View操作 swift
//创建View let view1 =UIView() let view2 =UIView(frame: CGRectMake(,, ,)) let view3 =UIView(frame: CGR ...
- oracle12C安装步骤
首先去官网下载两个架包链接如下:官网链接 第一步:将两个架包解压到同一个database目录下.如截图所示: 第二步:打开setup应用程序 打开后就到了下面这个页面 第三步:配置安全更新 环境变量配 ...
- 用c语言基本实现wc.exe功能
网址:https://github.com/3216005214/wc.exe wc项目要求 wc.exe 是一个常见的工具,它能统计文本文件的字符数.单词数和行数.这个项目要求写一个命令行程序,模仿 ...
- python 数据可视化 -- 生成可控的随机数据集合
生成可控的随机数据集合 使用 numpy.random 模块 numpy.random.random(size=None) 返回 [0.0, 1.0) 区间的随机 floats, 默认返回一个 fl ...
- MongoDB学习记录(二) - MongoDB的"增查改删"操作之"增"
如果做插入文档操作的集合不存在,那么集合将被创建 db.collection.insertOne() insertOne为collection插入一条文档,如果文档的_id字段未指定,MongoDB会 ...
- powershell脚本找修改的文件
$sourcedir="D:\workspace" $targetdir="E:\newf" $lastdate="2017-05-19" ...
- C#算法
递归 任何一个方法既可以调用其他方法又可以调用自己,而当这个方法调用自己时,我们就叫它递归函数或者递归方法! 通常递归有两个特点: 1.递归方法一直会调用自己直到某些条件满足,也就是说一定要有出口; ...