二叉树遍历之三(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层有叶子结点,并 ...
随机推荐
- python脚本处理下载的b站学习视频
作为常年在b站学习的我,一直以来看到有兴趣的视频,从来都是点赞收藏下载三连,但是苦于我那小钢炮iphone se屏幕大小有限,看起视频实在费劲,决定一定要找个下载电脑上下载b站视频的方法,以前用过硕鼠 ...
- (5)Linux权限管理
1.文件权限 2.1)文件类型 d:目录 -:文件 l:链接文件 b:可以存储的接口设备 c:串行端口设备(键盘,鼠标) 2)文件属性 接下来的九个字符以三个为一组分别是 rw ...
- HTML5学习路线导航
一.基本标签元素 1.基础标签第一篇 2.基础标签第二篇 3.表单form的使用 4.新增表单验证 二.CSS样式表 4.CSS插入样式表的三种格式 5.六大选择器 6.样式内容详细讲解 7.背景渐进 ...
- SqlServer2012,设置指定数据库对指定用户开放权限
REVOKE VIEW ANY DATABASE TO [public] --这个是取消数据库公开的权限,也就是除了sa角色外任何人都不能查看数据库 -- 现在用sa用户登录Use [要开放权限的数据 ...
- 分享一个可以把 iOS/Android 应用的下载链接合成一个二维码的工具
芝麻二维码官网:https://www.hotapp.cn 1.在iOS系统设备扫描时 如果是微信扫描,因为第一步里使用了中间页面,此时无法直接跳转到App Store了,所以需要给出提示页面,提示用 ...
- java之路 数据类型-常量
class Demo1{ public static void main(String[] args){ //数据类型 类名 = 初始值 int age = 10; int age1 = 20; Sy ...
- js中的原型对象链
由于原型对象也是一个对象,它也有自己的原型对象并继承对象中的属性,这就是原型对象链:对象继承其原型对象,而原型对象继承它的原型对象,以此类推. 我们创建的每一个函数都有一个prototype(原型)属 ...
- JAVA解决前端跨域问题。
什么是跨域? 通俗来说,跨域按照我自己的想法来理解,是不同的域名之间的访问,就是跨域.不同浏览器,在对js文件进行解析是不同的,浏览器会默认阻止,所以 现在我来说下用java代码解决前端跨域问题. 用 ...
- java的OSGi确实是个坑
sun已经把java的OSGi这个坑填得够深了,sun估计短时间想把这个坑调回来是不可能了,跟.net比包管理模块化开发确实java够烂的. java的模块化架构开发只能让OSGi回去睡觉,自定义模块 ...
- Python Day 7
阅读目录 内容回顾: 数据类型相互转换: 字符编码: ##内容回顾 #1.深浅拷贝 ls = [1, 'a', [10]] 值拷贝:直接赋值 ls1 = ls, ls中的任何值发生改变,ls1中的值都 ...