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

2023-12-31 20:08

本文主要是介绍左神算法:遍历二叉树的神级方法(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/557012

相关文章

Nginx设置连接超时并进行测试的方法步骤

《Nginx设置连接超时并进行测试的方法步骤》在高并发场景下,如果客户端与服务器的连接长时间未响应,会占用大量的系统资源,影响其他正常请求的处理效率,为了解决这个问题,可以通过设置Nginx的连接... 目录设置连接超时目的操作步骤测试连接超时测试方法:总结:设置连接超时目的设置客户端与服务器之间的连接

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

Python使用国内镜像加速pip安装的方法讲解

《Python使用国内镜像加速pip安装的方法讲解》在Python开发中,pip是一个非常重要的工具,用于安装和管理Python的第三方库,然而,在国内使用pip安装依赖时,往往会因为网络问题而导致速... 目录一、pip 工具简介1. 什么是 pip?2. 什么是 -i 参数?二、国内镜像源的选择三、如何

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Linux使用nload监控网络流量的方法

《Linux使用nload监控网络流量的方法》Linux中的nload命令是一个用于实时监控网络流量的工具,它提供了传入和传出流量的可视化表示,帮助用户一目了然地了解网络活动,本文给大家介绍了Linu... 目录简介安装示例用法基础用法指定网络接口限制显示特定流量类型指定刷新率设置流量速率的显示单位监控多个

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

C#中读取XML文件的四种常用方法

《C#中读取XML文件的四种常用方法》Xml是Internet环境中跨平台的,依赖于内容的技术,是当前处理结构化文档信息的有力工具,下面我们就来看看C#中读取XML文件的方法都有哪些吧... 目录XML简介格式C#读取XML文件方法使用XmlDocument使用XmlTextReader/XmlTextWr

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

oracle DBMS_SQL.PARSE的使用方法和示例

《oracleDBMS_SQL.PARSE的使用方法和示例》DBMS_SQL是Oracle数据库中的一个强大包,用于动态构建和执行SQL语句,DBMS_SQL.PARSE过程解析SQL语句或PL/S... 目录语法示例注意事项DBMS_SQL 是 oracle 数据库中的一个强大包,它允许动态地构建和执行