【数据结构与算法】二叉树的 Morris 遍历(前序、中序、后序)
前置说明
不了解二叉树非递归遍历的可以看我之前的文章【数据结构与算法】二叉树模板及例题
Morris 遍历
概述
Morris 遍历是一种遍历二叉树的方式,并且时间复杂度O(N),额外空间复杂度O(1) 。通过利用原树中大量空闲指针的方式,达到节省空间的目的
分析
设一棵二叉树有 n 个节点,则所有节点的指针域总和为 2 * n ,所有节点的非空指针域总和为 n - 1(非根节点被一个指针指向,根节点不被指针指向),所有节点的空指针域总和为 2n - (n - 1) = n + 1。
可以看到有大量的空指针域没有用到,在可以改变原二叉树结构的前提下,我们可以通过合理利用节点的空指针域,不开辟额外空间进行二叉树的非递归遍历。
那么先序、中序、后序遍历的节点访问顺序是如何确定的呢
如上图,根据紫色箭头顺序访问,第一次访问到的节点组成的集合就是先序遍历的结果。类似的,第二次访问到的节点组成的集合就是中序遍历的结果;第三次访问到的节点组成的集合就是后序遍历的结果。
通过设置节点访问不同次数的操作就可以实现三种遍历。
Morris 遍历的实质:建立一种机制,对于没有左子树的节点只到达一次,对于有左子树的节点会到达两次
Morris 遍历的原则
假设来到当前节点 cur,开始时 cur 来到头节点位置
如果 cur 没有左孩子,cur向右移动(cur = cur.right)
如果 cur 有左孩子,找到左子树上最右的节点 mostRight
a.如果 mostRight 的右指针指向空,让其指向 cur, 然后 cur 向左移动(cur = cur.left)
b.如果 mostRight 的右指针指向 cur,让其指向 null, 然后 cur 向右移动(cur = cur.right)
cur 为空时遍历停止
举个例子:
1️⃣ 首先 cur 来到头结点 1,按照 morris 原则的第二条第一点,它存在左孩子,cur 左子树上最右的节点为 5,它的 right 指针指向空,所以让其指向 1,cur 向左移动到2。
2️⃣ 2 有左孩子,且它左子树最右的节点 4 指向空,按照 morris 原则的第二条第一点,让 4 的 right 指针指向 2,cur 向左移动到 4
3️⃣ 4 不存在左孩子,按照 morris 原则的第一条,cur 向右移动,在第二步中,4 的 right 指针已经指向了 2,所以 cur 会回到 2
4️⃣ 重新回到 2,有左孩子,它左子树最右的节点为 4,但是在第二步中,4 的 right 指针已经指向了 2,不为空。所以按照 morris 原则的第二条第二点,cur 向右移动到 5,同时 4 的 right 指针重新指向空
5️⃣ 5 不存在左孩子,按照 morris 原则的第一条,cur 向右移动,在第一步中,5 的 right 指针已经指向了 1,所以 cur 会回到 1
6️⃣ cur 回到 1,回到头结点,左子树遍历完成,1 有左孩子,左子树上最右的节点为 5,它的 right 指针指向 1,按照 morris 原则的第二条第二点,1 向右移动到 3,同时 5 的 right 指针重新指回空
7️⃣ 3 有左孩子,且它左子树最右的节点 6 指向空,按照 morris 原则的第二条第一点,让 6 的 right 指针指向 3,cur 向左移动到 6
8️⃣ 6 不存在左孩子,按照 morris 原则的第一条,cur 向右移动,在第二步中,6 的 right 指针已经指向了 3,所以 cur 会回到 3
9️⃣ 重新回到 3,有左孩子,它左子树最右的节点为 6,但是在第二步中,6 的 right 指针已经指向了 3,不为空。所以按照 morris 原则的第二条第二点,cur 向右移动到 7,同时 6 的 right 指针重新指向空
1️⃣0️⃣ cur 没有左孩子,向右移动到 null,遍历停止
以上就是 Morris 遍历的全过程了,通过在遍历过程中适当的位置,即每个节点访问特定次数后设置操作,可以实现三种遍历
前序遍历
对于没有左子树的节点只到达一次,直接打印
对于有左子树的节点会到达两次,则在第一次到达时打印
public static void morrisPre(Node head) {
if(head == null){
return;
}
Node cur = head;
Node mostRight = null;
while (cur != null){
// cur表示当前节点,mostRight表示cur的左孩子的最右节点
mostRight = cur.left;
if(mostRight != null){
// cur有左孩子,找到cur左子树最右节点
while (mostRight.right !=null && mostRight.right != cur){
mostRight = mostRight.right;
}
// mostRight的右孩子指向空,让其指向cur,cur向左移动
if(mostRight.right == null){
mostRight.right = cur;
System.out.print(cur.value+" "); // 此时第一次访问节点
cur = cur.left;
continue; // 直接进入下一次循环
}else {
// mostRight的右孩子指向cur,让其指向空,cur向右移动
mostRight.right = null;
}
}else {
System.out.print(cur.value + " "); // 没有左孩子的话直接输出,该节点就是第一次访问
}
cur = cur.right;
}
System.out.println();
}
中序遍历
对于没有左子树的节点只到达一次,直接打印
对于有左子树的节点会到达两次,第二次到达时打印
public static void morrisIn(Node head) {
if(head == null){
return;
}
Node cur = head;
Node mostRight = null;
while (cur != null){
// cur表示当前节点,mostRight表示cur的左孩子的最右节点
mostRight = cur.left;
if(mostRight != null){
// cur有左孩子,找到cur左子树最右节点
while (mostRight.right !=null && mostRight.right != cur){
mostRight = mostRight.right;
}
// mostRight的右孩子指向空,让其指向cur,cur向左移动
if(mostRight.right == null){
mostRight.right = cur;
cur = cur.left;
continue; // 直接进入下一次循环
}else { // 第二次到达
// mostRight的右孩子指向cur,让其指向空,cur向右移动
mostRight.right = null;
}
}
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 node){ // 逆序打印:反转链表打印后再反转回原样
Node tail =reverseEdge(node);
Node cur = tail;
while (cur != null ){
System.out.print(cur.value+" ");
cur =cur.right;
}
reverseEdge(tail);
}
public static Node reverseEdge(Node node){ // 链表反转
Node pre = null;
Node next = null;
while (node != null){
next = node.right;
node.right = pre;
pre = node;
node = next;
}
return pre;
}
【数据结构与算法】二叉树的 Morris 遍历(前序、中序、后序)的更多相关文章
- python数据结构与算法——二叉树结构与遍历方法
先序遍历,中序遍历,后序遍历 ,区别在于三条核心语句的位置 层序遍历 采用队列的遍历操作第一次访问根,在访问根的左孩子,接着访问根的有孩子,然后下一层 自左向右一一访问同层的结点 # 先序遍历 # ...
- 算法进阶面试题03——构造数组的MaxTree、最大子矩阵的大小、2017京东环形烽火台问题、介绍Morris遍历并实现前序/中序/后序
接着第二课的内容和带点第三课的内容. (回顾)准备一个栈,从大到小排列,具体参考上一课.... 构造数组的MaxTree [题目] 定义二叉树如下: public class Node{ public ...
- javascript数据结构与算法--二叉树遍历(后序)
javascript数据结构与算法--二叉树遍历(后序) 后序遍历先访问叶子节点,从左子树到右子树,再到根节点. /* *二叉树中,相对较小的值保存在左节点上,较大的值保存在右节点中 * * * */ ...
- javascript数据结构与算法--二叉树遍历(先序)
javascript数据结构与算法--二叉树遍历(先序) 先序遍历先访问根节点, 然后以同样方式访问左子树和右子树 代码如下: /* *二叉树中,相对较小的值保存在左节点上,较大的值保存在右节点中 * ...
- javascript数据结构与算法--二叉树遍历(中序)
javascript数据结构与算法--二叉树遍历(中序) 中序遍历按照节点上的键值,以升序访问BST上的所有节点 代码如下: /* *二叉树中,相对较小的值保存在左节点上,较大的值保存在右节点中 * ...
- javascript数据结构与算法-- 二叉树
javascript数据结构与算法-- 二叉树 树是计算机科学中经常用到的一种数据结构.树是一种非线性的数据结构,以分成的方式存储数据,树被用来存储具有层级关系的数据,比如文件系统的文件,树还被用来存 ...
- javascript数据结构与算法---二叉树(删除节点)
javascript数据结构与算法---二叉树(删除节点) function Node(data,left,right) { this.data = data; this.left = left; t ...
- javascript数据结构与算法---二叉树(查找最小值、最大值、给定值)
javascript数据结构与算法---二叉树(查找最小值.最大值.给定值) function Node(data,left,right) { this.data = data; this.left ...
- SDUT OJ 数据结构实验之二叉树二:遍历二叉树
数据结构实验之二叉树二:遍历二叉树 Time Limit: 1000 ms Memory Limit: 65536 KiB Submit Statistic Discuss Problem Descr ...
- SDUT 3341 数据结构实验之二叉树二:遍历二叉树
数据结构实验之二叉树二:遍历二叉树 Time Limit: 1000MS Memory Limit: 65536KB Submit Statistic Problem Description 已知二叉 ...
随机推荐
- Windows上使用Python2.7安装pip
资料包含: setuptools-33.1.1 pip-18.1 ez_setup.py get-pip.py 方法一 先安装 setuptools: 进入 setuptools 文件夹,运行:pyt ...
- 聊聊 PC 端自动化最佳方案 - Pywinauto
1. 前言 大家好,我是安果! 上一篇文章,聊到 PC 端的一种自动化方案:WinAppDriver 聊聊 PC 端自动化最佳方案 - WinAppDriver 有小伙伴后台给我留言,说「 pywin ...
- 🏆【Alibaba微服务技术系列】「Dubbo3.0技术专题」回顾Dubbo2.x的技术原理和功能实现及源码分析(温故而知新)
RPC服务 什么叫RPC? RPC[Remote Procedure Call]是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范.它允许程序调用另一个地址空间(通常是共享网络的另 ...
- 阿里云服务器,http免费转https详细教程
1.搜ssl证书,点击立即购买 2.这里我们选择免费证书,点击右边立即购买,去支付 购买完成之后,申请证书状态会显示在审核中,不过很快的,几分钟就申请成功了,以下就是我申请成功的界面,因为我服务器用的 ...
- centos7 netstat
netstat 是控制台命令,它可以显示路由表.实际的网络连接以及每一个网络接口设备的状态信息.Netstat 用于显示与 IP . TCP . UDP 和 ICMP 协议相关的统计数据,一般用于检验 ...
- centos7 配置 ftp 服务器(本地用户)
2021-09-02 1. 安装 # 安装 vsftpd yum -y install vsftpd 2. 启动服务并添加到开机自启 # 启动 vsftpd systemctl start vsftp ...
- Nginx的高级使用
1.概述 之前介绍过Nginx的简单使用,今天来聊聊Nginx的一些高级使用. 2.使用Nginx解决跨域问题 当公司存在多个域名时,两个不同的域名相互访问就会存在跨域问题. 或者在进行前端开发时,通 ...
- MPI集群搭建
高性能计算 ubantu下集群搭建 参考博客:https://blog.csdn.net/u012304016/article/details/52423738(尊重别人的知识产权),一些细节 ...
- Object-源码
Object的结构 类构造器 一个类必须要有一个构造器的存在 , Object类源码中,是看不到构造器的,系统会自动添加一个无参构造器. Object obj = new Object(): equa ...
- C# Equals方法和==有什么区别
开发工具:VS2019 一.关于这两个比较,需要从值类型和引用类型两方面来说 (A)先说值类型 上图: 因为在对值类型进行比较时候,不管 .Equals() 方法还是 == 方法,都是对值类型变量(图 ...