数据结构 - 线索树

一、 为什么要用到线索二叉树?

我们先来看看普通的二叉树有什么缺点。下面是一个普通二叉树(链式存储方式):

在这里插入图片描述
乍一看,会不会有一种违和感?整个结构一共有 7 个结点,总共 14 个指针域,其中却有 8 个指针域都是空的。对于一颗有 n 个结点的二叉树而言,总共会有 n+1 个空指针域,这个规律使用所有的二叉树。

这么多的空指针域是不是显得很浪费?我们学习数据结构和算法的重点就是在想法设法地提高时间效率和空间利用率。这么多的指针域就这么白白浪费了,太败家了!

所以我们要想法子好好利用它们,利用它们来帮助我们更好地使用二叉树这个数据结构。

那么如何利用呢?

遍历二叉树的实质是将二叉树中非线性结构的结点转化为线性的序列,然后才能方便我们遍历。

比如上图的中序遍历序列为:DBGEACF。

对于一个线性序列(线性表)来说,它有直接前驱和直接后继的概念。比如在中序遍历序列中,B 的直接前驱为 D,直接后继为 G。

我们之所以能知道 B 的直接前驱和直接后继,是因为我们按照中序遍历的算法,把二叉树的中序遍历序列写出来了,然后根据这个顺序序列说谁的前驱是谁、后继是谁。

直接前驱和直接后继是不能完全直接通过二叉树得到的,因为二叉树中只有双亲和孩子结点之间的直接关系,即二叉树的结点指针域中只存储了其孩子结点的地址。

现在的需求是,我想能直接从二叉树上得到某结点在中序遍历方式下的直接前驱和直接后继。

这时候就需要用到线索二叉树了。

二、 什么是线索二叉树?

当然,我们肯定需要借助结点的指针域来保存直接前驱和直接后继的地址。

其实,在上图的普通二叉树中(以中序遍历得到的序列),部分结点(指针域不为空的结点)是可以找到其直接前驱或后继的,比如结点 E 的左孩子 G 就是结点 E 的直接前驱;结点 A 的右孩子 C 就是结点 A 的直接后继。

但部分结点(指针域为空)是行不通的,比如结点 G 的直接后继是 E,直接前驱是 B,但在二叉树中却不能得出这样的结论。怎么办呢?我们注意到,结点 G 的两个指针域都为 NULL,并未被利用,那么我们使用这两个指针,分别指向其前驱和后继不就好了吗?

在这里插入图片描述
实在是两全其美,天作之合!但是问题并没有解决!

因为我们是利用空指针域来指向前驱或后继的,对于那些指针域不为空的结点,这样是矛盾的,比如结点 E 和结点 B。

既然有矛盾,那么我们就发现产生矛盾的根源,解决矛盾。

产生矛盾的根源是:结点的指针域为空和不为空时,指针的指向矛盾。即,指针不为空时指向孩子和指针为空时指向前驱或后继之间的矛盾。

那么我们对症下药,把指针域为空和不为空给区分出来,清晰地告诉指针:不为空时指向孩子,为空时指向前驱或后继。这就需要我们给两个指针各添加一个标志位。

在这里插入图片描述

并约定以下规则:
left_flag == 0 时,指针 left_child 指向左孩子
left_flag == 1 时,指针 left_child 指向直接前驱
right_flag == 0 时,指针 right_child 指向右孩子
right_flag == 1 时,指针 right_child 指向直接前驱

二叉树的结点要有所变化:

/*线索二叉树的结点的结构体*/
typedef struct Node {char data; //数据域struct Node *left_child; //左指针域int left_flag; //左指针标志位struct Node *right_child; //右指针域int right_flag; //右指针标志位
} TTreeNode;

有了标志位,一切就能理清了。我们称指向直接前驱和后继的指针为线索。标志位为 0 的指针是指向孩子的指针,标志位为 1 的指针是线索。

一个二叉链表树,结点结构如上,我们将所有空指针都变为线索,这样的二叉树就是二叉线索树。

三、如何创造线索二叉树?

在普通二叉树中,我们想要获取某个结点在某种遍历次序下的直接前驱或后继,每次都需要遍历获取到遍历次序之后才能知道。而在线索二叉树中,我们只需要遍历一次(创造线索二叉树时的遍历),之后,线索二叉树就能“记住”每个结点的直接前驱和后继了,以后都不需要再通过遍历次序获取前驱或后继了。

我们按照某种遍历方式,把普通二叉树变为线索二叉树的过程被称为二叉树的线索化。

接下来,我们用中序遍历的方式,将下面的二叉树线索化为线索二叉树
在这里插入图片描述
将标志位为 1 的指针,按照中序遍历序列,使其指向前驱或后继:
在这里插入图片描述
其中,结点 D 没有直接前驱,结点 F 没有直接后继,故指针为 NULL。

到此,我们算是解决了拥有 n 个结点的二叉树存在 n+1 个空指针域所造成的浪费,解决方式是给每个结点的指针增加一个标志位,以此来利用空指针域。标志位中存储的是 0 或 1 的布尔值,与浪费的空指针域相比,是相对比较划算的。而且使二叉树具有了一种新特性——二叉树中能保存在某种遍历次序下的结点之间的前驱和后继关系。

四、线索化的实现

请注意一点,线索二叉树是由普通二叉树得来的,而且是按某种遍历顺序得来的。因为线索是在知道某个结点的前驱和后继的情况下才能设置,而前驱和后继关系不能通过二叉树直接体现,只能通过遍历二叉树得到的线性序列得出关系。所以要通过某种遍历方式得到具有前驱和后继关系的序列后,才能修改结点的空指针,进而设置线索。

即:线索化的实质是在按照某种遍历次序进行遍历二叉树的过程中修改结点的空指针,使其指向其在该遍历次序下的直接前驱或直接后继的过程。

所以,代码的大体结构还是一样的,我们只需把遍历代码中的打印代码换成线索化的代码,并作出一些其他改变即可。

下面以下图为例,分别介绍三种线索化:

一颗未线索化的二叉树,其所有标志位均默认为 0.

在这里插入图片描述
4.1. 中序线索化

按照中序遍历次序线索化后,可得下图:
在这里插入图片描述
我们先再次明确以下内容:

  • 我们是在遍历二叉树的过程中进行线索化的。
  • 中序遍历的顺序为:左子树 >> 根 >> 右子树。
  • 线索化修改两个东西:空指针域和其对应的标志位。
  • 如何修改?将空指针域置为直接前驱或后继。

所以我们的问题变成了:

  1. 找到所有空指针域。
  2. 找到空指针域所属结点,在先序次序下的直接前驱和直接后继。
  3. 修改空指针域的内容,及其标志位,使该指针称为线索。

说明:我们在遍历二叉树时,使用到了递归,所以在进行线索化的时候,也会使用它。

具体代码如下:

//全局变量 prev 指针,指向刚访问过的结点
TTreeNode *prev = NULL;/*** 中序线索化*/
void inorder_threading(TTreeNode *root)
{if (root == NULL) { //若二叉树为空,做空操作return;}inorder_threading(root->left_child);if (root->left_child == NULL) {root->left_flag = 1;root->left_child = prev;}if (prev != NULL && prev->right_child == NULL) {prev->right_flag = 1;prev->right_child = root;}prev = root;inorder_threading(root->right_child);
}

4.2. 先序线索化

按照先序顺序线索化后,可得下图:
在这里插入图片描述
具体代码如下:

// 全局变量 prev 指针,指向刚访问过的结点
TTreeNode *prev = NULL;/*** 先序线索化*/
void preorder_threading(TTreeNode *root)
{if (root == NULL) {return;}if (root->left_child == NULL) {root->left_flag = 1;root->left_child = prev;}if (prev != NULL && prev->right_child == NULL) {prev->right_flag = 1;prev->right_child = root;}prev = root;if (root->left_flag == 0) {preorder_threading(root->left_child);}if (root->right_flag == 0) {preorder_threading(root->right_child);}
}

4.3. 后序线索化

按照后序遍历次序线索化后,可得下图:

在这里插入图片描述
具体代码如下:

//全局变量 prev 指针,指向刚访问过的结点
TTreeNode *prev = NULL;/*** 后序线索化*/
void postorder_threading(TTreeNode *root)
{if (root == NULL) {return;}postorder_threading(root->left_child);postorder_threading(root->right_child);if (root->left_child == NULL) {root->left_flag = 1;root->left_child = prev;}if (prev != NULL && prev->right_child == NULL) {prev->right_flag = 1;prev->right_child = root;}prev = root;
}

五、总结

线索二叉树充分利用了二叉树中的空指针域,给予二叉树一个新特性——通过一次遍历进行线索化后,二叉树中就能保存其结点之间的前驱和后继关系。

所以,如果我们需要频繁遍历二叉树,查找某个结点的直接前驱或后继结点,使用线索二叉树是非常合适的。

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

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

相关文章

WordPress函数wptexturize的介绍及用法示例,字符串替换为HTML实体

在查看WordPress你好多莉插件时发现代码中使用了wptexturize()函数用来随机输出一句歌词,下面boke112百科就跟大家一起来学习一下WordPress函数wptexturize的介绍及用法示例。 WordPress函数wptexturize介绍 wptexturize( string $text, bool $reset false ): st…

Ubuntu搭建计算集群

计算机硬件和技术的发展使得高性能模拟和计算在生活和工作中的作用逐渐显现出来,无论是计算化学,计算物理和当下的人工智能都离不开高性能计算。笔者工作主要围绕计算化学和物理开展,亦受限于自身知识和技术所限,文中只是浅显地尝…

HarmonyOS class类对象基础使用

按我们之前的写法 就是 Entry Component struct Dom {p:Object {name: "小猫猫",age: 21,gf: {name: "小小猫猫",age: 18,}}build() {Row() {Column() {// ts-ignoreText(this.p.gf.name)}.width(100%)}.height(100%)} }直接用 Object 一层一层往里套 这…

C++进阶(十三)异常

📘北尘_:个人主页 🌎个人专栏:《Linux操作系统》《经典算法试题 》《C》 《数据结构与算法》 ☀️走在路上,不忘来时的初心 文章目录 一、C语言传统的处理错误的方式二、C异常概念三、异常的使用1、异常的抛出和捕获2、异常的重新…

java之spring AOP

AOP 面向切面编程&#xff0c; 切入点&#xff0c;就是你写的函数&#xff0c;装饰器&#xff0c;装饰到那些函数上 在哪里生效 引入依赖<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifact…

网络学习:数据链路层VLAN原理和配置

一、简介&#xff1a; VLAN又称为虚拟局域网&#xff0c;它是用来将使用路由器的网络分割成多个虚拟局域网&#xff0c;起到隔离广播域的作用&#xff0c;一个VLAN通常对应一个IP网段&#xff0c;不同VLAN通常规划到不同IP网段。划分VLAN可以提高网络的通讯质量和安全性。 二、…

MySQL进阶查询篇(5)-事务的隔离级别与应用

数据库事务(Transaction)是指作为一个单元执行的一系列操作&#xff0c;要么全部成功完成&#xff0c;要么全部失败回滚。数据库事务具有四个特性&#xff0c;即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。本文将重点介绍MySQL数据库中的…

2024年华为OD机试真题-螺旋数字矩阵-Java-OD统一考试(C卷)

题目描述: 疫情期间,小明隔离在家,百无聊赖,在纸上写数字玩。他发明了一种写法: 给出数字个数n和行数m(0 < n ≤ 999,0 < m ≤ 999),从左上角的1开始,按照顺时针螺旋向内写方式,依次写出2,3...n,最终形成一个m行矩阵。 小明对这个矩阵有些要求: 1.每行数字的…

跟着小德学C++之TCP基础

嗨&#xff0c;大家好&#xff0c;我是出生在达纳苏斯的一名德鲁伊&#xff0c;我是要立志成为海贼王&#xff0c;啊不&#xff0c;是立志成为科学家的德鲁伊。最近&#xff0c;我发现我们所处的世界是一个虚拟的世界&#xff0c;并由此开始&#xff0c;我展开了对我们这个世界…

红队打靶练习:GLASGOW SMILE: 1.1

目录 信息收集 1、arp 2、nmap 3、nikto 4、whatweb 目录探测 1、gobuster 2、dirsearch WEB web信息收集 /how_to.txt /joomla CMS利用 1、爆破后台 2、登录 3、反弹shell 提权 系统信息收集 rob用户登录 abner用户 penguin用户 get root flag 信息收集…

flutter 国内源

Flutter 在中国由于网络原因&#xff0c;从官方默认的国外源下载Dart包和Flutter SDK可能会比较慢或者不稳定。为了加速依赖包的获取与Flutter SDK的安装&#xff0c;可以使用国内镜像源。以下是一些国内常用的Flutter和Dart包镜像源&#xff1a; 清华大学开源软件镜像站 Flu…

计算机网络(第六版)复习提纲29

第六章&#xff1a;应用层 SS6.1 域名系统DNS 1 DNS被设计为一个联机分布式数据库系统&#xff0c;并采用客户服务器方式&#xff08;C/S&#xff09; 2 域名的体系结构 3 域名服务器及其体系结构 A 域名服务器的分类 1 根域名服务器 2 顶级域名服务器&#xff08;TLD服务器&a…

Gitlab和Jenkins集成 实现CI (一)

版本声明 部署时通过docker拉取的最新版本 gitlab: 16.8 jenkins: 2.426.3 安装环境 可参考这篇文章 停止防火墙 由于在内网&#xff0c;这里防火墙彻底关掉&#xff0c;如果再外网或者云上的悠着点 systemctl stop firewalled systemctl disable firewalledsystemctl sto…

K8S之运用亲和性设置Pod的调度约束

亲和性 Node节点亲和性硬亲和实践软亲和性实践 Pod节点亲和性和反亲和性pod亲和性硬亲和实践 pod反亲和性 Pod 的yaml文件里 spec 字段中包含一个 affinity 字段&#xff0c;使用一组亲和性调度规则&#xff0c;指定pod的调度约束。 kubectl explain pods.spec.affinity 配置…

【代码】Processing笔触手写板笔刷代码合集

代码来源于openprocessing&#xff0c;考虑到国内不是很好访问&#xff0c;我把我找到的比较好的搬运过来&#xff01; 合集 参考&#xff1a;https://openprocessing.org/sketch/793375 https://github.com/SourceOf0-HTML/processing-p5.js/tree/master 这个可以体验6种笔触…

ubuntu22.04安装部署03: 设置root密码

一、前言 ubuntu22.04 安装完成以后&#xff0c;默认root用户是没有设置密码的&#xff0c;需要手动设置。具体的设置过程如下文内容所示&#xff1a; 相关文件&#xff1a; 《ubuntu22.04装部署01&#xff1a;禁用内核更新》 《ubuntu22.04装部署02&#xff1a;禁用显卡更…

简单聊聊go语言中引用模块的版本控制以及invalid: should be v0 or v1, not v2问题的解决

文章目录 前言具体示例手动升级依赖库should be v0 or v1, not v2总结 前言 如果你接触go语言比较早&#xff0c;一定有过当年所有go源码全部放入 GOPATH 下的混乱经历&#xff0c;不过发展到今天&#xff0c;go的包管理使用 go.mod 和 go.work 已经能得心应手&#xff0c;满足…

macos安装local模式spark

文章目录 配置说明安装hadoop安装Spark测试安装成功 配置说明 Scala - 3.18 Spark - 3.5.0 Hadoop - 3.3.6 安装hadoop 从这里下载相应版本的hadoop下载后解压&#xff0c;配置系统环境变量 > sudo vim /etc/profile添加以下两行 export HADOOP_HOME/Users/collinsliu/…

Unity类银河恶魔城学习记录4-4 4-5 P57-58 On Hit Impactp- Attack‘direction fix源代码

Alex教程每一P的教程原代码加上我自己的理解初步理解写的注释&#xff0c;可供学习Alex教程的人参考 此代码仅为较上一P有所改变的代码 【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili Entity.cs using System.Collections; using System.Collections.Generic;…

排序算法---快速排序

原创不易&#xff0c;转载请注明出处。欢迎点赞收藏~ 快速排序是一种常用的排序算法&#xff0c;采用分治的策略来进行排序。它的基本思想是选取一个元素作为基准&#xff08;通常是数组中的第一个元素&#xff09;&#xff0c;然后将数组分割成两部分&#xff0c;其中一部分的…