经典算法 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;
}
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都很熟悉,很多网站都有类似如下的功能,“商品推荐”,"猜你喜欢“,在实体店中我们有导购来为 ...
随机推荐
- c++跨文件变量声明
常量是内部链接的,可以直接定义; 变量是外部链接,如果在头文件定义的话,如果出现多次引用同一个头文件的编译单元,就会引发multi redifine错误, 这个时候就要只是声明变量来解决:exter ...
- Mongodb $setOnInsert操作符 和upsert:true
upsert:true:如果要更新的文档不存在的话会插入一条新的记录 $setOnInsert操作符会将指定的值赋值给指定的字段,如果要更新的文档存在那么$setOnInsert操作符不做任何处理: ...
- HUD 1969:Pie(二分)
Pie Time Limit: 5000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others) Total Submis ...
- solr学习一(一大堆的学习资料)
这篇博客含有海量资料,学习solr必备字典 大概看完solr就基本上手了. solr原味资料:http://wiki.apache.org/solr/FrontPage ( http://wi ...
- adnanh webhook 框架request values 说明
request values 在adnanh webhook 是比较重要的,规则触发以及命令参数传递都是通过它 支持的request values 类似 http header 查询参数 play ...
- ASP.NET MVC中如何以ajax的方式在View和Action中传递数据
前言:写这篇随笔的时候,在url上漏写了斜线,找了好久错误,整个人都很不好.#我是猪系列 背景:之前介绍过一篇如何构建 MVC&AJax&JSon示例,这一篇单独讲解如何在View和A ...
- FastAdmin selectPage 前端传递查询条件
★夕狱-东莞 2018/2/2 16:19:33 selectpage 怎么在前端传递查询条件,看了下源码,好像有个custom,怎么用来的,比如我要下拉的时候,只显示id=1的数据 Karson-深 ...
- 按的第一个greasemonkey插件:评论时可以粘贴啦~~
原来的样子:如果按ctrl+V会跳出错误
- PHP解析xml文件时报错:I/O warning : failed to load external entity
在代码顶部增加 libxml_disable_entity_loader(false); libxml_disable_entity_loader()作用是设置是否禁止从外部加载XML实体,设为tru ...
- python 异常类型----后期需理解调整
1.Python内建异常体系结构The class hierarchy for built-in exceptions is: BaseException +-- SystemExit +-- Key ...