开心一刻

  一天,有个粉丝遇到感情方面的问题,找我出出主意

  粉丝:我女朋友吧,就是先天有点病,听不到人说话,也说不了话,现在我家里人又给我介绍了一个,我该怎么办

  我:这个问题很难去解释,我觉得一个人活着,他要对身边的人负责,对家人负责,对自己负责

  从语音中我能感受得到粉丝很难受,我继续补充

  我:我不是说让你放弃掉你的女朋友,你们一定是有一定的感情基础才在一起的,但你还是需要衡量衡量你的未来

  我能明显感觉到粉丝已经在抽泣,继续说道

  我:当然,这个时候离开肯定是不合适的,对吧?

  粉丝:是的

  我:这种感情的问题,我很难说让你怎么样,这个只有你自己去衡量,找到一个最合适的解决办法

  粉丝哭泣到:我真的不知道怎么办

  我最不忍心看别人哭,安慰道:你先别哭,问题总有办法解决的,哭不是解决问题的办法,你先平复下

  过了一会,粉丝说道:我知道了,我还是遵从家里的意见吧,给我现在的女朋友气放了

  我:女朋友气... 我放你个大乌龟

前情回顾

  二叉树的遍历 → 不用递归,还能遍历吗中讲到了二叉树的深度遍历的实现方式:递归、栈+迭代

  不管采用何种方式,额外空间复杂度都是 O(N)

  那有没有额外空间复杂度 O(1) 的遍历方式了?

  很早之前就被人给专研出来了,也就是本文的主角:Morris Traversal

Morris Traversal

  因为它由 Joseph Morris 发明的,所以叫 Morris Traversal

  递归、栈+迭代的遍历,本质都是使用了栈结构进行辅助,所以在处理完某个节点后能回到上层去

  二叉树的结构决定了它从上层到下层(根到叶子)很容易,但从下层到上层却很难,因为只有父节点指向子节点的指针,而没有子节点指向父节点的指针

  Morris 遍历的实质就是避免使用栈结构,而是让下层到上层有指针,通过底层节点指向 null 的空闲指针指向上层的某个节点,从而实现下层到上层的移动

  空闲指针从哪来?二叉树的叶子节点,或者只有单个孩子的节点(左指针空闲或右指针空闲)

  具体实现,我们往下看

  移动规则

  也就是遍历过程;设当前节点为 cur ,初始 cur = root ,则 cur 的移动规则如下

  1、如果 cur 没有左子树,则让 cur 向右移动,即 cur = cur.right

  2、如果 cur 有左子树,则找到 cur 左子树最右的节点,记作 mostRight

    2.1、如果 mostRight 的右指针指向 null ,让其指向 cur ,然后 cur 向左移动

      

    2.2、如果 mostRight 的右指针指向 cur ,让其指向 null ,然后 cur 向右移动

      

  3、当 cur 为 null 时,遍历停止

  这描述还是有点抽象,我们结合具体的二叉树,利用移动规则把二叉树遍历一遍

  初始二叉树如下

  1)初始 cur 在节点 a,此时 cur 有左子树,找到其左子树的最右节点,即节点 k,k 的右指针指向 null ,让其指向 cur ,然后 cur 左移

    此时二叉树结构如下, cur 第一次来到节点 b

  2)此时 cur 在节点 b, cur 有左子树,找到其左子树的最右节点,即节点 d,d 的右指针指向 null ,让其指向 cur ,然后 cur 左移

    此时二叉树结构如下, cur 第一次来到节点 d

  3)此时 cur 在节点 d,cur 没有左子树, cur 右移

    此时二叉树结构如下, cur 第二次来到节点 b

  4)此时 cur 在节点 b, cur 有左子树,找到其左子树的最右节点,即节点 d,d 的右指针指向 cur ,让其指向 null ,然后 cur 右移

    此时二叉树结构如下, cur 第一次来到节点 e

    这里大家可能会有疑问:找  cur 的左子树的最右节点时,找到的不应该是节点 c 吗?

    所以这里有细节要处理,找左子树最右节点的时候,遇到两种情况(右指针指向 null 或右指针指向 cur )都需要停止寻找,用代码描述就是:

  5)此时 cur 在节点 e, cur 有左子树,找到其左子树的最右节点,即节点 h,h 的右指针指向 null ,让其指向 cur ,然后 cur 左移

    此时二叉树结构如下, cur 第一次来到节点 h

  6)此时 cur 在节点 h, cur 没有左子树, cur 右移

    此时二叉树结构如下, cur 第二次来到节点 e

  7)此时 cur 在节点 e, cur 有左子树,找到其左子树的最右节点,即节点 h,h 的右指针指向 cur ,让其指向 null ,然后 cur 右移

    此时二叉树结构如下, cur 第一次来到节点 k

  8)此时 cur 在节点 k, cur 没有左子树, cur 右移

    此时二叉树结构如下, cur 第二次来到节点 a(为什么是第二次?因为最初从 a 开始的)

  9)此时 cur 在节点 a, cur 有左子树,找到其左子树的最右节点,即节点 k,k 的右指针指向 cur ,让其指向 null ,然后 cur 右移

    此时二叉树结构如下, cur 第一次来到节点 c

  10)此时 cur 在节点 c, cur 有左子树,找到其左子树的最右节点,即节点 g,g 的右指针指向 null ,让其指向 cur ,然后 cur 左移

    此时二叉树结构如下, cur 第一次来到节点 f

  11)此时 cur 在节点 f, cur 没有左子树, cur 右移

    此时二叉树结构如下, cur 第一次来到节点 g

  12)此时 cur 在节点 g, cur 没有左子树, cur 右移

    此时二叉树结构如下, cur 第二次来到节点 c

  13)此时 cur 在节点 c, cur 有左子树,找到其左子树的最右节点,即节点 g,g 的右指针指向 cur ,让其指向 null , cur 右移

    此时二叉树结构如下, cur = null

  14)此时 cur 为 null ,遍历停止

    可以看到,二叉树回到了最初的状态,最终结构与最初一致

  前面步骤有点长,看的可能不够直观,我们来看个完整版的

  上述的遍历就是 Morris Traversal , cur 所经历的节点 a -> b -> d -> b -> e -> h -> e -> k -> a -> c -> f -> g -> c 组成了 Morris 序

  在遍历的过程中,相信大家已经得出一个规律:有左子树的节点(b、e、a、c)会到达两次,没有左子树的节点(d、h、k、f、g)则只会到达一次

  这绝对不是巧合啊!这是 Morris Traversal 移动规所产生的必然结果

  对于那些能达到两次的节点,我们如何区分是第一次到达,还是第二次到达?

  在上述的遍历过程中,相信大家已经找到答案了

    1、如果其左子树的最右节点指向 null ,即 mostRight.right = null ,则该节点是第一次到达

    2、如果其左子树的最右节点指向自身,即 mostRight.right = cur ,则该节点是第二次到达

  经过了上述诸多的准备, Morris Traversal 代码实现就非常简单了

  代码实现

  相信大家都能看懂这个代码,没看懂的再去把前面的遍历过程再看看

   Morris Traversal 一定要看懂,不然后面的深度遍历就玩不动了

先序遍历

  我们对比下 先序序列 和 Morris 序列

  发现了什么? Morris Traversal 第二次到达的节点不打印,就是 先序序列 了

  代码也就手到擒来了

中序遍历

  我们对比下 中序序列 和 Morris 序列

  只会遍历一次的节点,直接打印;会遍历两次的节点,第一次的时候不打印,第二次打印,就得到了 中序序列

  代码很容易撸出来了

后序遍历

  对比 后序序列 和 Morris 序列

  一眼看不出有什么关系

  通过 Morris Traversal 得到 后续序列 确实不容易想到,我们直接看前辈们的经验

  被遍历到两次的节点的先后顺序:b、e、a、c

  1、b 节点的左子树的右边界:d,逆序打印它还是 d

  2、e 节点的左子树的右边界:h,逆序打印它还是 h

  3、a 节点的左子树的右边界:b -> e -> k,逆序打印就是:k -> e -> b

  4、c 节点的左子树的右边界:f -> g,逆序打印就是:g -> f

  5、整棵树的右边界:a -> c,逆序打印就是:c -> a

  把逆序列串起来:d -> h -> k -> e -> b -> g -> f -> c -> a,这就是 后序序列

  问题又来了,如何逆序打印右边界,并且额外空间复杂度  O(1) ;其实就是单向链表的逆序输出,不知道的可以查看:单向链表的花式玩法 → 还在玩反转?

  我们来看代码

总结

  额外空间复杂度

  只用到了有限几个变量, Morris Traversal 额外空间复杂度 O(1)

  时间复杂度

   Morris Traversal 时间复杂度是不是 O(N) ?

  我们先看个极端的案例

  它的时间复杂度是 2 * O(N),这个没什么问题吧?

  常数项可以拿掉,所以时间复杂度是 O(N)

  注意点

   Morris Traversal 遍历过程中会改变二叉树的结构,在一些并发的场景需要慎重使用

参考

  《程序员代码面试指南:IT 名企算法与数据结构题目最优解》

  Morris遍历图解

额外空间复杂度O(1) 的二叉树遍历 → Morris Traversal,你造吗?的更多相关文章

  1. 二叉树遍历 Morris

    二叉树的遍历,先根遍历,不适用递归,存储空间为 O(1) 转自:http://chuansongme.com/n/100461 MorrisInOrder(): while 没有结束 如果当前节点没有 ...

  2. 二叉树中序遍历,先序遍历,后序遍历(递归栈,非递归栈,Morris Traversal)

    例题 中序遍历94. Binary Tree Inorder Traversal 先序遍历144. Binary Tree Preorder Traversal 后序遍历145. Binary Tre ...

  3. 二叉树遍历之三(Moriis traversal)

     二叉树的Morris traversal是个很值得学习的算法,也是此系列重点想要记叙的一个算法.Morris traversal的一个亮点在于它是O(1)空间复杂度的.前面的递归和迭代都是需要O(n ...

  4. 二叉树的遍历——Morris

    在之前的博客中,博主讨论过二叉树的经典遍历算法,包括递归和常规非递归算法,其时间复杂度和空间复杂度均为O(n).Morris算法巧妙地利用了二叉树的线索化思路,将二叉树的遍历算法的空间复杂度降低为O( ...

  5. [转载]Morris Traversal方法遍历二叉树(非递归,不用栈,O(1)空间)

    本文主要解决一个问题,如何实现二叉树的前中后序遍历,有两个要求: 1. O(1)空间复杂度,即只能使用常数空间: 2. 二叉树的形状不能被破坏(中间过程允许改变其形状). 通常,实现二叉树的前序(pr ...

  6. 二叉树遍历,递归,栈,Morris

    一篇质量非常高的关于二叉树遍历的帖子,转帖自http://noalgo.info/832.html 二叉树遍历(递归.非递归.Morris遍历) 2015年01月06日 |  分类:数据结构 |  标 ...

  7. Morris Traversal方法遍历二叉树(非递归,不用栈,O(1)空间)——无非是在传统遍历过程中修改叶子结点加入后继结点信息(传统是stack记录),然后再删除恢复

    先看看线索二叉树 n个结点的二叉链表中含有n+1(2n-(n-1)=n+1)个空指针域.利用二叉链表中的空指针域,存放指向结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索 ...

  8. 面试中很值得聊的二叉树遍历方法——Morris遍历

    Morri遍历 通过利用空闲指针的方式,来节省空间.时间复杂度O(N),额外空间复杂度O(1).普通的非递归和递归方法的额外空间和树的高度有关,递归的过程涉及到系统压栈,非递归需要自己申请栈空间,都具 ...

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

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

随机推荐

  1. 一道栈溢出babystack

    我太天真了,师傅说让我做做这个平台的题,我就注册了个号,信心满满的打开了change,找到了pwn,一看第一道题是babystack,我想着,嗯,十分钟搞定他!直到我下载了题目,题目给了libc,然后 ...

  2. 预算(Project)

    <Project2016 企业项目管理实践>张会斌 董方好 编著 预算是件重要的事,不然银几一花没边了,那结果可是要牺牺的(以下省略具体描述9^323字) 在Project里做预算,步骤不 ...

  3. 添加备注信息(Project)

    <Project2016 企业项目管理实践>张会斌 董方好 编著 就在任务信息的[高级]选项卡隔壁,还有一个[备注]选项卡,可别拿备注不当回事,因为任务名称的字数不能太多. 好吧,张同学也 ...

  4. LuoguP7257 [COCI2009-2010#3] FILIP 题解

    Content 有两个十进制三位数 \(a,b\),请输出这两个数翻转之后的较大数. 数据范围:\(100\leqslant a,b\leqslant 999\),\(a,b\) 中不包含 \(0\) ...

  5. java 输入输出IO流 IO异常处理try(IO流定义){IO流使用}catch(异常){处理异常}finally{死了都要干}

    IO异常处理 之前我们写代码的时候都是直接抛出异常,但是我们试想一下,如果我们打开了一个流,在关闭之前程序抛出了异常,那我们还怎么关闭呢?这个时候我们就要用到异常处理了. try-with-resou ...

  6. MySQL 定时器

    mysql定时器是系统给提供了event,而oracle里面的定时器是系统给提供的job.废话少说,下面创建表:create table mytable (id int auto_increment ...

  7. 在react项目中实现表格导出为Excel

    需求背景 数据表格有时需要增加导出Excel功能,大多数情况下都是后端出下载接口,前端去调用. 对于数据量少的数据,可以通过前端技术实现,减少后端工作. 实现方式 使用插件--xlsx 根据自己项目情 ...

  8. nanogui之更新子模块glfw3.3.2踩坑总结

    nanogui源码下载: A . https://github.com/wjakob/nanogui B . https://github.com/dalerank/nanogui B是fork的A, ...

  9. clang编译代码报错:`_start': (.text+0x24): undefined reference to `main'

    1. 说明 使用clang++10.1编译报错: /usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/crt1 ...

  10. 【LeetCode】998. Maximum Binary Tree II 解题报告(C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 递归 日期 题目地址:https://leetcod ...