【数据结构】—— 线索二叉树

引入

我们现在提倡节约型杜会, 一切都应该节约为本。对待我们的程序当然也不例外,能不浪费的时间或空间,都应该考虑节省。我们再观察团下图的二叉树(链式存储结构),会发现指针域并不是都充分的利用了,有许多的空指针域的存在,这实在不是好现象,应该要想办法利用起来。通常,对于这种情况的常用做法就是使其存储某些数据,方便某种操作!

 举个我们之前学习单链表的时候,我们在学习单链表的时候,采用的是带头结点的链式存储结构。通常,头结点的数据域是不存储数据的,但是,我们可以令其存储当前链表的长度。因为单链表的特殊性!当我们需要知道链表的长度时,就需要从头到尾遍历链表,时间复杂度为O(n),但是,如果我们在头结点的数据域存储当前链表的长度,就可以使上述操作的时间复杂度变成O(1)。

回到二叉树的讨论中,首先我们要来看着这空指针有多少个呢?对于一个有n个结点的二叉树,每个结点有指向左右孩子的两个指针域,所以一共是 2n 个指针域。而 n个结点的二叉树一共有 n -1条分支线数,也就是说,其实是存在 2n  - (n -1)  = n + 1个空指针域。如上图二叉树的结构示意图所示:共有10个结点,11个空指针域。这些空间不存储任何事物,白白的浪费着内存的资源。那对于二叉树该如何利用这些空间资源!

线索二叉树的定义

        回想之前讲述的二叉树的遍历(前、中、后、层序遍历)。我们在做遍历时,比如上图二叉树的结构示意图做中序遍历时,得到了H-D-I-B-J-E-A-F-C-G 这样的字符序列,遍历过后,我们可以知道,结点I的前驱结点是D,后继结点是B,结点F的前驱结点是A,后继结点是C。也就是说,我们可以很清楚的知道任意一个结点,它的前驱和后继是哪一个。可是这是建立在已经遍历过的基础之上的。在二叉链表上,我们只能知道每个结点指向其左右孩子结点的地址,而不知道某个结点的前驱是谁,后继是谁。要想知道,必须遍历一次。以后每次需要知道时,都必须先遍历一次。为什么不考虑在创建时就记住这些前驱和后继呢,那将是多大的时间上的节省。

        综合刚才两个角度的分析后,我们可以考虑利用那些空地址,存放指向结点在某种遍历次序下的前驱和后继结点的地址。就好像 GPS 导航仪一样,我们开车的时候,哪怕我们对具体目的地的位置一无所知,但它每次都可以告诉我从当前位置的下一步应该走向哪里。这就是我们现在要研究的问题。

我们把这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树('Threaded Binary Tree) 。

我们把这棵二叉树进行中序遍历后:1、将这棵二叉树的所有空指针域中的lchild,改为指向当前结点的前驱。因此H的前驱是NULL(图中),I的前驱结点是D(图中),J的前驱结点是B(图中③),F的前驱结点是A(图中④),G的前驱结点是C(图中⑤)。一共5个空指针域被利用。2、将所有的空指针域中的rchild,改为指向它的后继结点。于是我们就可以通过指针知道H的后继结点是D,I的后继结点是 B,J的后继结点是E,E的后继结点是A,F的后继结点是C,G的后继结点因为不存在而指向 NULL。此时共有6个空指针域被利用。总共加起来11个空指针域全部利用!

 通过上图的右图所示(空心箭头为前驱结点,实心黑箭头为后继结点),就更容易看出,其实线索二叉树,等于是把一棵二叉树转变成了一个双向链表,这样对我们的插入删除结点、查找某个结点都带来了方便。所以我们对二叉树以某种次序遍历使其变为线索二叉树的过程称做是线索化

线索二叉树的结构实现

结点结构实现

现在我们已经知道了线索二叉树的概念和实现的思路,但问题并没有彻底解决。我们如何知道某一结点的lchild 是指向它的左孩子还是指向前驱结点?rchild 是指向右孩子还是指向后继结点?比如E结点的lchild是指向它的左孩子,该结点的rchild 却是指向它的后继结点A。显然我们在决定lchild 是指向左孩子还是前驱结点,rchild 是指向右孩子还是后继上是需要一个区分标志的。因此,我们在每个结点再增设两个标志域lTag和rTag。(注意;lTag和rTag的类型,没有明确的限制!按照自己的编程习惯来。通常使用0、1这种布尔值类型。)

关于线索二叉树的结点定义如下:(C/C++混编)

using TBTree_Type = int;
typedef struct TBNode
{
	TBTree_Type val;
	struct TBNode* lChild;
	struct TBNode* rChild;
	PointerTag lTag;
	PointerTag rTag;
	
}TBNode;

 成员变量解释:

  1. lTag为0时指向该结点的左孩子,为1时指向该结点的前驱结点。
  2. rTag为0时指向该结点的右孩子,为1时指向该结点的后继结点。

其中关于PointerTag的类型,其实是对枚举类型的封装而已!

typedef enum
{ 
	Link,  //Link = 0
	Thread //Thread = 1
} PointerTag;

因此对于图6-10-1的二叉链表图可以修改为下图所示的样子

 线索化

        现在,关于线索二叉树的结点结构已经给出!那么假设现在给你一颗二叉树的根结点,那么你要怎么将其变成线索二叉树?我们之前已经提过,其实线索二叉树,等于是把一棵二叉树转变成了一个双向链表,而我们对二叉树以某种次序遍历使其变为线索二叉树的过程称做是线索化

线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索。由于前驱和后继结点的信息只有在遍历该二叉树时才能得到,所以线索化的过程就是在遍历的过程结点中修改空指针的过程。

 现在以中序遍历线索化为例!总体的实现思路如下:结点空指针域前驱的填入,需要知道刚刚访问的结点,后继的填入,需要知道下一个访问的结点,所以可以用一个 pre 指针专门用于记录刚刚访问的结点(也就是现在访问的结点的上一个结点)。总体实现思路如下:

  • 如果当前结点的左孩子为空,那么就将当前结点的左孩子指向prev,同时修改当前结点左孩子的标记,那么当前结点的前驱节点就存储完成
  • 后继结点的存储会比较复杂一点,需要判断prev的右孩子是否为空,如果为空,那么就将prev的右孩子指向当前结点,同时修改prev右孩子的标记

代码实现如下:

int inorder_thread(TBNode* root, TBNode*& prev)
{
	if (root == nullptr)
		return;
	inorder_thread(root->lChild, prev);
	if (root->lChild == nullptr)
	{
		root->lTag = Thread;
		root->lChild = prev;
	}
	if (prev != nullptr && prev->rChild == nullptr)
	{
		prev->rTag = Thread;
		prev->rChild = root;
	}
	prev = root;
	inorder_thread(root->rChild, prev);
}

形参解释:

  1. root:二叉树的根结点
  2. prev:指向刚刚访问过的结点(也就是现在访问的结点的上一个结点)

【注意】:关于prev指向谁,如果不理解一定要去画图理解!

如果有学过C++的话,这份代码没有什么难度!没学过的话,我重点提一下形参的TBNode*& prev,‘&’代表引用,就是给变量起别名。不理解也没关系!直接用二级指针TBNode** prev替代、或者使用全局变量。

 我们还是来演示一段代码的执行逻辑。从二叉树的根结点开始,此时prev指向空指针(NULL),因为,当前一个结点都还没有访问;接下来直接递归根结点的左子树(中序遍历的次序),找到中序遍历的第一个结点‘H’,此时的root指向结点为‘H’的结点,prev还是为NULL,判断root的左孩子是否为空,如果为空,那么就将root的左孩子指向prev(root->lChild = prev),再将左孩子的结点的标记lTag变成1(root->lTag = Thread),只是当前结点的前驱结点就存储完成了,

 由于当前prev为空指针域,因为中序遍历的第一个访问的结点为'H‘,没有比它更早访问的了。接下来将prev指向root,因外要去遍历当前结点root的右子树,因为root的右子树为空,直接回到结点'D',这时,prev的右孩子为空,那么就将prev的右孩子指向当前结点root (prev->rChild = root),同时修改prev的右孩子的标记(prev->rTag = Thread),再将prev指向当前结点root,因为下一步要去遍历root的右子树了。

 不画了!剩下的步骤一样,只要注意prev的改动即可。

遍历操作

        有了线索二叉树后,我们对它进行遍历时发现,其实就等于是操作一个双向链表结构。
和双向链表结构一样,在二叉树线索链表上添加一个头结点,如下图所示,并令其 lchild 域的指针指向二叉树的根结点(图中的①),其rchild 域的指针指向中序遍历时访问的最后一个结点(图中的②)。反之,令二叉树的中序序列中的第一个结点中,Ichild 域指针和最后一个结点的rchild 域指针均指向头结点(图中的③和④)。这样定义的好处就是我们既可以从第一个结点起顺后继结点进行遍历,也可以从最后一个结点起顺前驱结点进行遍历。

 代码实现如下:

void thread_inorder(TBNode* head)
{
	TBNode* root = head->lChild;//根结点
	while (root != head)
	{
		while (root->lTag == Link)//查找遍历的第一个结点
		{
			root = root->lChild;
		}
		while (root->rTag == Thread && root->rChild != head)
		{
			printf("%c ", root->val);
			root = root->rChild;
		}
		root = root->rChild; //root进入它的右子树
	}
}

从这段代码也可以看出,它等于是一个双向链表的扫描,所以时间复杂度为 O(n)。由于它充分利用了空指针域的空间(这等于节省了空间),又保证了创建时的一次遍历就可以终生受用前驱后继的信息(这意味着节省了时间)。所以在实际问题中,如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择。(说实话,我几乎就没有用过线索二叉树!话又说回来,你可以不用,但是不能不会。可以作为一种拓展知识吧。)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

NVR管理平台EasyNVR多个NVR同时管理:全方位安防监控视频融合云平台方案

EasyNVR是基于端-边-云一体化架构的安防监控视频融合云平台,具有简单轻量的部署方式与多样的功能,支持多种协议(如GB28181、RTSP、Onvif、RTMP)和设备类型(IPC、NVR等),提供视频直播、录像、回放…

虚幻引擎---初识篇

一、学习途径 虚幻引擎官方文档:https://dev.epicgames.com/documentation/zh-cn/unreal-engine/unreal-engine-5-5-documentation虚幻引擎在线学习平台:https://dev.epicgames.com/community/unreal-engine/learning哔哩哔哩:https://www.b…

汽车HiL测试:利用TS-GNSS模拟器掌握硬件性能的仿真艺术

一、汽车HiL测试的概念 硬件在环(Hardware-in-the-Loop,简称HiL)仿真测试,是模型基于设计(Model-Based Design,简称MBD)验证流程中的一个关键环节。该步骤至关重要,因为它整合了实际…

C++编程库与框架实战——sqlite3数据库

一,SQLite数据库简介 SQLite是可以实现类似于关系型数据库中各种操作的事务性SQL数据库引擎。 SQLite可以为应用程序提供存储于本地的嵌入式数据库,帮助应用程序实现轻量级的数据存储。 SQLite是一个库文件,并不是单独的进程,它可以静态或动态链接到C++应用程序中,然后…

STM32F10x 定时器

使用定时器实现:B5 E5的开关 添加相关的.h路径文件 添加相关的.c配置文件 led.h文件 用于声明LED函数 #ifndef __LED_H //没有定义__LED_H #define __LED_H //就定义__LED_H #define LED1_ON GPIO_ResetBits(GPIOB,GPIO_Pin_5) #defi…

PyQt6+pyqtgraph折线图绘制显示

1、实现效果 2、环境: 确认已经安装pyqtgraph的模块,如果没有安装,使用命令安装: pip install pyqtgraph 3、代码实现: 绘制折线函数: import sys import random from PySide6.QtWidgets import QAppl…

Linux---ps命令

​​​​​​Linux ps 命令 | 菜鸟教程 (runoob.com) process status 用于显示进程的状态 USER: 用户名,运行此进程的用户名。PID: 进程ID(Process ID),每个进程的唯一标识号%CPU: 进程当前使用的CPU百分比%MEM: 进程当前使用的…

高新技术行业中的知识管理:关键性、挑战、策略及工具应用

知识管理的关键性 在瞬息万变的信息时代,知识已成为高新技术行业的核心竞争要素。知识管理,这一旨在高效组织、整合并应用企业内外部知识资源的管理策略,对于推动高新技术企业的持续创新与发展至关重要。它不仅能够激发研发团队的创造力&…

IDEA 2024安装指南(含安装包以及使用说明 cannot collect jvm options 问题 四)

汉化 setting 中选择插件 完成 安装出现问题 1.可能是因为之前下载过的idea,找到连接中 文件,卸载即可。

【MyBatis】全局配置文件—mybatis.xml 创建xml模板

文章目录 模板文件配置元素typeAliasessettings 模板文件 创建模板 按照顺序打开【File】–>【settings】–>【Editor】–>【File and Code Templates】&#xff08;或直接搜索&#xff09; <?xml version"1.0" encoding"UTF-8" ?> <…

uni-app 发布媒介功能(自由选择媒介类型的内容) 设计

1.首先明确需求 我想做一个可以选择媒介的内容&#xff0c;来进行发布媒介的功能 &#xff08;媒介包含&#xff1a;图片、文本、视频&#xff09; 2.原型设计 发布-编辑界面 通过点击下方的加号&#xff0c;可以自由选择添加的媒介类型 但是因为预览中无法看到视频的效果&…

【Go】-go中的锁机制

目录 一、锁的基础知识 1. 互斥量/互斥锁 2. CAS&#xff08;compare and swap&#xff09; 3. 自旋锁 4. 读写锁 5. 乐观锁 & 悲观锁 6. 死锁 二、go中锁机制 1. Mutex-互斥锁 2. RWMutex-读写锁 2.1 RWMutex流程概览 2.2 写锁饥饿问题 2.3. golang的读写锁源…

Python 使用 Selenuim进行自动化点击入门,谷歌驱动,以百度为例

一、首先要下载谷歌驱动 1.&#xff08;打开谷歌浏览器 - 设置 - 关于谷歌&#xff0c;查看谷歌浏览器版本&#xff0c;否则不对应无法调用&#xff0c;会提示&#xff1a;selenium.common.exceptions.SessionNotCreatedException: Message: session not created: This versio…

RCVS:A Unifed Registration and FusionFramework for Video Streams 译文

摘要:红外与可见光的跨模态配准与融合可以生成更全面的目标和场景信息表示。以前的框架主要关注于解决模态差异以及保留不同模态信息对不同静态图像对之间配准和融合任务性能的影响。然而&#xff0c;这些框架忽略了在现实世界设备上的实际部署&#xff0c;特别是在视频流的背景…

JDBC编程---Java

目录 一、数据库编程的前置 二、Java的数据库编程----JDBC 1.概念 2.JDBC编程的优点 三.导入MySQL驱动包 四、JDBC编程的实战 1.创造数据源&#xff0c;并设置数据库所在的位置&#xff0c;三条固定写法 2.建立和数据库服务器之间的连接&#xff0c;连接好了后&#xff…

Python 抓取笑话内容并存入 CSV

在互联网上&#xff0c;有许多有趣的内容等待我们去挖掘和收集。今天&#xff0c;我们就来深入了解一段 Python 代码&#xff0c;它能够帮助我们从指定网站抓取笑话内容&#xff0c;并将其整理保存为 CSV 文件&#xff0c;方便后续查看和分析。 结果展示&#xff08;文末附完整…

Redis-09 SpringBoot集成Redis

Jedis 和 lettuce 基本已经过时 集成RedisTemplate 单机 1.建 Modul 2.改pom <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instanc…

Linux:自定义Shell

本文旨在通过自己完成一个简单的Shell来帮助理解命令行Shell这个程序。 目录 一、输出“提示” 二、获取输入 三、切割字符串 四、执行指令 1.子进程替换 2.内建指令 一、输出“提示” 这个项目基于虚拟机Ubuntu22.04.5实现。 打开终端界面如图所示。 其中。 之前&#x…

夜天之书 #104 开源软件有断供的风险吗?

近期&#xff0c;Linux 上游因为受美国出口管制条例的影响&#xff0c;将移除部分开发者的 MAINTAINER 权限&#xff0c;引起了新一轮对开源依赖的重新评估。 关于其中开源精神和社群治理的讨论&#xff0c;卫 Sir 的两篇文章已经讨论得比较清楚&#xff08;见尾注&#xff09;…

tensorforce(dqn框架)安装

win7 64位操作系统 python版本&#xff1a;3.8.10 pip install tensorflow 默认的tensorflow的版本是2.31.0&#xff0c;安装tensorforce后自动升级到3.6.0 tensorflow:升级到3.6.0 keras&#xff1a;升级到3.6.0 tensorforce安装 pip3 install tensorforce protobuf 需要降到…