左神算法:遍历二叉树的神级方法(Morris遍历 / 线索二叉树)

2023-11-06 09:59

本文主要是介绍左神算法:遍历二叉树的神级方法(Morris遍历 / 线索二叉树),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本题来自左神《程序员代码面试指南》“遍历二叉树的神级方法”题目。

题目

给定一棵二叉树的头节点 head,完成二叉树的先序、中序和后序遍历。如果二叉树的节点数为N,则要求时间复杂度为O(N),额外空间复杂度为O(1)

题解

之前的题目已经剖析过如何用递归和非递归的方法实现遍历二叉树,但是很不幸,之前所有的方法虽然常用,但都无法做到额外空间复杂度为O(1)。这是因为遍历二叉树的递归方法实际使用了函数栈,非递归的方法使用了申请的栈,两者的额外空间都与树的高度相关,所以空间复杂度为O(h),h为二叉树的高度。

如果完全不用栈结构,能完成三种遍历吗?答案是可以。方法是使用二叉树节点中大量指向null 的指针,本题实际上就是大名鼎鼎的Morris 遍历,由Joseph Morris 于1979年发明。

首先来看普通的递归和非递归解法,其实都使用了栈结构,在处理完二叉树某个节点后可以回到上层去。为什么从下层回到上层会如此之难?因为二叉树的结构如此,每个节点都有指向孩子节点的指针,所以从上层到下层容易,但是没有指向父节点的指针,所以从下层到上层需要用栈结构辅助完成。

Morris 遍历的实质就是避免用栈结构,而是让下层到上层有指针,具体是通过让底层节点指向null 的空闲指针指回上层的某个节点,从而完成下层到上层的移动。我们知道,二叉树上的很多节点都有大量的空闲指针,比如,某些节点没有右孩子节点,那么这个节点的right 指针就指向null,我们称为空闲状态,Morris 遍历正是利用了这些空闲指针。

我们先不管先序、中序、后序的概念,先看看 Morris 遍历的过程

假设当前节点为cur,初始时cur 就是整棵树的头节点,根据以下标准让cur 移动

  1. 如果cur 为null,则过程停止,否则继续下面的过程。
  2. 如果cur 没有左子树,则让cur 向右移动,即令cur = cur.right。
  3. 如果cur 有左子树,则找到cur 左子树上最右的节点,记为mostRight。
    1. 如果mostRight 的right 指针指向 null,则令mostRight.right = cur,也就是让mostRight
      的right 指针指向当前节点,然后让cur 向左移动,即令cur = cur.left。
    2. 如果mostRight 的right 指针指向 cur,则令mostRight.right = null,也就是让mostRight
      的 right 指针指向 null(目的是让二叉树恢复原状),然后让cur 向右移动,即令cur = cur.right。

举个例子:假设一棵二叉树如图 3-9 所示
在这里插入图片描述

cur 依次到达的节点为:4、2、1、2、3、4、6、5、6、7,我们将这个序列叫Morris 序。

可以看出,在一棵二叉树中,对于有左子树的节点都可以到达两次,对于没有左子树的节点都只会到达一次。

对于任何一个能够到达两次的节点Y,是 如何知道此时的 cur 是第一次来到 Y 还是第二次来到 Y 呢如果Y 的左子树上的最右节点的指针(mostRight.right)是指向 null的,那么此时 cur 就是第一次到达Y;如果 mostRight.right 是指向 Y 的,那么此时cur 就是第二次到达 Y。这就是 Morris 遍历和 Morris 序的实质。

请读者先理解Morris 遍历和Morris 序,因为可以根据Morris 序进一步加工出先序、中序和后序.

package chapter_3_binarytreeproblem;public class Problem_05_MorrisTraversal {public static class Node {public int value;Node left;Node right;public Node(int data) {this.value = data;}}/*** Morris序遍历*/public static void morris(Node head) {if (head == null) {return;}Node cur = head;Node mostRight = null;while (cur != null) {mostRight = cur.left;if (mostRight != null) { // 如果当前cur有左子树// 找到cur左子树上最右的节点while (mostRight.right != null && mostRight.right != cur) {mostRight = mostRight.right;}// 从上面的while里出来后,mostRight就是cur左子树上最右的节点if (mostRight.right == null) { // 如果mostRight.right是指向null的mostRight.right = cur; // 让其指向curcur = cur.left; // cur向左移动continue; // 回到最外层的while,继续判断cur的情况} else { // 如果mostRight.right是指向cur的mostRight.right = null; // 让其指向null}}// cur如果没有左子树,cur向右移动// 或者cur左子树上最右节点的右指针是指向cur的,cur向右移动cur = cur.right;}}/*** 中序遍历*/public static void morrisIn(Node head) {if (head == null) {return;}Node cur = head;Node mostRight = null;while (cur != null) {mostRight = cur.left;if (mostRight != null) {  // 如果当前cur有左子树while (mostRight.right != null && mostRight.right != cur) {mostRight = mostRight.right;}if (mostRight.right == null) { // 如果mostRight.right是指向null的mostRight.right = cur;cur = cur.left;continue;} else { // 如果mostRight.right是指向cur的mostRight.right = null;}}// cur没有左子树,或者cur左子树上最右节点的右指针是指向cur的System.out.print(cur.value + " ");cur = cur.right;}System.out.println();}/*** 前序遍历*/public static void morrisPre(Node head) {if (head == null) {return;}Node cur = head;Node mostRight = null;while (cur != null) {mostRight = cur.left;if (mostRight != null) {  // 如果当前cur有左子树while (mostRight.right != null && mostRight.right != cur) {mostRight = mostRight.right;}if (mostRight.right == null) { // 如果mostRight.right是指向null的,即第一次到达mostRight.right = cur;System.out.print(cur.value + " ");cur = cur.left;continue;} else { // 如果mostRight.right是指向cur的,第二次到达mostRight.right = null;}} else {System.out.print(cur.value + " ");}cur = cur.right;}System.out.println();}/*** 后序遍历*/public static void morrisPos(Node head) {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;printEdge(cur.left);}}cur = cur.right;}printEdge(head);System.out.println();}public static void printEdge(Node head) {Node tail = reverseEdge(head);Node cur = tail;while (cur != null) {System.out.print(cur.value + " ");cur = cur.right;}reverseEdge(tail);}public static 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;}// for test -- print treepublic static void printTree(Node head) {System.out.println("Binary Tree:");printInOrder(head, 0, "H", 17);System.out.println();}public static void printInOrder(Node head, int height, String to, int len) {if (head == null) {return;}printInOrder(head.right, height + 1, "v", len);String val = to + head.value + to;int lenM = val.length();int lenL = (len - lenM) / 2;int lenR = len - lenM - lenL;val = getSpace(lenL) + val + getSpace(lenR);System.out.println(getSpace(height * len) + val);printInOrder(head.left, height + 1, "^", len);}public static String getSpace(int num) {String space = " ";StringBuffer buf = new StringBuffer("");for (int i = 0; i < num; i++) {buf.append(space);}return buf.toString();}public static void main(String[] args) {Node head = new Node(4);head.left = new Node(2);head.right = new Node(6);head.left.left = new Node(1);head.left.right = new Node(3);head.right.left = new Node(5);head.right.right = new Node(7);printTree(head);morrisIn(head);morrisPre(head);morrisPos(head);printTree(head);}}

这篇关于左神算法:遍历二叉树的神级方法(Morris遍历 / 线索二叉树)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/355921

相关文章

Conda与Python venv虚拟环境的区别与使用方法详解

《Conda与Pythonvenv虚拟环境的区别与使用方法详解》随着Python社区的成长,虚拟环境的概念和技术也在不断发展,:本文主要介绍Conda与Pythonvenv虚拟环境的区别与使用... 目录前言一、Conda 与 python venv 的核心区别1. Conda 的特点2. Python v

Spring Boot中WebSocket常用使用方法详解

《SpringBoot中WebSocket常用使用方法详解》本文从WebSocket的基础概念出发,详细介绍了SpringBoot集成WebSocket的步骤,并重点讲解了常用的使用方法,包括简单消... 目录一、WebSocket基础概念1.1 什么是WebSocket1.2 WebSocket与HTTP

SQL Server配置管理器无法打开的四种解决方法

《SQLServer配置管理器无法打开的四种解决方法》本文总结了SQLServer配置管理器无法打开的四种解决方法,文中通过图文示例介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的... 目录方法一:桌面图标进入方法二:运行窗口进入检查版本号对照表php方法三:查找文件路径方法四:检查 S

MyBatis-Plus 中 nested() 与 and() 方法详解(最佳实践场景)

《MyBatis-Plus中nested()与and()方法详解(最佳实践场景)》在MyBatis-Plus的条件构造器中,nested()和and()都是用于构建复杂查询条件的关键方法,但... 目录MyBATis-Plus 中nested()与and()方法详解一、核心区别对比二、方法详解1.and()

golang中reflect包的常用方法

《golang中reflect包的常用方法》Go反射reflect包提供类型和值方法,用于获取类型信息、访问字段、调用方法等,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值... 目录reflect包方法总结类型 (Type) 方法值 (Value) 方法reflect包方法总结

C# 比较两个list 之间元素差异的常用方法

《C#比较两个list之间元素差异的常用方法》:本文主要介绍C#比较两个list之间元素差异,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录1. 使用Except方法2. 使用Except的逆操作3. 使用LINQ的Join,GroupJoin

MySQL查询JSON数组字段包含特定字符串的方法

《MySQL查询JSON数组字段包含特定字符串的方法》在MySQL数据库中,当某个字段存储的是JSON数组,需要查询数组中包含特定字符串的记录时传统的LIKE语句无法直接使用,下面小编就为大家介绍两种... 目录问题背景解决方案对比1. 精确匹配方案(推荐)2. 模糊匹配方案参数化查询示例使用场景建议性能优

关于集合与数组转换实现方法

《关于集合与数组转换实现方法》:本文主要介绍关于集合与数组转换实现方法,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、Arrays.asList()1.1、方法作用1.2、内部实现1.3、修改元素的影响1.4、注意事项2、list.toArray()2.1、方

Java中的雪花算法Snowflake解析与实践技巧

《Java中的雪花算法Snowflake解析与实践技巧》本文解析了雪花算法的原理、Java实现及生产实践,涵盖ID结构、位运算技巧、时钟回拨处理、WorkerId分配等关键点,并探讨了百度UidGen... 目录一、雪花算法核心原理1.1 算法起源1.2 ID结构详解1.3 核心特性二、Java实现解析2.

Python中注释使用方法举例详解

《Python中注释使用方法举例详解》在Python编程语言中注释是必不可少的一部分,它有助于提高代码的可读性和维护性,:本文主要介绍Python中注释使用方法的相关资料,需要的朋友可以参考下... 目录一、前言二、什么是注释?示例:三、单行注释语法:以 China编程# 开头,后面的内容为注释内容示例:示例:四