左神算法书籍《程序员代码面试指南》——3_05Morris遍历二叉树的神级方法【★★★★★】
【问题】介绍一种时间复杂度O(N),额外空间复杂度O(1)的二叉树的遍
历方式,N为二叉树的节点个数
无论是递归还是非递归,避免不了额外空间为O(h),h 为二叉树的高度
使用morris遍历,即利用空节点空间
morris遍历:
【思路:】
空间复杂度O(1)的要求很严格。常规的递归实现是显然不能满足要求的[其空间复杂度是树的深度O(h)]。本篇文章介绍著名的Morris遍历,该方法利用了二叉树结点中大量指向null的指针。
常规的栈结构遍历方式,遍历到某个节点之后并不能回到上层的结点,这是由二叉树本身的结构所限制的,每个结点并没有指向父节点的指针,因此需要使用栈来完成回到上层结点的步骤。
Morris遍历避免了使用栈结构,让下层有指向上层的指针,但并不是所有的下层结点都有指向上层的指针([这些指针也称为空闲指针])。
要使用O(1)空间进行遍历,最大的难点在于,遍历到子节点的时候怎样重新返回到父节点(假设节点中没有指向父节点的p指针),由于不能用栈作为辅助空间。为了解决这个问题,Morris方法用到了线索二叉树(threaded binary tree)的概念。在Morris方法中不需要为每个节点额外分配指针指向其前驱(predecessor)和后继节点(successor),只需要利用叶子节点中的左右空指针指向某种顺序遍历下的前驱节点或后继节点就可以了。
Morris只提供了中序遍历的方法,在中序遍历的基础上稍加修改可以实现前序,而后续就要再费点心思了。所以先从中序开始介绍。
一、中序遍历步骤:
1. 来到当前节点cur, 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点, 即cur = cur->right。
2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点, 即找到当前节点左子树上的最右节点,记为mostRight
a) 如果mostRight的右孩子为空,将它的右孩子设置指向为当前节点cur。当前节点更新为当前节点的左孩子, 即cur = cur->left。
b) 如果mostRight的右孩子为当前节点cur,将它的右孩子重新设为空(恢复树的形状)。输出当前节点。当前节点更新为当前节点的右孩子, 即cur = cur->right。
3. 重复以上1、2直到当前节点为空。
图示:
下图为每一步迭代的结果(从左至右,从上到下),cur代表当前节点,深色节点表示该节点已输出。
代码:
//中序遍历
void morrisIn(Node* head)
{
if(head==null)
return;
Node* cur1=head;
Node* cur2=null;
while(cur1 != null)
{
cur2=cur1.left;
if(cur2 != null)
{
while(cur2.right != null && cur2.right !=cur1)
cur2=cur2. right; //找到最右节点
if(cur2. right==null)
{
cur2. right=cur1; //辅助节点
cur1=cur1. left;
continue;
}
else
cur2. right=null;
}
cout<<cur1.value<<" ";
cur1=cur1. right;
}
cout<<endl;
}
复杂度分析:
空间复杂度:O(1),因为只用了两个辅助指针。
时间复杂度:O(n)。证明时间复杂度为O(n),最大的疑惑在于寻找中序遍历下二叉树中所有节点的前驱节点的时间复杂度是多少,即以下两行代码:
1 while (prev->right != NULL && prev->right != cur)
2 prev = prev->right;
直觉上,认为它的复杂度是O(nlgn),因为找单个节点的前驱节点与树的高度有关。但事实上,寻找所有节点的前驱节点只需要O(n)时间。n个节点的二叉树中一共有n - 1条边,整个过程中每条边最多只走2次,一次是为了定位到某个节点,另一次是为了寻找上面某个节点的前驱节点,如下图所示,其中红色是为了定位到某个节点,黑色线是为了找到前驱节点。所以复杂度为O(n)。
二、前序遍历
前序遍历与中序遍历相似,代码上只有一行不同,不同就在于输出的顺序。
步骤:
1. 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。
2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。
a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。输出当前节点(在这里输出,这是与中序遍历唯一一点不同)。当前节点更新为当前节点的左孩子。
b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。当前节点更新为当前节点的右孩子。
3. 重复以上1、2直到当前节点为空。
图示:
代码:
//前序遍历
void morrisIn(Node* head)
{
if(head==null)
return;
Node* cur1=head;
Node* cur2=null;
while(cur1 != null)
{
cur2=cur1.left;
if(cur2 != null)
{
while(cur2.right != null && cur2.right !=cur1)
cur2=cur2. right; //找到最右节点
if(cur2. right==null)
{
cur2. right=cur1; //辅助节点
cout << cur1.value << " ";//前序遍历是先打印
cur1=cur1. left;
continue;
}
else
cur2. right=null;
}
else
cout<<cur1.value<<" ";
cur1=cur1. right;
}
cout << endl;
}
复杂度分析:
时间复杂度与空间复杂度都与中序遍历时的情况相同。
三、后序遍历
后续遍历稍显复杂,需要建立一个临时节点dump,令其左孩子是root。并且还需要一个子过程,就是倒序输出某两个节点之间路径上的各个节点。
步骤:
当前节点设置为临时节点dump。
1. 如果当前节点的左孩子为空,则将其右孩子作为当前节点。
2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。
a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。
b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。倒序输出从当前节点的左孩子到该前驱节点这条路径上的所有节点。当前节点更新为当前节点的右孩子。
3. 重复以上1、2直到当前节点为空。
图示:
代码:
//后序遍历
Node* reverseEdge(Node* from)
{
Node* pre=null;
Node* next=null;
while(from != null)
{
next=from. right;
from.right=pre;
pre=from;
from=next;
}
return pre;
}
void printEdge(Node* head)
{
Node* tail=reverseEdge(head);
Node* cur=tail;
while(cur!=null)
{
cout << cur.value << " ";
cur=cur. right;
}
reverseEdge(tail);
}
void morrisPos(Node* head)
{
if(head==null)
return;
Node* cur1=head;
Node* cur2=null;
while(cur1 != null)
{
cur2=cur1.left;
if(cur2!=null)
{
while(cur2.right != null && cur2.right!= cur1)
cur2=cur2.right;//找到最右节点
if(cur2.right==null)
{
cur2.right=cur1;
cur1=cur1.left;
continue;
}
else
{
cur2.right=null;
printEdge(cur1.left);
}
}
cur1=cur1. right;
}
printEdge(head);
cout << endl;
}
复杂度分析:
空间复杂度同样是O(1);时间复杂度也是O(n),倒序输出过程只不过是加大了常数系数。
左神算法书籍《程序员代码面试指南》——3_05Morris遍历二叉树的神级方法【★★★★★】的更多相关文章
- 《程序员代码面试指南》第三章 二叉树问题 遍历二叉树的神级方法 morris
题目 遍历二叉树的神级方法 morris java代码 package com.lizhouwei.chapter3; /** * @Description:遍历二叉树的神级方法 morris * @ ...
- 程序员代码面试指南:IT名企算法与数据结构题目最优解
第1章栈和队列 1设计一个有getMin功能的栈(士★☆☆☆) 1由两个栈组成的队列(尉★★☆☆) 5如何仅用递归函数和栈操作逆序一个栈(尉★★☆☆) 8猫狗队列(士★☆☆☆)10用一个栈实现另一 ...
- 程序员代码面试指南 IT名企算法与数据结构题目最优解
原文链接 这是一本程序员面试宝典!书中对IT名企代码面试各类题目的最优解进行了总结,并提供了相关代码实现.针对当前程序员面试缺乏权威题目汇总这一痛点,本书选取将近200道真实出现过的经典代码面试题,帮 ...
- 左神算法书籍《程序员代码面试指南》——1_08构造数组的MaxTree
[题目] 将一个没有重复数字的数组中的数据构造一个二叉树 每个节点都是该子树的最大值 [要求] 时间复杂度为O(N)[题解] 使用单调栈,栈的顺序是维持从大到小排序 通过使用单调栈,将数组中中所有数的 ...
- 左神算法书籍《程序员代码面试指南》——2_11将单链表的每K个节点之间逆序
[题目]给定一个单链表的头节点head,实现一个调整单链表的函数,使得每K个节点之间逆序,如果最后不够K个节点一组,则不调整最后几个节点.例如:链表:1->2->3->4->5 ...
- 左神算法书籍《程序员代码面试指南》——1_01设计一个有getMin功能的栈
[题目] 实现一个特殊的栈,在实现栈的基本功能的基础上,再实现返回栈中最小元素的操作. [要求] 1.pop.push.getMin操作的时间复杂度都是O(1).2.设计的栈类型可以使用现成的栈结构. ...
- 左神算法书籍《程序员代码面试指南》——2_03删除链表的中间节点和a/b处的节点
[题目]给定链表的头节点head,实现删除链表的中间节点的函数.例如:不删除任何节点:1->2,删除节点1:1->2->3,删除节点2:1->2->3->4,删除节 ...
- 左神算法书籍《程序员代码面试指南》——2_02在单链表和双链表中删除倒数第k个字节
[题目]分别实现两个函数,一个可以删除单链表中倒数第K个节点,另一个可以删除双链表中倒数第K个节点.[要求]如果链表长度为N,时间复杂度达到O(N),额外空间复杂度达到O(1).[题解]从头遍历链表, ...
- 左神算法书籍《程序员代码面试指南》——1_10最大值减去最小值小于或等于num的子数组数量
[题目]给定数组arr和整数num,共返回有多少个子数组满足如下情况:max(arr[i.j]) - min(arr[i.j]) <= num max(arfi.j])表示子数组ar[ij]中的 ...
随机推荐
- 区别 |DCL |DDL |DML |DQL
DCL(Data Control Language)数据控制语言: 用来设置或更改数据库用户或角色权限的语句,包括(grant,deny,revoke等)语句.这个比较少用到. 对于大多数人,在公司一 ...
- JavaScript中的函数柯里化与反柯里化
一.柯里化定义 在计算机科学中,柯里化是把 接受多个参数的函数 变换成 接受一个单一参数(最初函数的第一个参数)的函数 并且返回 接受余下参数且返回结果的新函数的技术 高阶函数 高阶函数是实现柯里化的 ...
- 线性基算贡献——19牛客多校第一场H
/* 给定数组a[],求有多少集合的异或值为0,将这些集合的大小之和求出来 对于每个数来说,如果除去这个数后数组里做出的线性基和这个数线性相关,那么这个数贡献就是2^(n-1-线性基的大小) 反之这个 ...
- Python Django主机管理
1.新建一个django project项目 django-admin startproject DjangoWeb 2.新建app python manage.py startapp master ...
- Feign连接超时 Read timed out
在远程调用的过程中由于连接超时,导致无法成功请求数据,下面是报错 项目中用的是spring-cloud-starter-openfeign 2.2.0版本 找到对应的文档,开始查阅资料 文档首页就找到 ...
- ACM-ICPC 2018 沈阳赛区网络预赛-B,F,G
学长写的 F. Fantastic Graph "Oh, There is a bipartite graph.""Make it Fantastic." X ...
- IPv6 三个访问本地地址的小Tips
最近发现家里宽带支持IPv6了,这里分享三个利用IPv6访问本地地址(内网地址)的方法. 通常来说,我们用localhost来代表本地地址127.0.0.1.其实在IPv6中有他自己的表示方法ip6- ...
- Spring-Boot中如何使用多线程处理任务
看到这个标题,相信不少人会感到疑惑,回忆你们自己的场景会发现,在Spring的项目中很少有使用多线程处理任务的,没错,大多数时候我们都是使用Spring MVC开发的web项目,默认的Controll ...
- 关于jquery的一些插件
1.fullPage.js插件 fullPage.js 是一个基于 jQuery 的插件,它能够很方便.很轻松的制作出全屏网站.如今我们经常能见到全屏网站,在手机上也经常能看到一些活动页面.这些网站用 ...
- ubuntu自带截图工具
ubuntu自带的截图工具感觉能够满足基本的截图功能,可以不必安装另外的截图软件. 一般用到的截图类型有三种:全屏.当前活动窗口.自定义区域,其中自定义区域截图是最灵活也是我们用的最多的方式.在ubu ...