内容:

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遍历的更多相关文章

  1. 算法进阶面试题03——构造数组的MaxTree、最大子矩阵的大小、2017京东环形烽火台问题、介绍Morris遍历并实现前序/中序/后序

    接着第二课的内容和带点第三课的内容. (回顾)准备一个栈,从大到小排列,具体参考上一课.... 构造数组的MaxTree [题目] 定义二叉树如下: public class Node{ public ...

  2. 【数据结构与算法】二叉树的 Morris 遍历(前序、中序、后序)

    前置说明 不了解二叉树非递归遍历的可以看我之前的文章[数据结构与算法]二叉树模板及例题 Morris 遍历 概述 Morris 遍历是一种遍历二叉树的方式,并且时间复杂度O(N),额外空间复杂度O(1 ...

  3. JS的十大经典算法排序

    引子 有句话怎么说来着: 雷锋推倒雷峰塔,Java implements JavaScript. 当年,想凭借抱Java大腿火一把而不惜把自己名字给改了的JavaScript(原名LiveScript ...

  4. JAVA经典算法40题及解答

    JAVA经典算法40题 [程序1]   题目:古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第四个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少? 1.程序分 ...

  5. (转)白话经典算法系列之八 MoreWindows白话经典算法之七大排序总结篇

    在我的博客对冒泡排序,直接插入排序,直接选择排序,希尔排序,归并排序,快速排序和堆排序这七种常用的排序方法进行了详细的讲解,并做成了电子书以供大家下载.下载地址为:http://download.cs ...

  6. Java经典算法四十例编程详解+程序实例

    JAVA经典算法40例 [程序1]   题目:古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第四个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少?   1.程 ...

  7. 二叉树的Morris遍历

    二叉树的遍历,除了上篇文章中的传统递归和使用的栈结构的非递归方式,还有如下这种Morris遍历方式,该算法的构思非常巧妙:利用前驱空闲的rightChild指针指向当前节点,形成一个环.时间复杂度和前 ...

  8. 经典算法题每日演练——第十一题 Bitmap算法

    原文:经典算法题每日演练--第十一题 Bitmap算法 在所有具有性能优化的数据结构中,我想大家使用最多的就是hash表,是的,在具有定位查找上具有O(1)的常量时间,多么的简洁优美, 但是在特定的场 ...

  9. 经典算法题每日演练——第六题 协同推荐SlopeOne 算法

    原文:经典算法题每日演练--第六题 协同推荐SlopeOne 算法 相信大家对如下的Category都很熟悉,很多网站都有类似如下的功能,“商品推荐”,"猜你喜欢“,在实体店中我们有导购来为 ...

随机推荐

  1. Linux Foundation(笔记)

    /************************************************************* * Linux Foundation * 1. 总结一下Linux的基础内 ...

  2. Ubuntu终端及VI 快捷键

    Ubuntu终端 快捷键 功能 Tab 自动补全 Ctrl+a 光标移动到开始位置 Ctrl+e 光标移动到最末尾 Ctrl+k 删除此处至末尾的所有内容 Ctrl+u 删除此处至开始的所有内容 Ct ...

  3. IIS 使用 HTTP/2

    什么叫HTTP/2? HTTP 2.0即超文本传输协议 2.0,是下一代HTTP协议.是由互联网工程任务组(IETF)的Hypertext Transfer Protocol Bis (httpbis ...

  4. 大家一起做训练 第一场 A Next Test

    题目来源:CodeForce #27 A 题目的意思简而言之就是要你输出一个没有出现过的最小的正整数. 题意如此简单明了,做法也很明了. 直接读入所有的数,然后排个序,设置个变量从1开始,出现过+1, ...

  5. 网络流--最大流dinic模板

    标准的大白书式模板,除了变量名并不一样……在主函数中只需要用到 init 函数.add 函数以及 mf 函数 #include<stdio.h> //差不多要加这么些头文件 #includ ...

  6. 龙儿经理嘴上经常说的B树

    国内的数据结构教材一般是按照Knuth定义,即“阶”定义为一个节点的子节点数目的最大值. 对于一棵m阶B-tree,每个结点至多可以拥有m个子结点.各结点的关键字和可以拥有的子结点数都有限制 规定m阶 ...

  7. hasura graphql 集成pipelinedb测试

    实际上因为pipelinedb 是原生支持pg的,所以应该不存在太大的问题,以下为测试 使用doker-compose 运行 配置 docker-compose 文件 version: '3.6' s ...

  8. oracle 、sql server 、mysql 复制表数据

    我们知道在oracle 中复制表数据的方式是使用 create table table_name as select * from table_name 而在sql server  中是不能这么使用的 ...

  9. JVM(下)

    持久代:不会被 gc 给轻易回收的,创建后一直存在,持久代在堆内存里面,但是不归 java 程序使用.持久代是 动态 load 的那些 class,局部变量,去 gc 其实也 gc 不了啥 1.8 之 ...

  10. 数独求解程序 php版

    数独求解程序 php版 <?php class Sudoku { var $matrix; function __construct($arr = null) { if ($arr == nul ...