Morris遍历

news2024/10/6 1:44:52

1、引入

  • 二叉树的遍历

递归实现的方式:

public static class Node {
	public int value;
	Node left;
	Node right;
	
	public Node(int data) {
		this.value = data;
	}
}

//每个节点都是被有限次访问,时间复杂度O(N),因为每次递归都要存储返回信息,N为树的节点个数,
//递归使用了系统栈,额外空间复杂度为树的高度
public static void process(Node root) {
	if (root == null) return ;
	
	//1. 如果在此处打印 root.value 是先序遍历
	//print(root.value);
	process(root.left);
	//2. 如果在此处打印root.value,是中序遍历
	process(root.right;
	//3. 如果在此处打印root.value,是后序遍历
}

非递归方式实现的时候依然要使用栈(自己实现的栈),额外空间复杂度还是树的高度。

也就是说无论是递归还是非递归方式遍历二叉树,树的高度这个额外空间是省不了的。

2、Morris简介

Morris遍历是一种遍历二叉树的方式,且时间复杂度为 O ( N ) O(N) O(N)额外空间复杂度为 O ( 1 ) O(1) O(1)

通过利用原树中大量空闲指针的方式,达到节省空间的目的。

3、Morris 遍历的细节

假设来到当前节点 cur,开始时 cur 来到头节点位置

  1. 如果 cur 没有左孩子,cur 向右移动( cur = cur.right)

  2. 如果 cur 有左孩子,找到左子树上最右的节点 mostRight
    a. 如果 mostRight 的右指针指向空,让其指向 cur,然后 cur 向左移动(cur = cur.left)
    b. 如果 mostRight 的右指针指向 cur,让其指向 null,然后 cur 向右移动(cur = cur.right)

  3. cur为空时遍历停止

举例:
请添加图片描述
请添加图片描述
请添加图片描述
请添加图片描述
请添加图片描述
整个遍历过程访问各个节点的顺序:[a, b, d, b, e, a, c, f, c, g],这个顺序就叫做Morris序,该序的特点是有左树的节点会访问两次,并且是在遍历完该节点的左树后第二次来到这个节点。

该流程的实质是在用每个节点的左树上最右节点的右指针状态来标记到底是第一次还是第二次访问这个节点。 一开始 cur 指向头节点 a 时,左树上最右节点 e 的右指针指向空,所以是第一次来到 a;然后调整 e 的右指针指向 a,经过一些变换,最后 cur 又指向 a,而此时的 e 的右指针指向 a,所以是第二次来到 a

4、通过Morris序加工出先序、中序和后序

Morris 序加工出先序:对于可以访问到两次的节点,在第一次访问到的时候就处理打印,第二次访问到的时候忽略;对于只访问一次的节点,就直接处理打印。

如上文的 Morris序:[a,b,d,b,e,a,c,f,c,g],处理得到的先序:[a,b,d,e,c,f,g]


Morris序加工出中序:对于访问到两次的节点,第一次访问到时忽略,第二次访问到时处理打印;对于只访问一次的节点,直接处理打印。

如上文的 Morris序:[a,b,d,b,e,a,c,f,c,g],处理得到的中序:[d,b,e,a,f,c,g]


Morris序加工出后序:处理时机放在能访问两次的节点的第二次访问上,当一个节点是第二次被访问时,逆序地打印它左树到右边界。

如上文的 Morris序:[a,b,d,b,e,a,c,f,c,g],处理时机只有标红的三个。

处理 b 的时候,逆序打印 b 的左树到右边界:d

处理 a 的时候,逆序打印 a 的左树到右边界:e,b

处理 c 的时候,逆序打印 c 的左树到右边界:f

Morris 序执行完后,单独逆序打印整棵树的右边界:g,c,a,使用链表反转实现

所以处理得到的后序:[d,e,b,f,g,c,a]

5、Morris遍历的实现

public class MorrisTraversal {

	public static class Node {
		public int value;
		Node left;
		Node right;

		public Node(int data) {
			this.value = data;
		}
	}
	
	//树的递归遍历
	public static void process(Node root) { 
		if (root == null) {
			return;
		}
		// 1
		process(root.left);
		// 2
		process(root.right);
		// 3
	}

	//Morris遍历
	public static void morris(Node head) {
		if (head == null) {
			return;
		}
		Node cur = head; //cur指针一开始在头结点
		Node mostRight = null; //左树上的最右节点
		while (cur != null) { //cur为空就停止
			mostRight = cur.left; //指向左孩子
			if (mostRight != null) { //左孩子不为空,即左树不为空
				//找到最右节点
				//因为有人为改的情况,所以当最右节点的右指针为空或者指向cur的时候停止
				while (mostRight.right != null && mostRight.right != cur) { 
					mostRight = mostRight.right;
				}
				//此时的mostRight是cur左树上的最右节点
				if (mostRight.right == null) {
					mostRight.right = cur; //人为修改最右节点右指针的指向
					cur = cur.left; //cur向左移动
					continue;
				} else { //mostRight的右指针指向cur
					mostRight.right = null; //使mostRight的右指针指向空
				}
			}
			cur = cur.right; //cur向右移动
		}
	}

	//Morris遍历得到先序
	//如果当前节点左树为空,则直接打印;如果左树不为空,在第一次访问的时候打印
	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) {
				while (mostRight.right != null && mostRight.right != cur) {
					mostRight = mostRight.right;
				}
				if (mostRight.right == null) { //这是第一次访问到cur的时候
					System.out.print(cur.value + " "); //所以直接打印
					mostRight.right = cur;
					cur = cur.left;
					continue;
				} else {
					mostRight.right = null;
				}
			} else { //没有左树,则直接打印
				System.out.print(cur.value + " ");
			}
			cur = cur.right;
		}
		System.out.println();
	}

	//Morris遍历得到中序
	//如果当前节点左树为空,则直接打印;如果左树不为空,在第二次访问的时候打印
	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) {
				while (mostRight.right != null && mostRight.right != cur) {
					mostRight = mostRight.right;
				}
				if (mostRight.right == null) {
					mostRight.right = cur;
					cur = cur.left;
					continue; //第一次访问cur时,会continue不会往下执行
				} else {
					mostRight.right = null; //第二次访问到cur,会继续往下执行
				}
			}
			//cur没有左树 和 cur第二次被访问到时都会执行该code
			System.out.print(cur.value + " "); 
			cur = cur.right;
		}
		System.out.println();
	}

	// Morris遍历得到后序
	// 每个节点访问其左树到右边界的节点2遍,但是所有节点遍历其左树右边界的规模也就是整棵树的规模,所以时间复杂度仍然是O(N)
	
	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;
		}
		//Morris完毕之后,逆序打印整棵树的右边界
		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 tree
	public 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);

	}
}

6、应用

很多题目都涉及到二叉树的遍历,利用Morris遍历可以改写得到最优解。

如,判断一棵树是否为搜索二叉树。

利用Morris遍历,就是在得到中序的过程中将打印行为替换为与前一个节点值比较即可。

	public static boolean isBST(Node head) {
		if (head == null) {
			return true;
		}
		Node cur = head;
		Node mostRight = null;
		Integer pre = null; //中序遍历中上一个节点的值
		boolean ans = true;
		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;
				}
			}
			if (pre != null && pre >= cur.value) {
				ans = false;
			}
			pre = cur.value;
			cur = cur.right;
		}
		//必须等Morris跑完之后才返回
		//因为Morris遍历过程中有人为修改指针的情况,必须等整个遍历过程结束全部恢复之后再返回
		return ans;
	}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/134495.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

hadoop 集群搭建(详细版)

hadoop 集群搭建更改主机名映射设置免密同步时间创建工作目录下载jdk安装配置Hadoop修改配置文件向其他节点分发配置完成的程序为Hadoop添加环境变量启动集群初始化启动集群web页面web页面:[hdfsweb页面](http://192.168.88.128:9870/)web页面:[yarnweb页面](http://192.168.88…

3.0、Linux-常用目录、文件基本命令

3.0、Linux-常用目录、文件基本命令 命令&#xff1a;ls&#xff08;列出目录&#xff09; ls 命令在 Linux 中是常常被使用到的&#xff0c;因为 Linux 不像 Windows有可视化的界面&#xff1b; -a 参数&#xff1a;all &#xff0c;查看全部的文件&#xff0c;包括隐藏文件&…

【免杀前置课——Windows编程】二十三、内存管理—堆内存管理、虚拟内存管理、文件映射、共享内存、不依靠临界区限制文件多开、DLL注入

内存管理—堆文件映射***文件映射的概念:***共享内存文件多开限制新思路DLL注入远程线程注入远程线程注入.exetest.dll文件映射 文件映射的概念: 文件映射(Mapping&#xff09;是一种将文件内容映射到进程虚拟内存的技术。 映射成功的文件可以用视图,来引用这段内存,从而达到…

中科易安联网智能门锁2022年度总结

时光如梭&#xff0c;步履不辍。在这繁忙而又充实的一年&#xff0c;中科易安从提升服务、优化产品、扩展市场的维度发力&#xff0c;通过扎实的努力、不懈的勤勉&#xff0c;圆满地完成了2022年的工作。接下来&#xff0c;中科易安将为媒体、友商、用户朋友们呈现中科易安2022…

通过Lambda表达式 简单体验一下java方法引用

观看本文前 您需要先掌握 Lambda表达式 如果您之前没有接触过 可以先查看我的文章 java Lambda概念 通过实现线程简单体验一下Lambda表达式 java Lambda表达式的标准格式及其前提带有(代码演示) 然后 我们用 Lambda表达式 写在里面的其实就是一种解决方案 拿参数做操作 那么 …

Qss文件设置Qt界面风格

需要协商才能修改软件界面的风格&#xff0c;所以要留出通用的接口&#xff0c;于是选择使用QSS文件设置软件风格。 一、创建Qss文件 直接创建以.qss为后缀的文件 二、Qt使用Qss文件有两种办法 1、第一种办法&#xff0c;添加资源文件.qrc&#xff0c;然后在qrc文件中添加qss文…

【云边有个小卖部】

童年就像童话&#xff0c;这是他们在童话里第一次相遇。 那么热的夏天&#xff0c;少年的后背被女孩的悲伤烫出一个洞&#xff0c;一直贯穿到心脏。 刘十三被欺负得最惨&#xff0c;却想保护凶巴巴的程霜。 每当她笑的时候&#xff0c;就让他想起夏天灌木丛里的萤火虫&#xff…

Tic-Tac-Toe有多少种不同棋局和盘面状态(python实现)

目录 1. 前言 2. 如何去重&#xff1f; 3. 代码实现 3.1 对称等价判断 3.2 find_neighbor()改造 3.3 主程序及运行结果 4. 延申思考 1. 前言 在前两篇博客中实现了遍历搜索所有的Tic-Tac-Toe的棋局的python程序实现。 Tic-Tac-Toe可能棋局搜索的实现&#xff08;python…

【Java寒假打卡】Java基础-多态

【Java寒假打卡】Java基础-多态概述多态中成员访问的特点多态的好处和弊端多态中转型多态中转型存在的风险概述 同一个对象在不同时刻表现出来的不同形态 多态的前提和体现 有继承/实现关系有方法重写。子类对父类进行方法重写有父类引用指向子类对象 package com.hfut.edu.…

【阶段二】Python数据分析NumPy工具使用02篇:数组的基本属性与数组的数据获取

本篇的思维导图: 数组的基本属性 NumPy数组的基本属性主要包括数组的形状、大小、类型和维数。 描述 代码 结果

Zookeeper详解(一)——基础介绍

概念 zookeeper官网&#xff1a;https://zookeeper.apache.org/ 大数据生态系统里的很多组件的命名都是某种动物或者昆虫&#xff0c;比如hadoop就是 &#x1f418;&#xff0c;hive就是&#x1f41d;。zookeeper即动物园管理者&#xff0c;顾名思义就是管理大数据生态系统各…

Linux 系统调用的本质

简单概念 fd #include <unistd.h> #include <string.h>int main(int argc,char* argv[]) {char buf[20]{0};read(0,buf,15);write(1,buf,strlen(buf));return 0; }如果想查看某个系统编程的接口&#xff0c;比如想查看 open 函数的用法&#xff0c;可以这样操作…

Python调用C++代码用法——Linux

目录 前言 C/C动态共享库编译 ctype模块 ctype数据类型 使用案例 float数据 指针 结构体及结构体指针 numpy图像当作指针传入 参考资料&#xff1a; 前言 在项目开发中&#xff0c;有时会使用到多种编程语言&#xff0c;比如部分功能是C/C代码实现的&#xff0c;而另一…

《机器学习与应用》实验二:BP神经网络实验

文章目录 一、实验目的二、实验原理BP算法的数学描述三、程序四、实验结论一、实验目的 1、 熟悉MATLAB中神经网络工具箱的使用方法; 2、 通过在MATLAB下面编程实现BP网络逼近标准正弦函数,来加深对BP网络的了解和认识,理解信号的正向传播和误差的反向传递过程。 二、实验…

SAP MM物料与客户主数据的税分类

一&#xff0e;说明 在物料主数据、客户主数据中均有税分类的维护&#xff0c;税分类既不是税码也不代表税率&#xff0c;它们的作用是通过税务条件记录确定税码。所有的税分类在主数据中都是与国家相关的无组织机构数据&#xff0c;例如物料的销售组织有中国&#xff08;ZH&am…

智慧WMS立体仓库管理系统源码 基于springboot框架(已经测试完整带部署搭建教程)源码分享!

淘源码&#xff1a;国内知名的高品质源码免费下载平台 分享一套智慧WMS立体仓库管理系统源码&#xff0c;基于springboot框架 已经测试完整带部署搭建教程。&#xff08;MF00767&#xff09; 需要源码学习可私信我获取。 技术架构 技术框架&#xff1a;SpringBoot layui H…

ESLint插件的使用

官网地址 规范写代码的工具. 多人开发不同规则,提交代码一堆冲突 培养代码风格使用 vscode更改tab缩进空格数----设置—搜索tabsize—找到tab size—修改2(每次按下tab都缩进俩空格)—Vetur > Format Options: Tab Size这个也要修改为2 vscode搜索format----勾选Editor: Fo…

javaweb01--mysql的介绍和增删改查操作

文章目录Mysql的介绍和增删改查说明1. mysql的登陆和退出11 登陆1.2 退出2. SQL语法的简单介绍2.1 语法2.2 SQL分类3. SQL主要操作语句3.1 DDL:操作数据库3.1.1 查询3.1.2 创建数据库3.1.3 删除数据库3.1.4 使用数据库3.2 DDL:操作表3.2.1 查询表3.2.2 创建表3.2.3 数据类型3.2…

《梁启超家书》笔记二——一个人若是在舒服的环境中会消磨志气,那么在困苦懊丧的环境中也一定会消磨志气

目录 一、做事的态度 二、学习与未来 三、发挥其个性之特长&#xff0c;以靖献于社会 四、鼓励相信孩子 五、犯错 六、身体健康 七、做事 八、与费用相关 九、在困苦中求快活 十、让孩子自由决策与建议 十一、处事态度&#xff1a;不要悲观 十二、时事分析 一、做事…

在IDEA中获取文件绝对路径(通用方式)

package com.javase.reflect;/*** 关于文件路径问题* 以前我们都是在IDEA中&#xff0c;依据IDEA默认的当前路径&#xff1a;project的根来获取文件&#xff0c;但是这种方法有它的局限性&#xff0c;那就是当代码离开了IDEA* 换到了其他位置&#xff0c;我们就找不到文…