408数据结构-线索二叉树 自学知识点整理

前置知识:二叉树的概念、性质与存储结构


线索二叉树的基本概念

遍历二叉树是以一定的规则将二叉树中的结点排列成一个线性序列(如中序遍历序列),从而得到几种遍历序列,使得该序列中的每个结点(除了首尾两个结点外)都有一个直接前驱和直接后继。
(注:此处的前驱后继指的是遍历序列中的,并不是树的逻辑结构中的前驱后继。)

typedef struct ElemType {int value;
}ElemType;typedef struct BiTNode {//二叉树的结点(链式存储)ElemType data;struct BiTNode* lchild, * rchild;
}BiTNode, * BiTree;

在传统的二叉链表中,仅能体现出结点之间的一种父子关系,并不能直接得到结点在遍历中的前驱或后继。在之前的内容里提到过:

由二叉链表的性质, n n n个结点的二叉链表共有 n + 1 n+1 n+1个空指针域,这一部分可用于构造线索二叉树
(计算方法:度为 2 2 2的结点有 0 0 0个空指针域,度为 1 1 1的结点有 1 1 1个空指针域,度为 0 0 0的结点有 2 2 2个空指针域。由此前的性质可知,当 n 1 = 1 n_1=1 n1=1时, n 0 = n / 2 n_0=n/2 n0=n/2,空指针域数量为 1 + 2 ∗ n / 2 = n + 1 1+2*n/2=n+1 1+2n/2=n+1;当 n 1 = 0 n_1=0 n1=0时, n 0 = ( n + 1 ) / 2 n_0=(n+1)/2 n0=(n+1)/2,空指针域数量为 0 + 2 ∗ ( n + 1 ) / 2 = n + 1 0+2*(n+1)/2=n+1 0+2(n+1)/2=n+1。故无论 n n n的值为奇数还是偶数, n n n个结点的二叉链表的空指针域都为 n + 1 n+1 n+1个)

因此,可规定:对一个结点,若其无左子树,则令 l c h i l d lchild lchild指针指向其前驱节点;若无右子树,则令 r c h i l d rchild rchild指针指向其后继结点。为此,还需增加两个标志域,以标识指针域指向其左(右)孩子或前驱(后继)。

typedef struct ThreadNode {//线索二叉树结点ElemType data;struct ThreadNode* lchild, * rchild;bool ltag = false, rtag = false;//左、右线索标志//当tag值为0的时候,表示指针指向孩子//而tag值为1的时候,表示指针指向线索
}ThreadNode, * ThreadTree;

以这种结点结构构成的二叉链表作为二叉树的存储结构,称为线索链表,其中指向结点的前驱和后继的指针称为线索。加上线索的二叉树称为线索二叉树。它的作用是方便从一个指定结点出发,找到其前驱、后继;方便遍历。
无论根据先序序列、中序序列还是后序序列,定义线索二叉树结点的方法都是一样的,区别在于三种序列中的前驱和后继不同,因此仅在二叉树线索化时算法设计有所不同。
对这一部分内容,408考研初试要求掌握能够手算画出线索二叉树——首先确定线索二叉树的类型(先序/中序/后序),再按照对应规则,确定各个结点的访问顺序并写上编号,最后将 n + 1 n+1 n+1空链域连上前驱和后继即可。


二叉树的线索化(前置知识:二叉树的遍历)

二叉树的线索化是将二叉链表中的空指针改为指向前驱或后继的线索。而前驱或后继的信息只有在遍历时才能得到,因此线索化的实质就是遍历一次二叉树
预处理如下:

ThreadNode* pre = NULL;//全局变量pre,指向当前访问节点的前驱

以中序线索二叉树的建立为例,增加一个全局 T h r e a d N o d e ∗ ThreadNode* ThreadNode类型的变量 p r e pre pre,用于指向当前访问结点 p p p的上一个访问的结点,即 p r e pre pre指向 p p p的前驱。在中序遍历的过程中,检查 p p p的左孩子指针是否为空,若为空,则将其指向 p r e pre pre,同时修改 l t a g ltag ltag的值为 1 1 1;同时检查检查 p r e pre pre的右孩子指针是否为空,若为空,则将其指向 p p p,同时修改 r t a g rtag rtag的值为 1 1 1
具体代码实现如下,使用递归算法:

void visit(ThreadNode* q) {if (q->lchild == NULL) {//左子树为空,建立前驱线索q->lchild = pre;q->ltag = true;}if (pre != NULL && pre->rchild == NULL) {pre->rchild = q;//建立前驱节点的后继线索pre->rtag = true;}pre = q;return;
}void InThread(ThreadTree T) {//中序遍历二叉树,并线索化if (T != NULL) {InThread(T->lchild);visit(T);InThread(T->rchild);}return;
}void CreateInThread(ThreadTree T) {//中序线索化二叉树Tpre = NULL;//pre初始为NULLif (T != NULL) {//若二叉树非空InThread(T);//中序遍历并线索化处理if (pre->rchild == NULL)pre->rtag = true;//对最后一个结点特殊处理}return;
}

先序线索二叉树和后序线索二叉树的实现与中序基本相同,唯一不同的是遍历时访问结点的顺序。
此外需特别注意,在先序遍历时,一定要在访问左子树前加上一个判断条件,若 l t a g = = 0 ltag==0 ltag==0才继续访问并处理左子树。否则,因为当前结点的左孩子指针指向其前驱结点,会出现递归中的死循环。而后序遍历则无需担心这个问题,因为在对一棵子树的处理中,它的根结点是最后才处理的。

void PreThread(ThreadTree T) {//先序遍历二叉树并线索化if (T != NULL) {visit(T);//先处理根节点if (!T->ltag)PreThread(T->lchild);//若左孩子不是线索才处理PreThread(T->rchild);}return;
}void PostThread(ThreadTree T) {//后序遍历二叉树并线索化if (T != NULL) {PostThread(T->lchild);PostThread(T->rchild);visit(T);//最后处理根结点//因此无需担心左右子树被线索化后可能产生“死循环”}return;
}

线索二叉树的遍历

中序线索二叉树的遍历

中序线索二叉树的结点中隐含了线索二叉树的前驱和后继信息。在对其进行遍历时,只需要先找到序列中的第一个结点,然后依次找结点的后继,直到其后继为空。

  1. 求中序线索二叉树的中序序列下的第一个结点
//找到以p为根的子树中第一个被中序遍历到的结点
ThreadNode* FirstNode(ThreadNode* p) {while (!p->ltag)p = p->lchild;//循环找到最左下角的结点(不一定是叶结点)return p;
}
  1. 求中序线索二叉树中结点 p p p在中序序列下的后继
//在中序线索二叉树中找到结点p的后继结点
ThreadNode* NextNode(ThreadNode* p) {if (p->rtag == 0)return FirstNode(p->rchild);//若右子树不为空,则返回右子树中第一个被中序遍历到的结点else return p->rchild;//否则直接返回右孩子(后继线索)
}
  1. 利用线索非递归地实现中序线索二叉树的中序遍历
void _Visit(ThreadNode* p) {cout << p->data.value << " ";return;
}
//利用线索实现对中序线索二叉树的中序遍历(非递归算法)
void InOrder(ThreadNode* T) {for (ThreadNode* p = FirstNode(T); p != NULL; p = NextNode(p))_Visit(p);return;
}

同理,对上续操作稍加修改,就能得到中序线索二叉树的逆中序遍历序列。

  1. 找到中序线索二叉树的中序遍历序列的最后一个结点
//找到以p为根的子树中最后一个被中序遍历到的结点
ThreadNode* LastNode(ThreadNode* p) {while (!p->rtag)p = p->rchild;//循环找到最右下角的结点(不一定是叶结点)return p;
}
  1. 找到中序线索二叉树中结点 p p p在中序序列中的前驱
//在中序线索二叉树中找到结点p的前驱结点
ThreadNode* PreNode(ThreadNode* p) {if (p->ltag == 0)return LastNode(p->lchild);//若左子树不为空,则返回左子树中最后一个被中序遍历到的结点else return p->lchild;//否则直接返回左孩子(前驱线索)
}
  1. 利用线索非递归地实现中序线索二叉树的逆中序遍历
//利用线索实现对中序线索二叉树的逆向中序遍历(非递归算法)
void RevInOrder(ThreadNode* T) {for (ThreadNode* p = LastNode(T); p != NULL; p = PreNode(p))_Visit(p);return;
}

完整代码可以看我的Github:传送门

先序线索二叉树(根左右)

1 ) 1) 1) 找先序后继 n e x t next next

若右子树为空, r c h i l d rchild rchild指向后继线索,即 p − > r t a g = = 1 p->rtag==1 p>rtag==1,则 n e x t = p − > r c h i l d next=p->rchild next=p>rchild
若右子树非空,即 p − > r t a g = = 0 p->rtag==0 p>rtag==0,此时需要分两种情况:
①若 p p p有左孩子,则 p p p的先序后继必为其左孩子;
②若 p p p没有左孩子,则 p p p的先序后继必为其右孩子。

2 ) 2) 2)找先序前驱 p r e pre pre

若左子树为空, l c h i l d lchild lchild指向后继线索,即 p − > l t a g = = 1 p->ltag==1 p>ltag==1,则 p r e = p − > l c h i l d pre=p->lchild pre=p>lchild
若左子树非空,即 p − > l t a g = = 0 p->ltag==0 p>ltag==0,此时需要进一步讨论:
因为先序遍历的规则是“根左右”,因此其左右子树中所有结点一定都是它的先序后继,不可能在当前结点的左右子树中找到它的先序前驱。
解决方案有两种,一种是对整棵先序线索二叉树从头开始再进行一次完整的先序遍历,找到当前结点的先序前驱;另一种是在构建二叉树时使用三叉链表,即给各个结点设置一个指向其父结点的指针。
以三叉链表为基础再进一步探讨,共分为 4 4 4种情况:
①能找到结点 p p p的父结点,且 p p p是左孩子。此时 p p p的父结点一定是它的先序前驱;
②能找到结点 p p p的父结点, p p p是右孩子,且 p p p的左兄弟为空。此时 p p p的父结点也一定是它的先序前驱;
③能找到结点 p p p的父结点, p p p是右孩子,且 p p p的左兄弟非空。此时 p p p的先序前驱一定是其左兄弟子树中,按照先序遍历的规则最后一个被访问到的结点(向下查找,优先向右,无右向左,直至叶结点);
④若不能找到结点 p p p的父结点,此时 p p p为根结点,没有先序前驱。

后序线索二叉树(左右根)

1 ) 1) 1) 找后序前驱 p r e pre pre

若左子树为空, l c h i l d lchild lchild指向前驱线索,即 p − > l t a g = = 1 p->ltag==1 p>ltag==1,则 p r e = p − > l c h i l d pre=p->lchild pre=p>lchild
若左子树非空,即 p − > l t a g = = 0 p->ltag==0 p>ltag==0,此时需要分两种情况:
①若 p p p有右孩子,则 p p p的后序前驱必为其右孩子;
②若 p p p没有右孩子,则 p p p的后序前驱必为其左孩子。

2 ) 2) 2)找后序后继 n e x t next next

若右子树为空, r c h i l d rchild rchild指向后继线索,即 p − > r t a g = = 1 p->rtag==1 p>rtag==1,则 n e x t = p − > r c h i l d next=p->rchild next=p>rchild
若右子树非空,即 p − > r t a g = = 0 p->rtag==0 p>rtag==0,此时需要进一步讨论:
因为后序遍历的规则是“左右根”,因此其左右子树中所有结点一定都是它的后序前驱,不可能在当前结点的左右子树中找到它的后序后继。
解决方案有两种,一种是对整棵先序线索二叉树从头开始再进行一次完整的后序遍历,找到当前结点的后序后继;另一种是使用三叉链表构建二叉树。
以三叉链表为基础再进一步探讨,共分为 4 4 4种情况:
①能找到结点 p p p的父结点,且 p p p是右孩子。此时 p p p的父结点一定是它的后序后继;
②能找到结点 p p p的父结点, p p p是左孩子,且 p p p的右兄弟为空。此时 p p p的父结点也一定是它的后序后继;
③能找到结点 p p p的父结点, p p p是左孩子,且 p p p的右兄弟非空。此时 p p p的后序后继一定是其右兄弟子树中,按照后序遍历的规则第一个被访问到的结点(向下查找,优先向左,无左向右,直至叶结点)(和找先序前驱正好相反);
④若不能找到结点 p p p的父结点,此时 p p p为根结点,没有后序后继。

对上述“八股文”无需背诵,只需理解其中过程与算法思想,考试时能够手动推算出来即可。
有兴趣的读者可以尝试自己写一写代码,我就不写了。


对线索二叉树,408初试中的高频考点有二叉树的线索化,或者在一个已经线索化的二叉树中找前驱和找后继,最常考的方式是手算。当然,代码编写也不难,无非就是对二叉树进行一次先序、中序、后序遍历,在其中对二叉树进行线索化处理。这一节的内容还是要结合课后习题多加巩固练习。
以上。

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

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

相关文章

使用PyTorch从头实现Transformer

前言 本文使用Pytorch从头实现Transformer&#xff0c;原论文Attention is all you need paper&#xff0c;最佳解读博客&#xff0c;学习视频GitHub项目地址Some-Paper-CN。本项目是译者在学习长时间序列预测、CV、NLP和机器学习过程中精读的一些论文&#xff0c;并对其进行了…

acwing算法提高之基础算法--前缀和、差分、二分

目录 1 介绍2 训练 1 介绍 本博客用来记录前缀和、差分、二分相关的题目。 2 训练 题目1&#xff1a;99激光炸弹 C代码如下&#xff0c; #include <cstdio> #include <string> #include <iostream> #include <algorithm>using namespace std;cons…

node.js中path模块-路径处理,语法讲解

node中的path 模块是node.js的基础语法&#xff0c;实际开发中&#xff0c;我们通过使用 path 模块来得到绝对路径&#xff0c;避免因为相对路径带来的找不到资源的问题。 具体来说&#xff1a;Node.js 执行 JS 代码时&#xff0c;代码中的路径都是以终端所在文件夹出发查找相…

JSON.stringify()和JSON.parse()

JSON.stringify() JSON.stringify() 是 JavaScript 中的一个内置方法&#xff0c;用于将一个 JavaScript 值&#xff08;对象或值&#xff09;转换为一个 JSON 字符串。这个方法对于在客户端和服务器之间传输数据特别有用&#xff0c;因为 JSON 是一种轻量级的数据交换格式&am…

Scratch编程和Python编程的区别,孩子更适合哪一种

Scratch编程和Python编程有一些显著的不同之处&#xff1a; Scratch是一种基于可视化编程的编程语言&#xff0c;通过拖拽代码块来构建程序&#xff0c;适合初学者和年龄较小的孩子学习。而Python是一种文本编程语言&#xff0c;需要输入代码来编写程序&#xff0c;更适合有一定…

SpringBoot对接口配置跨域设置

目录 1. 使用 @CrossOrigin 注解 2. 全局跨域配置 2.1. 注意事项 在 Spring Boot 应用中,接口配置跨域(Cross-Origin Resource Sharing,CORS)设置是一个常见的需求,特别是当你的前端应用和后端服务部署在不同的域名下时。 以下是几种设置跨域的方法: 1. 使用 @Cross…

基于Springboot的滑雪场管理系统(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的滑雪场管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&a…

11、Flink 的 Keyed State 详解

1.Keyed DataStream 使用 keyed state&#xff0c;首先需要为DataStream指定 key&#xff08;主键&#xff09;&#xff1b;这个 key 用于状态分区&#xff08;数据流中的 Record 也会被分区&#xff09;可以使用 DataStream 中 Java/Scala API 的 keyBy(KeySelector) 或者是 …

搜好货API接口:快速获取商品列表的利器

搜好货商品列表API接口允许开发者根据关键字搜索并获取相关的商品列表数据。接口支持多种参数配置&#xff0c;可以根据需求灵活调整搜索条件和结果返回格式。 点击获取key和secret API接口请求说明 请求地址&#xff1a;https://api.souhaohuo.com/goods/search请求方法&…

速卖通关键字搜索API接口:快速获取商品列表的利器

速卖通关键字搜索API接口允许开发者根据用户输入的关键字进行商品搜索&#xff0c;并返回与之相关的商品列表。通过调用该接口&#xff0c;您可以快速获取与关键字匹配的商品信息&#xff0c;包括商品标题、价格、图片等&#xff0c;为您的电商业务提供有力支持。 三、API接口…

以信息挖掘为关键技术的智慧校园建设

随着信息技术的快速发展&#xff0c;数据信息资源以井喷的姿态涌现。数据信息的大量涌现给人们带来丰富的数据信息资源&#xff0c;但面对海量的信息资源时&#xff0c;加大了人们对有效信息资源获取的难度&#xff0c;数据挖掘技术正是这一背景下的产物&#xff0c;基于数据挖…

【Redis】Redis安装、配置、卸载使用可视化工具连接Redis

文章目录 1.前置条件2.安装Redis2.1下载Redis安装包并解压2.2在redis目录下执行make命令2.3修改Redis配置文件2.4启动Redis服务2.5连接redis服务 3.Redis卸载4.使用可视化工具连接Redis 1.前置条件 Linux操作系统需要要是64位.如果不清楚自己Linux上是多少位的,可以使用以下命…

C语言之详细讲解文件操作(抓住文件操作的奥秘)

什么是文件 与普通文件载体不同&#xff0c;文件是以硬盘为载体存储在计算机上的信息集合&#xff0c;文件可以是文本文档、图片、程序等等。文件通常具有点三个字母的文件扩展名&#xff0c;用于指示文件类型&#xff08;例如&#xff0c;图片文件常常以KPEG格式保存并且文件…

一文了解复杂度

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、算法效率二、时间复杂度1.定义2.大O的渐进表示法3.一般常见复杂度4.实例 三、空间复杂度1.定义2.空间复杂度计算3.实例 总结 前言 计算复杂性理论&#xf…

Redis的持久化方法,各自优缺点,怎么选择?

持久化&#xff1a; redis基于内存是数据库&#xff0c;内容存到内存中&#xff0c;也可以存到硬盘中&#xff0c;这个过程就叫持久化。有两种方案&#xff0c;RDB和AOP两种。 RDB RDB持久化就是把当前进程数据生成快照保存到硬盘的过程RDB文件是⼀个压缩的二进制文件&#…

VisualGLM-6B微调(V100)

Visualglm-6b-CSDN博客文章浏览阅读1.3k次。【官方教程】XrayGLM微调实践&#xff0c;&#xff08;加强后的GPT-3.5&#xff09;能力媲美4.0&#xff0c;无次数限制。_visualglm-6bhttps://blog.csdn.net/u012193416/article/details/131074962?ops_request_misc%257B%2522req…

使用Axios从前端上传文件并且下载后端返回的文件

前端代码&#xff1a; function uploadAndDownload(){showLoading();const fileInput document.querySelector(#uploadFile);const file fileInput.files[0];const formData new FormData()formData.append(file, file)return new Promise((resolve, reject) > {axios({…

【经典论文阅读1】FM模型——搜推算法里的瑞士军刀

全文由『说文科技』原创出品&#xff0c;文章同步更新于公众号『说文科技』。版权所有&#xff0c;翻版必究。 FM模型发表于2010年&#xff0c;它灵活好用且易部署。作者行文极其流畅&#xff0c;作者首先对要处理的问题进行介绍&#xff0c;接着作者提出FM模型&#xff0c;这…

Vue单页面应用和多页面应用的区别

概念&#xff1a; SPA单页面应用&#xff08;SinglePage Web Application&#xff09;&#xff0c;指只有一个主页面的应用&#xff0c;一开始只需要加载一次js、css等相关资源。所有内容都包含在主页面&#xff0c;对每一个功能模块组件化。单页应用跳转&#xff0c;就是切换…

Grad-CAM(梯度加权类激活图)

Grad-CAM&#xff08;Gradient-weighted Class Activation Mapping&#xff09;是一种可视化技术&#xff0c;用于解释卷积神经网络&#xff08;CNN&#xff09;的决策过程。它通过生成类激活图&#xff08;Class Activation Map&#xff0c;CAM&#xff09;来突出显示对网络预…