左神算法:遍历二叉树的神级方法(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

相关文章

Java实现任务管理器性能网络监控数据的方法详解

《Java实现任务管理器性能网络监控数据的方法详解》在现代操作系统中,任务管理器是一个非常重要的工具,用于监控和管理计算机的运行状态,包括CPU使用率、内存占用等,对于开发者和系统管理员来说,了解这些... 目录引言一、背景知识二、准备工作1. Maven依赖2. Gradle依赖三、代码实现四、代码详解五

JAVA中整型数组、字符串数组、整型数和字符串 的创建与转换的方法

《JAVA中整型数组、字符串数组、整型数和字符串的创建与转换的方法》本文介绍了Java中字符串、字符数组和整型数组的创建方法,以及它们之间的转换方法,还详细讲解了字符串中的一些常用方法,如index... 目录一、字符串、字符数组和整型数组的创建1、字符串的创建方法1.1 通过引用字符数组来创建字符串1.2

Java调用Python代码的几种方法小结

《Java调用Python代码的几种方法小结》Python语言有丰富的系统管理、数据处理、统计类软件包,因此从java应用中调用Python代码的需求很常见、实用,本文介绍几种方法从java调用Pyt... 目录引言Java core使用ProcessBuilder使用Java脚本引擎总结引言python

Apache Tomcat服务器版本号隐藏的几种方法

《ApacheTomcat服务器版本号隐藏的几种方法》本文主要介绍了ApacheTomcat服务器版本号隐藏的几种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需... 目录1. 隐藏HTTP响应头中的Server信息编辑 server.XML 文件2. 修China编程改错误

Java中switch-case结构的使用方法举例详解

《Java中switch-case结构的使用方法举例详解》:本文主要介绍Java中switch-case结构使用的相关资料,switch-case结构是Java中处理多个分支条件的一种有效方式,它... 目录前言一、switch-case结构的基本语法二、使用示例三、注意事项四、总结前言对于Java初学者

使用Python实现大文件切片上传及断点续传的方法

《使用Python实现大文件切片上传及断点续传的方法》本文介绍了使用Python实现大文件切片上传及断点续传的方法,包括功能模块划分(获取上传文件接口状态、临时文件夹状态信息、切片上传、切片合并)、整... 目录概要整体架构流程技术细节获取上传文件状态接口获取临时文件夹状态信息接口切片上传功能文件合并功能小

Oracle Expdp按条件导出指定表数据的方法实例

《OracleExpdp按条件导出指定表数据的方法实例》:本文主要介绍Oracle的expdp数据泵方式导出特定机构和时间范围的数据,并通过parfile文件进行条件限制和配置,文中通过代码介绍... 目录1.场景描述 2.方案分析3.实验验证 3.1 parfile文件3.2 expdp命令导出4.总结

更改docker默认数据目录的方法步骤

《更改docker默认数据目录的方法步骤》本文主要介绍了更改docker默认数据目录的方法步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录1.查看docker是否存在并停止该服务2.挂载镜像并安装rsync便于备份3.取消挂载备份和迁

JavaScript DOM操作与事件处理方法

《JavaScriptDOM操作与事件处理方法》本文通过一系列代码片段,详细介绍了如何使用JavaScript进行DOM操作、事件处理、属性操作、内容操作、尺寸和位置获取,以及实现简单的动画效果,涵... 目录前言1. 类名操作代码片段代码解析2. 属性操作代码片段代码解析3. 内容操作代码片段代码解析4.

SpringBoot3集成swagger文档的使用方法

《SpringBoot3集成swagger文档的使用方法》本文介绍了Swagger的诞生背景、主要功能以及如何在SpringBoot3中集成Swagger文档,Swagger可以帮助自动生成API文档... 目录一、前言1. API 文档自动生成2. 交互式 API 测试3. API 设计和开发协作二、使用