当前位置: 首页 > news >正文

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

引入

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

 举个我们之前学习单链表的时候,我们在学习单链表的时候,采用的是带头结点的链式存储结构。通常,头结点的数据域是不存储数据的,但是,我们可以令其存储当前链表的长度。因为单链表的特殊性!当我们需要知道链表的长度时,就需要从头到尾遍历链表,时间复杂度为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 = 0Thread //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)。由于它充分利用了空指针域的空间(这等于节省了空间),又保证了创建时的一次遍历就可以终生受用前驱后继的信息(这意味着节省了时间)。所以在实际问题中,如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择。(说实话,我几乎就没有用过线索二叉树!话又说回来,你可以不用,但是不能不会。可以作为一种拓展知识吧。)

相关文章:

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

引入 我们现在提倡节约型杜会, 一切都应该节约为本。对待我们的程序当然也不例外,能不浪费的时间或空间,都应该考虑节省。我们再观察团下图的二叉树(链式存储结构),会发现指针域并不是都充分的利用了,有许…...

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

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

How to update the content of one column in Mysql

How to update the content of one column in Mysql by another column name? UPDATE egg.eggs_record SET sold 2024-11-21 WHERE id 3 OR id 4;UPDATE egg.eggs_record SET egg_name duck egg WHERE id 2;...

URL在线编码解码- 加菲工具

URL在线编码解码 打开网站 加菲工具 选择“URL编码解码” 输入需要编码/解码的内容,点击“编码”/“解码”按钮 编码: 解码: 复制已经编码/解码后的内容。...

Python3 爬虫 Scrapy的安装

Scrapy是基于Python的分布式爬虫框架。使用它可以非常方便地实现分布式爬虫。Scrapy高度灵活,能够实现功能的自由拓展,让爬虫可以应对各种网站情况。同时,Scrapy封装了爬虫的很多实现细节,所以可以让开发者把更多的精力放在数据的…...

QT中QString类的各种使用

大部分的QString使用可以参考:QT中QString 类的使用--获取指定字符位置、截取子字符串等_qstring 取子串-CSDN博客 补充一种QString类的分离:Qt QString切割(Split()与Mid()函数详解)_qstring split-CSDN博客 1. Trimmed和Simplified函数(去除空白) trimmed:去除了…...

linux 网络安全不完全笔记

一、安装Centos 二、Linux网络网络环境设置 a.配置linux与客户机相连通 b.配置linux上网 三、Yum详解 yum 的基本操作 a.使用 yum 安装新软件 yum install –y Software b.使用 yum 更新软件 yum update –y Software c.使用 yum 移除软件 yum remove –y Software d.使用 yum …...

uniapp将图片url转换成base64支持app和h5

uniapp将图片url转换成base64支持app和h5 imageToBase64支持app和h5, app内使用plus.io.resolveLocalFileSystemURL方法转换 h5内使用uni.request方法转换 // 图片转base64 export const imageToBase64 (path) > {// #ifdef APP-PLUSreturn new Promise((resolve, rejec…...

odoo17 档案管理之翻译2

翻译格式:#: model_terms:对象名称,arch_db:模块名.xml_id #. module: dms #: model_terms:ir.ui.view,arch_db:dms.view_dms_directory_kanban #: model_terms:ir.ui.view,arch_db:dms.view_dms_file_kanban #: model_terms:ir.ui.view,arch_db:dms.view_dms_tag_…...

风尚云网前端学习:制作一款简易的在线计算器

风尚云网前端学习:制作一款简易的在线计算器 简介 在前端开发的学习过程中,实现一个简单的在线计算器是一个常见的练习项目。它不仅能够帮助我们熟悉HTML、CSS和JavaScript的基本用法,还能够加深我们对事件处理和DOM操作的理解。今天&#…...

Android蓝牙架构,源文件目录/编译方式学习

Android 版本 发布时间 代号(Codename) Android 1.0 2008年9月23日 无 Android 1.1 2009年2月9日 Petit Four Android 1.5 2009年4月27日 Cupcake Android 1.6 2009年9月15日 Donut Android 2.0 2009年10月26日 Eclair Android 2.1 2…...

ubuntu中使用ffmpeg和nginx推流rtmp视频

最近在测试ffmpeg推流rtmp视频,单独安装ffmpeg是无法完成推流的,需要一个流媒体服务器,常用nginx,可以直接在ubuntu虚拟机里面测试一下。 测试过程不涉及编译ffmpeg和nginx,仅使用基本功能: 1 安装ffmpeg …...

strongswan测试流程

测试shell脚本文件testing/do-tests,测试配置文件testing/testing.conf。do-tests脚本不加参数,将依次执行testing/tests/目录下的所有测试用例。do-tests脚本有两个参数-v和-t,前者在测试中记录详细信息,后者在输出信息中增加时间…...

[CKS] CIS基准测试,修复kubelet和etcd不安全项

目前的所有题目为2024年10月后更新的最新题库,考试的k8s版本为1.31.1 ​ 专栏其他文章: [CKS] K8S Admission Set Up[CKS] CIS基准测试,修复kubelet和etcd不安全项[CKS] K8S NetworkPolicy Set Up[CKS] 利用Trivy对image进行扫描[CKS] 利用falco进行容器…...

Linux/Windows/OSX 上面应用程序重新启动运行。

1、Linux/OSX 上面重新运行程序,直接使用 execvp 函数就可以了,把main 函数传递来的 argv 二维数组(命令行参数)传进去就可以,注意不要在 fork 出来的子进程搞。 2、Windows 平台可以通过 CreateProcess 函数来创建新的…...

React拆分组件中的传值问题

在我们实际项目开发中,很多时候为为了项目后期便于维护,都会将相关的组件进行拆分,拆分过后,会将数据方法在父组件中进行编写,然后将一些逻辑拆分为组件,在这个过程中,最重要的就是数据的传递&a…...

RocketMQ的使⽤

初识MQ 1.1.同步和异步通讯 微服务间通讯有同步和异步两种⽅式: 同步通讯:就像打电话,需要实时响应。 异步通讯:就像发邮件,不需要⻢上回复。 两种⽅式各有优劣,打电话可以⽴即得到响应,但…...

Android Studio 设置不显示 build-tool 无法下载

2024版本查看build-tool版本 File -> Settings -> Languages & Frameworks -> Android SDK 或者直接打开Settings后搜索“SDK” 解决方案 将 Android Studio 升级到2022.2.1以上的版本将 C:/Windows/System32/drivers/etc/hosts 文件用管理员身份打开&#xff0c…...

【Y20030007】基于java+servlet+mysql的垃圾分类网站的设计与实现(附源码 配置 文档)

网垃圾分类网站的设计与实现 1.摘要2.开发目的和意义3.系统功能设计4.系统界面截图5.源码获取 1.摘要 随着全球环境保护意识的提升,垃圾分类已成为一项紧迫且重要的任务。为了有效推动垃圾分类的实施,提升公众的环保意识和参与度,垃圾分类已…...

细说敏捷:敏捷四会之standup meeting

上一篇文章中,我们讨论了 敏捷四会 中 冲刺计划会 的实施要点,本篇我们继续分享敏捷四会中实施最频繁,团队最容易实施但往往也最容易走形的第二个会议:每日站会 关于每日站会的误区 站会是一个比较有标志性的仪式活动&#xff0…...

ThinkPHP8使用workerman

应用场景说明:通过建立通信,不同用户进行消息推送或数据更新,因为本身需要作为服务端进行主动消息推送,因此使用Gateway方式,如果不需要的可以不采用这种形式,以下内容仅为参考,具体业务场景&am…...

C语言超详细教程

系列文章目录 文章目录 系列文章目录1 运算符1.1 算术运算符:2 控制语句2.1 条件语句:2.2 循环语句:3 函数3.1 函数的定义与声明:3.2 递归函数:4 指针4.1 指针的定义与使用函数指针:5. 数组与字符串5.1 数组一维数组:相同类型元素的集合(如:多维数组:数组的数组(如:…...

[开源]3K+ star!微软Office的平替工具,跨平台,超赞!

大家好,我是JavaCodexPro! 数字化的当下,高效的办公工具是提升工作效率的关键,然而大家想到的一定是 Microsoft Office 办公软件,然而价格也是相当具有贵的性价比。 今天JavaCodexPro给大家分享一款超棒的开源办公套…...

如何借助计算机视觉算法通过识别水尺精准识别水位

如何借助计算机视觉算法通过识别水尺精准识别水位 随着技术的发展,计算机视觉在多个领域得到了广泛的应用,尤其是在环境监测方面。本文将介绍一种利用计算机视觉算法通过识别水尺来精准识别水位的方法。这种方法可以用于河流、水库等场景的水位监测&…...

C++(进阶) 第1章 继承

C(进阶) 第1章 继承 文章目录 前言一、继承1.什么是继承2.继承的使用 二、继承方式1.private成员变量的(3种继承方式)继承2. private继承方式3.继承基类成员访问⽅式的变化 三、基类和派生类间的转换1.切片 四、 继承中的作⽤域1.隐藏规则&am…...

获国家权威机构认可 亚信安全荣获CNVD技术组支撑单位认证

近日,国家信息安全漏洞共享平台(CNVD)依据《CNVD管理办法》及《CNVD支撑单位能力要求》,对申请加入考察期的单位进行了全面而严格的能力评估。经过层层筛选与审核,亚信安全凭借卓越的技术实力与专业的服务能力&#xf…...

2. Autogen官网教程 (Terminating Conversations Between Agents)

在这一章中,我们将探讨如何结束自动生成代理之间的对话。 导入必要的库 import osfrom autogen import ConversableAgent配置智能体 我们需要配置智能体使用的语言模型(LLM)。以下是一个配置示例: llm_config {"config_…...

java 排序 详解

Java 提供了多种方式对数据进行排序,包括数组和集合的排序。排序在日常开发中非常常见,以下将从排序算法的基本原理、Java 中的内置排序方法以及自定义排序三方面进行详解。 1. 排序的基本概念 排序是将一组数据按特定顺序排列的过程,常见顺…...

【数据集】城市通量塔站点观测数据

【数据集】城市通量塔站点观测数据 数据概述数据下载参考数据概述 数据集简介:Harmonized gap-filled dataset from 20 urban flux tower sites 数据集名称:Harmonized gap-filled dataset from 20 urban flux tower sites (用于 Urban-PLUMBER 项目的 20 个城市通量塔站点…...

scau编译原理综合性实验

一、题目要求 题目: 选择部分C语言的语法成分,设计其词法分析程序、语法语义分析程序。 要求: 设计并实现一个一遍扫描的词法语法语义分析程序,将部分C语言的语法成分(包含赋值语句、if语句、while循环语句&#xf…...