数据结构 - 线索树

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

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

在这里插入图片描述
乍一看,会不会有一种违和感?整个结构一共有 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…

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、异常的重新…

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

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

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

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

红队打靶练习: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 信息收集…

Gitlab和Jenkins集成 实现CI (一)

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

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

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

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

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

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

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

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

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

排序算法---快速排序

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

苹果mac电脑如何优化系统?保持不卡顿呢

再强悍的性能和优秀的操作系统,但长时间使用后,有时也会出现卡顿的情况。为了让你的苹果电脑保持高效运行,我们将深入探讨导致电脑卡顿的原因,并提供苹果电脑如何优化系统的解决方案,帮助你优化系统。 过多的启动项 …

排序算法---归并排序

原创不易,转载请注明出处。欢迎点赞收藏~ 归并排序是一种常见的排序算法,它采用了分治的思想。它将一个待排序的数组递归地分成两个子数组,分别对两个子数组进行排序,然后将排好序的子数组合并成一个有序数组。 具体的归并排序过…

Spring第二天

一、第三方资源配置管理 说明:以管理DataSource连接池对象为例讲解第三方资源配置管理 1 管理DataSource连接池对象 问题导入 配置数据库连接参数时,注入驱动类名是用driverClassName还是driver? 1.1 管理Druid连接池【重点】 数据库准备…

【集合系列】TreeMap 集合

TreeMap 集合 1. 概述2. 方法3. 遍历方式4. 排序方式5. 代码示例16. 代码示例27. 代码示例38. 注意事项 其他集合类 父类 Map 集合类的遍历方式 TreeSet 集合 具体信息请查看 API 帮助文档 1. 概述 TreeMap 是 Java 中的一个集合类,它实现了 SortedMap 接口。它是…

深入理解Netty及核心组件使用—上

目录 Netty的优势 为什么Netty使用NIO而不是AIO? Netty基本组件 Bootstrap、EventLoop(Group) 、Channel 事件和 ChannelHandler、ChannelPipeline ChannelFuture Netty入门程序 服务端代码 客户端代码 运行结果 Netty的优势 1. API 使用简单&#xff0c…

docker部署showdoc

目录 安装 1.拉取镜像 2.创建容器 使用 1.选择语言 2.默认账户/密码:showdoc/123456​编辑 3.登陆 4.首页 安装 1.拉取镜像 docker pull star7th/showdoc 2.创建容器 mkdir -p /opt/showdoc/html docker run -d --name showdoc --userroot --privilegedtrue -p 1005…

RocketMQ事务消息

事务消息 应用场景: ​ 事务消息是RocketMQ非常有特色的一个高级功能。他的基础诉求是通过RocketMQ的事务机制,来保证上下游的数据一致性。 ​ 以电商为例,用户支付订单这一核心操作的同时会涉及到下游物流发货、积分变更、购…

黄金交易策略:手工同向单减保留仓

虽然保留仓的仓位不大,扛个一年半载不是问题,但闲着也可以手工处理掉(10000点以内的不要处理)。挑一个最大的单,同向相同的手数,并把两单的止盈设置中位数(也没有这么严格,差不多就好…