经典算法 Morris遍历
内容:
1、什么是morris遍历
2、morris遍历规则与过程
3、先序及中序
4、后序
5、morris遍历时间复杂度分析
1、什么是morris遍历
关于二叉树先序、中序、后序遍历的递归和非递归版本,在这里有详细代码:https://www.cnblogs.com/wyb666/p/10176980.html
明显这6种遍历算法的时间复杂度都需要 O(H) (H 为树高)的额外空间复杂度
另外因为二叉树遍历过程中只能向下查找孩子节点而无法回溯父结点,因此这些算法借助栈来保存要回溯的父节点
并且栈要保证至少能容纳下 H 个元素(比如遍历到叶子结点时回溯父节点,要保证其所有父节点在栈中)
而morris遍历则能做到时间复杂度仍为 O(N) 的情况下额外空间复杂度只需 O(1) 。
2、morris遍历规则与过程
首先在介绍morris遍历之前,我们先把先序、中序、后序定义的规则抛之脑后,
比如先序遍历在拿到一棵树之后先 遍历头结点然后是左子树后是右子树,并且在遍历过程中对于子树的遍历仍是这样。
在忘掉这些遍历规则之后,我们来看一下morris遍历定义的标准:
(1)定义一个遍历指针 cur ,该指针首先指向头结点
(2)判断 cur 的左子树是否存在
如果 cur 的左孩子为空,说明 cur 的左子树不存在,那么 cur右移(cur=cur.right)
如果 cur 的左孩子为cur,说明 cur 的左子树存在,找出该左子树上最右节点记为 mostRight
如果mostRight 的右孩子为空,那就让其指向 cur ( mostRight.right=cur ),并左移 cur ( cur=cur.left )
如果mostRight 的右孩子不为空,那么让 cur 右移( cur=cur.right ),并将 mostRight 的右孩子置空
(3)经过步骤2之后,如果 cur 不为空,那么继续对 cur 进行步骤2,否则遍历结束
下图所示举例演示morris遍历的整个过程:

代码1:
public static void morrisProcess(Node head){
// morris遍历的过程 第一种写法
if(head == null){
return;
}
Node cur = head;
Node mostRight = null;
while(cur!=null){
mostRight = cur.left;
if(mostRight != null){
while(mostRight.right!= null && mostRight.right!=cur){
mostRight = mostRight.right;
}
if(mostRight.right==null){
mostRight.right = cur;
cur = cur.left;
continue;
} else{
mostRight.right = null;
}
}
cur = cur.right;
}
}
代码2:
public static void morrisProcess2(Node head) {
// morris遍历的过程 第二种写法
if (head == null) {
return;
}
Node cur = head;
Node mostRight = null;
while (cur != null) {
mostRight = cur.left;
if (mostRight == null) {
cur = cur.right;
} else {
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
if (mostRight.right == null) {
mostRight.right = cur;
cur = cur.left;
} else {
mostRight.right = null;
cur = cur.right;
}
}
}
}
3、先序及中序
遍历完成后对 cur 进过的节点序列稍作处理就很容易得到该二叉树的先序、中序序列:

morris遍历会来到一个左孩子不为空的结点两次,而其它结点只会经过一次
因此使用 morris遍历打印先序序列时:
- 如果来到的结点无左孩子,那么直接打印(只会经过一次)
- 如果来到的结点的左子树的右结点的右孩子为空才打印(第一次来到该结点时)
而使用morris遍历打印中序序列时:
- 如果来到的结点无左孩子,那么直接打印 (只会经过一次)
- 如果来到的结点的左子树的右结点不为空时才打印(第二次来到该结点时)
- 上述两种情况实际上可以总结成一种情况:在cur右移时打印
遍历代码如下:
public static void morrisPre(Node head) {
// morris先序遍历 =》第一次来到节点就打印
if (head == null) {
return;
}
Node cur = head;
while (cur != null) {
if (cur.left == null) {
System.out.print(cur.value + " ");
cur = cur.right;
} else {
Node mostRight = cur.left;
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
if (mostRight.right == null) {
System.out.print(cur.value + " ");
mostRight.right = cur;
cur = cur.left;
} else {
mostRight.right = null;
cur = cur.right;
}
}
}
System.out.println();
}
public static void morrisIn(Node head) {
// morris中序遍历 =》放在cur右移的位置打印
if (head == null) {
return;
}
Node cur = head;
while (cur != null) {
if (cur.left == null) {
System.out.print(cur.value + " ");
cur = cur.right;
} else {
Node mostRight = cur.left;
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
if (mostRight.right == null) {
mostRight.right = cur;
cur = cur.left;
} else {
System.out.print(cur.value + " ");
mostRight.right = null;
cur = cur.right;
}
}
}
System.out.println();
}
4、后序
使用morris遍历得到二叉树的后序序列就没那么容易了,因为对于树种的非叶结点,
morris遍历都会经过它两 次,而我们后序遍历实在是在第三次来到该结点时打印该结点的。
因此要想得到后序序列,仅仅改变在morris遍历时打印结点的时机是无法做到的。
morris实现后序遍历:如果在每次遇到第二次经过的结点时,将该结点的左子树的右边界上的结点
从下到上打印,最后再将整颗树的右边界从下到上打印,终就是这个数的后序序列:


其中无非就是在morris遍历中在第二次经过的结点的时机执行一下打印操作。
而从下到上打印一棵树的右边界,可以将该右边界上的结点看做以 right 指针为后继指针的链表,
然后将其反转reverse,然后打印,最后恢复成原始结构即可
代码如下:
public static void morrisPos(Node head) {
// morris后序遍历
if (head == null) {
return;
}
Node cur = head;
while (cur != null) {
if (cur.left == null) {
cur = cur.right;
} else {
Node mostRight = cur.left;
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
if (mostRight.right == null) {
mostRight.right = cur;
cur = cur.left;
} else {
mostRight.right = null;
// 在这打印左子树的右边界
printRightEdge(cur.left);
cur = cur.right;
}
}
}
// 在这打印整颗树的右边界
printRightEdge(head);
System.out.println();
}
// 打印节点下左子树的右边界
private static void printRightEdge(Node root) {
if (root == null) {
return;
}
// reverse the right edge
Node cur = root;
Node pre = null;
while (cur != null) {
Node next = cur.right;
cur.right = pre;
pre = cur;
cur = next;
}
// print
cur = pre;
while (cur != null) {
System.out.print(cur.value + " ");
cur = cur.right;
}
// recover
cur = pre;
pre = null;
while(cur!=null){
Node next = cur.right;
cur.right = pre;
pre = cur;
cur = next;
}
}
5、morris遍历时间复杂度分析
因为morris遍历中,只有左孩子非空的结点才会经过两次而其它结点只会经过一次,也就是说遍历的次数小于 2N
因此使用morris遍历得到先序、中序序列的时间复杂度自然也是 O(N) ;
但产生后序序列的时间复杂度还要 算上 printRightEdge 的时间复杂度,但是你会发现整个遍历的过程中,所有的
printRightEdge 加起来也只是 遍历并打印了 N 个结点,因此时间复杂度仍然为 O(N)
总结:
morris遍历结点的顺序不是先序、中序、后序,而是按照自己的一套标准来决定接下来要遍历哪个结点
morris遍历的独特之处就是充分利用了叶子结点的无效引用(引用指向的是空,但该引用变量仍然占内存),
从而实现了O(N)的时间复杂度和O(1)的空间复杂度
经典算法 Morris遍历的更多相关文章
- 算法进阶面试题03——构造数组的MaxTree、最大子矩阵的大小、2017京东环形烽火台问题、介绍Morris遍历并实现前序/中序/后序
接着第二课的内容和带点第三课的内容. (回顾)准备一个栈,从大到小排列,具体参考上一课.... 构造数组的MaxTree [题目] 定义二叉树如下: public class Node{ public ...
- 【数据结构与算法】二叉树的 Morris 遍历(前序、中序、后序)
前置说明 不了解二叉树非递归遍历的可以看我之前的文章[数据结构与算法]二叉树模板及例题 Morris 遍历 概述 Morris 遍历是一种遍历二叉树的方式,并且时间复杂度O(N),额外空间复杂度O(1 ...
- JS的十大经典算法排序
引子 有句话怎么说来着: 雷锋推倒雷峰塔,Java implements JavaScript. 当年,想凭借抱Java大腿火一把而不惜把自己名字给改了的JavaScript(原名LiveScript ...
- JAVA经典算法40题及解答
JAVA经典算法40题 [程序1] 题目:古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第四个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少? 1.程序分 ...
- (转)白话经典算法系列之八 MoreWindows白话经典算法之七大排序总结篇
在我的博客对冒泡排序,直接插入排序,直接选择排序,希尔排序,归并排序,快速排序和堆排序这七种常用的排序方法进行了详细的讲解,并做成了电子书以供大家下载.下载地址为:http://download.cs ...
- Java经典算法四十例编程详解+程序实例
JAVA经典算法40例 [程序1] 题目:古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第四个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少? 1.程 ...
- 二叉树的Morris遍历
二叉树的遍历,除了上篇文章中的传统递归和使用的栈结构的非递归方式,还有如下这种Morris遍历方式,该算法的构思非常巧妙:利用前驱空闲的rightChild指针指向当前节点,形成一个环.时间复杂度和前 ...
- 经典算法题每日演练——第十一题 Bitmap算法
原文:经典算法题每日演练--第十一题 Bitmap算法 在所有具有性能优化的数据结构中,我想大家使用最多的就是hash表,是的,在具有定位查找上具有O(1)的常量时间,多么的简洁优美, 但是在特定的场 ...
- 经典算法题每日演练——第六题 协同推荐SlopeOne 算法
原文:经典算法题每日演练--第六题 协同推荐SlopeOne 算法 相信大家对如下的Category都很熟悉,很多网站都有类似如下的功能,“商品推荐”,"猜你喜欢“,在实体店中我们有导购来为 ...
随机推荐
- 更适合程序员使用的Vim配置 显示行号 语法高亮 智能缩进
在终端下使用vim进行编辑时,默认情况下,编辑的界面上是没有显示行号.语法高亮度显示.智能缩进等功能的.为了更好的在vim下进行工作,需要手动设置一个配置文件:.vimrc.在启动vim时,当前用户根 ...
- ElasticSearch(八):springboot集成ElasticSearch集群并使用
1. 集群的搭建 见:ElasticSearch(七) 2. springboot配置集群 2.1 创建springboot项目,使用idea创建,不过多介绍(创建项目时候建议不要勾选elastics ...
- CentOS安装crontab 定时备份文件夹
一. 编写脚本编写一个脚本文件,使脚本可以执行备份命令. 例如,将文件目录 /home/backups/balalala 备份到/home目录下,并压缩.1. 创建脚本命令格式: touch 路径/文 ...
- HTML第二课——css
请关注公众号:自动化测试实战 先给大家提个建议,就是用sublime编辑器来编写.用其他的也无所谓,我只是建议,因为这个会帮你自动补全很多代码. css概念 css叫层叠样式表.意思就是一层一层的叠加 ...
- ZOJ3545 Rescue the Rabbit
分析 未知定长串中不同已知模板串的出现次数问题,一般做法是AC自动机上dp. 考虑背包,\(dp(i,j,k)\)表示当前串长为\(i\),在AC自动机上对应节点\(j\),已匹配的模板串的状态为\( ...
- MySQL--禁用账号和设置账号有效期
======================================================================= MySQL5.5/5.6版本 在MySQL 5.7 版本 ...
- day31 python学习 并发编程之多进程理论部分
一 什么是进程 进程:正在进行的一个过程或者说一个任务.而负责执行任务则是cpu. 举例(单核+多道,实现多个进程的并发执行): 二 进程与程序的区别 程序仅仅只是一堆代码而已,而进程指的是程序的运行 ...
- 转oracle 学习 - 表空间
Oracle 数据库的表空间和 Oracle 数据库数据文件 关于 Oracle 数据库的表空间. 很多 Oracle 初学者弄不明白表空间的概念和意义,他们只知道给数据库建表的时候需要到表空间这个东 ...
- 使用rewrite 让php 实现类似asp.net 的IHttpModule 进行带参数js文件的参数获取
asp.net 的IHttpModule 接口具有很大的作用,我们可以使用实现的模块进行全局的控制,但是在学习php 的过程中也想实现类似的功能,查找php 的文档,自己没有找到, 但是我们大家应该知 ...
- BLE 4.1 和 BLE 4.2
BLE 4.2 比 BLE4.1 多了一些新的特性. Low-power IP (IPv6/6LoWPAN) Bluetooth Smart Internet Gateways (GATT) http ...