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) {//中序线索化二叉树T
	pre = NULL;//pre初始为NULL
	if (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.mfbz.cn/a/592157.html

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

相关文章

Vue3 + Vite + TypeScript + Element-Plus创建管理系统项目

官方文档 Vue3官网 Vite官方中文文档 创建项目 使用npm命令创建项目&#xff1a; npm create vitelatest输入项目名称&#xff1a; ? Project name:项目名选择vue&#xff1a; ? Select a framework: - Use arrow-keys. Return to submit.Vanilla > VueReactPrea…

【网站项目】木里风景文化管理平台

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

CSS精灵图、字体图标、HTML5新增属性、界面样式和网站 favicon 图标

精灵图 为什么要使用精灵图 一个网页中往往会应用很多小的背景图像作为修饰&#xff0c;当网页中的图像过多时&#xff0c;服务器就会频繁地接收和发送请求图片&#xff0c;造成服务器请求压力过大&#xff0c;这将大大降低页面的加载速度,因此&#xff0c;为了有效地减少服务…

JAVA基础|常用API-JDK8之前传统的日期,时间

一. Date &#xff08;一&#xff09;说明 代表的是日期和时间 &#xff08;二&#xff09;常用的用法 构造器说明public Date()创建一个Date对象&#xff0c;代表的是系统当前此刻日期时间public Date(long time)把时间毫秒值转换成Date日期对象 常见方法说明public long …

算法提高之潜水员

算法提高之潜水员 核心思想&#xff1a;二维01背包 两个容量v1v2注意状态计算时j和p可以<各自的v #include <iostream>#include <cstring>#include <algorithm>using namespace std;const int N 1010,M 80,K 22;int f[K][M];int k,V1,V2;int main(){ci…

FloodFill-----洪水灌溉算法(DFS例题详解)

目录 一.图像渲染&#xff1a; 代码详解&#xff1a; 二.岛屿数量&#xff1a; 代码详解&#xff1a; 三.岛屿的最大面积&#xff1a; 代码详解&#xff1a; 四.被围绕的区域&#xff1a; 代码详解&#xff1a; 五.太平洋大西洋水流问题&#xff1a; 代码详解&#x…

锂电池充放电方式曲线

作为一种“化学能-电能”相互转换的能量装置&#xff0c;锂电池在使用过程中必然会进行充电和放电&#xff0c;合理的充放电方式既能减轻锂电池的损伤程度&#xff0c;又能充分发挥锂电池的性能&#xff0c;具有重要的应用价值。 如《GB/T 31484-2015&#xff1a;电动汽车用动…

非对称齿轮的跨棒距算的对不对

前面有一期咱们聊了非对称齿轮《》&#xff0c;非对称齿轮的齿厚测量一般都为跨棒距。最近研究了下计算方法&#xff0c;对计算结果的正确性做了下验证。 在MATLAB中编制了相关的计算程序&#xff1a; 齿轮的模数4&#xff0c;左侧分度圆压力角25&#xff0c;右侧分度圆压力角…

Sqli-labs第一关到第四关

目录 一&#xff0c;了解PHP源代码 二&#xff0c;破解第一关 2.1在了解完源码之后&#xff0c;我们重点看一下 2.2破解这道题表中有几列 2.3查看表中哪一列有回显 2.4查询库&#xff0c;表&#xff0c;列信息 三&#xff0c;总结 前提&#xff1a; 之所以把1234关…

MySQL基础_1.MySQL概述

文章目录 一、关系型数据库和非关系型数据库1.1 关系型&#xff08;RDBMS&#xff09;1.2 非关系型&#xff08;非RDBMS&#xff09; 二、常用的基础语句2.1 查看表的创建信息2.2 编码问题 一、关系型数据库和非关系型数据库 1.1 关系型&#xff08;RDBMS&#xff09; 是最古…

都上3D数字孪生了,2D的WEB组态和大屏可视化未来的发展在哪里?趋势是基于页面嵌套、蓝图连线等新技术,与功能业务应用融合

首先回顾下组态工具的发展史&#xff1a; 回顾发展史&#xff0c;WEB组态终于可以搭建业务系统了&#xff01;&#xff08;页面嵌套 节点编辑 WEB组态 上位机 大屏可视化 无代码 0代码 iframe nodered 蓝图&#xff09;-CSDN博客文章浏览阅读624次&#xff0c;点赞12次&#x…

ThreeJS:纹理的颜色空间

色彩空间Color Space 在ThreeJS中&#xff0c;纹理的colorSpace属性用于定义文里的颜色空间。 颜色空间是一个用于描述颜色的数学模型&#xff0c;在现实生活中&#xff0c;人眼可以观察到无数种颜色&#xff0c;而颜色空间就是用来描述这些颜色的一个方法&#xff0c;不同的颜…

C语言-自定义类型:结构体,枚举,联合

目录 一、结构体1.1 结构体变量的定义和初始化1.2 结构体内存对齐1.3 修改默认对齐数1.4 结构体传参 二、位段2.1 什么是位段2.2 位段的内存分配2.3 位段的跨平台问题2.4 位段的应用 三、枚举3.1 枚举类型的定义3.2 枚举的优点 四、联合&#xff08;共用体&#xff09;4.1 联合…

c#数据库: 9.删除和添加新字段/数据更新

先把原来数据表的sexy字段删除,然后重新在添加字段sexy,如果添加成功,sexy列的随机内容会更新.原数据表如下: using System; using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Data.SqlClient; using System.Linq; using System.…

Linux理解文件操作 文件描述符fd 理解重定向 dup2 缓冲区 C语言实现自己的shell

文章目录 前言一、文件相关概念与操作1.1 open()1.2 close()1.3 write()1.4 read()1.4 写入的时候先清空文件内容再写入1.5 追加&#xff08;a && a&#xff09; 二、文件描述符2.1 文件描述符 fd 0 1 2 的理解2.2 FILE结构体&#xff1a;的源代码 三、深入理解文件描述…

jupyter notebook 设置密码报错ModuleNotFoundError: No module named ‘notebook.auth‘

jupyter notebook 设置密码报错ModuleNotFoundError: No module named ‘notebook.auth‘ 原因是notebook新版本没有notebook.auth 直接输入以下命令即可设置密码 jupyter notebook password

k8s调度原理以及自定义调度器

kube-scheduler 是 kubernetes 的核心组件之一&#xff0c;主要负责整个集群资源的调度功能&#xff0c;根据特定的调度算法和策略&#xff0c;将 Pod 调度到最优的工作节点上面去&#xff0c;从而更加合理、更加充分的利用集群的资源&#xff0c;这也是我们选择使用 kubernete…

Linux---软硬链接

软链接 我们先学习一下怎样创建软链接文件&#xff0c;指令格式为&#xff1a;ln -s 被链接的文件 生成的链接文件名 我们可以这样记忆&#xff1a;ln是link的简称&#xff0c;s是soft的简称。 我们在下面的图片中就是给test文件生成了一个软链接mytest&#xff1a; 我们来解…

数据结构篇其四---栈:后进先出的魔法世界

前言 栈的学习难度非常简单&#xff0c;前提是如果你学过顺序表和单链表的话&#xff0c;我直接说我的观点了&#xff0c;栈就是有限制的顺序表和单链表。 栈只允许一端进行插入删除。栈去除了各种情况复杂的插入删除&#xff0c;只允许一端插入删除的特性&#xff0c;这一种数…

5月4(信息差)

&#x1f384; HDMI ARC国产双精度浮点dsp杜比数码7.1声道解码AC3/dts/AAC环绕声光纤、同轴、USB输入解码板KC33C &#x1f30d; 国铁集团回应高铁票价将上涨 https://finance.eastmoney.com/a/202405043066422773.html ✨ 源代码管理平台GitLab发布人工智能编程助手DuoCha…