数据结构 - 线索树

news2025/1/20 3:55:32

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

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

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

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

相关文章

Web前端框架-Vue(初识)

文章目录 web前端三大主流框架**1.Angular****2.React****3.Vue**什么是Vue.js 为什么要学习流行框架框架和库和插件的区别一.简介指令v-cloakv-textv-htmlv-pre**v-once**v-onv-on事件函数中传入参数事件修饰符双向数据绑定v-model 按键修饰符自定义按键修饰符别名v-bind(属性…

速过计算机二级python——第9讲 详解第 2 套真题

第9讲 详解第 2 套真题 基本编程题【15 分】简单应用题【25 分】综合应用题【20 分】**问题 1**【10 分】:**问题 2【10 分】:**基本编程题【15 分】 考生文件夹下存在一个文件 PY101.py,请写代码替换横线,不修改其他代码,实现以下功能:【5 分】题目: import __________ b…

【数据结构与算法】力扣刷题记之 稀疏数组

🎉🎉欢迎光临🎉🎉 🏅我是苏泽,一位对技术充满热情的探索者和分享者。🚀🚀 🌟特别推荐给大家我的最新专栏《数据结构与算法:初学者入门指南》📘&am…

手把手教你开发Python桌面应用-PyQt6图书管理系统-图书添加模块UI设计实现

锋哥原创的PyQt6图书管理系统视频教程: PyQt6图书管理系统视频教程 Python桌面开发 Python入门级项目实战 (无废话版) 火爆连载更新中~_哔哩哔哩_bilibiliPyQt6图书管理系统视频教程 Python桌面开发 Python入门级项目实战 (无废话版) 火爆连载更新中~共计24条视频&…

ClickHouse的优缺点和应用场景

当业务场景需要一个大批量、快速的、可支持聚合运算的数据库,那么可选择ClickHouse。 选择ClickHouse 的原因: 记录类型类似于LOG,读取、运算远远大于写入操作选取有限列,对近千万条数据,快算的运算出结果。数据批量…

LiteFlow规则引擎框架

LiteFlow规则引擎框架 Hi,我是阿昌,今天介绍一个规则引擎框架,LiteFlow; 一、前言 那首先得知道什么是规则引擎?规则引擎是 一种用于自动化处理业务规则的软件组件。 在软件行业中,规则引擎通常用于解决…

HTML+CSS+JS网页设计

文章目录 作品介绍一、代码演示1.登录、注册,获取当前时间2.轮播图3.家乡简介4.热门景点5.特色美食6.页尾 二、效果图总结 作品介绍 HTML页面主要由:登录、注册跳转页面,轮播图,家乡简介,热门景点,特色美食…

js中new操作符详解

文章目录 一、是什么二、流程三、手写new操作符 一、是什么 在JavaScript中,new操作符用于创建一个给定构造函数的实例对象 例子 function Person(name, age){this.name name;this.age age; } Person.prototype.sayName function () {console.log(this.name) …

NOVATEK显示技术系列之CEDSCHPI Training差异简介

CEDS的数据封包格式:首先CEDS数据封包包括三个部分: Training Pattern即Phase1Control Data 即 Phase2RGB Data 即Phase3 Power on Timing: 工作原理: Power ON时,TCON会发Training Pattern,当COF接受Tr…

springboot-web服务迁移Kubernetes

1、搞定基础镜像 docker pull openjdk:8-jre-alpine docker tag openjdk:8-jre-alpine 10.204.82.15/kubernetes/openjdk:8-jre-alpine docker push 10.204.82.15/kubernetes/openjdk:8-jre-alpine 2、springboot-web应用服务打包 3、编写Dockerfile构建镜像 FROM 10.204.82.…

Vue源码系列讲解——虚拟DOM篇【二】(Vue中的DOM-Diff)

目录 1. 前言 2. patch 3. 创建节点 4. 删除节点 5. 更新节点 6. 总结 1. 前言 在上一篇文章介绍VNode的时候我们说了,VNode最大的用途就是在数据变化前后生成真实DOM对应的虚拟DOM节点,然后就可以对比新旧两份VNode,找出差异所在&…

uTools工具使用

之前发现一款非常有用的小工具,叫uTools,该软件集成了比如进制转换、json格式化、markdown、翻译、取色等等集插件大成,插件市场提供了很多开源插件工具。可以帮助开发人员节省了寻找各种处理工具的时间,非常推荐。 1、软件官方下…

横扫Spark之 - 22个常见的转换算子

水善利万物而不争,处众人之所恶,故几于道💦 文章目录 1. map()2. flatMap()3. filter()4. mapPartitions()5. mapPartitionsWithIndex()6. groupBy()7. distinct()8. coalesce()9. repartition()10. sortBy()11. intersection()12.union()13.…

Python实现文本情感分析

前言 文本情感分析是一种重要的自然语言处理(NLP)任务,旨在从文本数据中推断出情感信息,例如正面、负面或中性情感。它在社交媒体分析、产品评论、市场调研等领域都有广泛的应用。本文将详细介绍如何使用Python进行文本情感分析,包括基础概念…

07 A B 从计数器到可控线性序列机

07. A.从计数器到可控线性序列机 让LED灯按照亮0.25秒。灭0.75秒的状态循环亮灭让LED灯按照亮0.25秒,灭0.5秒,亮0.75秒,灭1秒的状态循环亮灭让LED灯按照指定的亮灭模式亮灭,亮灭模式未知,由用户随即指定。以0.25秒为一…

前端实现支付跳转以及回跳

// 支付地址 const baseURL http://pcapi-xiaotuxian-front-devtest.itheima.net/ const backURL http://127.0.0.1:5173/paycallback const redirectUrl encodeURIComponent(backURL) const payUrl ${baseURL}pay/aliPay?orderId${route.query.id}&redirect${redirec…

js-添加网页快捷方式

title: js-添加网页快捷方式 categories: Javascript tags: [p快捷方式] date: 2024-02-04 15:28:25 comments: false mathjax: true toc: true js-添加网页快捷方式 前篇 谷歌上包困难的情况, 只能通过投放落地页来缓解一下痛苦, web2app 那种形式有几个比较大的缺点就是需要…

江科大STM32 终

目录 SPI协议10.1 SPI简介W25Q64简介10.3 SPI软件读写W25Q6410.4 SPI硬件外设读写W25Q64 BKP备份寄存器、PER电源控制器、RTC实时时钟11.0 Unix时间戳代码示例:读写备份寄存器BKP11.2 RTC实时时钟 十二、PWR电源控制12.1 PWR简介代码示例:修改主频12.3 串…

奇瑞汽车,好好卖车,别趟个人是非的浑水

文 | AUTO芯球 作者 | 雷歌 这下,奇瑞法务部忙都忙不过来了。 一个字,就是,告!告!告! 刚投诉完这家,又去告那家。 可是骂奇瑞的实在太多了,告不完,根本告不完。 有骂…

一起玩儿Proteus仿真(C51)——04. 直流电机的启停、加减速和正反转仿真(L298)(二)

摘要:本文介绍PWM信号的产生办法和直流电机的启停、加减速和正反转的仿真程序的编写方法 前边已经介绍了2中生成PWM信号的方法了。那么怎样才能节省一下资源,只使用一个定时器呢?这就是介绍的第三种方法,单定时器中断法生成PWM信号…