数据结构--树二叉树顺序结构存储的二叉树(堆)

前言

前面我们学习了顺序表、链表、栈和队列,这些都是线性的数据结构。今天我们要来学习一种非线性的数据结构——树。

11c6b0b6e2654f0abe03cb8ed3e57cb8.png

树的概念及结构

树的概念

树是一种非线性的数据结构,是由n(n≥0)个有效结点组成的一个具有层次关系的集合。

  • 树有一个特殊的结点——根结点 根结点没有前驱结点。
  • 除根结点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1≤i≤m)又是一颗结构与树类似的子树。每棵子树的根结点有且只有一个前驱,但可以有0个或多个后继。
  • 因此,树是递归定义的。

6489b5ac43024e5d8bfc40e946b09da5.jpg

 注意:树形结构中,子树之间不能有交集,否则就不是树形结构。

af87886766754ce49997ec1dee7023ed.png

与树相关的概念

a45cba2adf384d05a58a0aa4bd39d58d.png

 

节点的度:一个节点含有的子树的个数称为该节点的度。(如上图中节点A的度为6)

树的度:一棵树中,最大的节点的度称为树的度。(如上图中树的度为6)

叶子节点或终端节点:度为0的节点为叶子节点。(如上图中B、C、H、I、P、Q、K、L、M、N都为叶子节点)

非终端节点或分支节点:度不为0的结点。(如上图中D、E、J、F、G节点为分支节点)

双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点。(例如,A是B的父节点)

孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点。(例如,B是A的孩子节点)

兄弟节点:具有相同父节点的节点互称为兄弟节点。(例如,B和C互为兄弟节点)

堂兄弟节点:双亲在同一层的节点互为堂兄弟。(例如,H和I互为堂兄弟节点)

节点的祖先:从根节点到该节点所经过分支上的所有节点。(上图中A是所有节点的祖先)

子孙:以某节点为根的子树中任意一个节点都称为该节点的子孙。(如上图中,所有节点都是A的子孙)

树的高度或深度:树中节点的最大层次。(上图中,树的高度为4)

森林:由m(m>0)棵互不相交的树的集合称为森林。
 

树的表示

树的结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了。既要保存值域,又要保存结点和结点之间的关系,实际中树有很多种表示方式,如:双亲表示法、孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。在这里,我们简单了解比较常用的孩子兄弟表示法

左孩子右兄弟

c483f4db00514fe7bc185e499efd47d1.png

树在实际中的运用

树在实际中的运用:表示文件系统的目录结构。

我们一起来看一下树在Linux树状目录结构和Windows文件系统层次结构中的应用。

  • Linux树状目录结构

ecf1ce6971284bc683e6d19fdfbee060.jpg

  •    Windows文件系统层次结构

bfbb45eeaeed4a92b995208ad08dd582.png

这里的C盘、E盘和D盘就是根,每个根下面可以有多个文件或者目录,空目录的话就是当前结构中的叶子节点(不考虑未来可能添加内容的情况下)。

二叉树的概念及结构

概念

二叉树是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树形结构。每个节点最多只能有两棵子树,且有左右之分。

28c4b30e879c4f07b0a2154dd3194a81.png

特殊的二叉树

满二叉树

如果一个二叉树的每一层的结点树都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为k,且结点总数是2ᵏ-1,那它就是满二叉树。

完全二叉树

完全二叉树是效率很高的数据结构,它是由满二叉树引来的。对于深度为k的且有n个结点的二叉树,当且仅当它每一个结点都与深度为k的满二叉树中的编号从1到n的结点一一对应时,则称之为完全二叉树。满二叉树是一种特殊的完全二叉树。(如一个完全二叉树有h层,那它前h-1层是满二叉树,最后一层要求从左到右都是连续的。

我们通过一张图片能更好地理解满二叉树和完全二叉树的结构:

c09f5c3dec1d4b44bf7583cb852a7a54.jpeg

二叉树的性质

我们令一棵二叉树里叶子结点的数量为n0,度为2的分支结点的数量为n2,高度为h,一共有n个结点。

  1. n0=n2+1
  2. 如果该二叉树为满二叉树,那么它一共有2ʰ-1个结点227a98b7ba904060a60c84350e59b428.png
  3. 如果该二叉树为完全二叉树,那么它的节点数量的范围为[2ʰ⁻¹,2ʰ-1]
  4. 非空二叉树的第i层最多有2ⁱ⁻¹个节点
  5. 如果该二叉树为满二叉树,那么它的深度h=log₂(n+1)
  6. 父节点与孩子结点在数组中的下标关系(具体见二叉树的顺序结构部分的讲解)。

二叉树的存储结构

在讲二叉树的存储结构之前,我们要先弄懂逻辑结构和物理结构的概念。

逻辑结构描述了数据元素之间的逻辑关系,而物理结构则描述了数据元素在计算机中的存储方式。

我们以链表为例进行讲解,帮助我们更好地理解这两个概念:

f8541eb5deaf4b21b8875b0629a668e3.jpg二叉树一般可以使用两种存储结构:顺序结构和链式结构。

二叉树的顺序结构

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储。

(ps:需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。)

父节点与孩子结点在数组中的下标关系

如果该二叉树为完全二叉树,按照从上到下、从左到右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:

  1. i>0时,i位置节点的双亲序号为(i-1)/2;i=0时,i为根节点编号,无双亲节点。
  2. 2i+1<n时,左孩子序号为2i+1(2i+1≥n,否则无左孩子)。
  3. 2i+1<n时,右孩子序号为2i+2(2i+2≥n,否则无右孩子)。

e39ce3fe6fda4e2ba775d7369dfb074f.jpg

这种存储方式更适合存储满完全二叉树;如果存储其他二叉树,由于结构的特点会出现浪费空间的情况。我们通过一张非完全二叉树的顺序存储图片来理解为什么会出现浪费的情况:

b3e83438cb9843a4bb8eebcdb172f9af.jpg

什么是堆?

堆是一种特殊的完全二叉树,通常分为最大堆(大根堆)和最小堆(小根堆):

  1. 最大堆:每个节点的值都大于或等于其子节点的值。这意味着根节点的值是整棵树中的最大值。
  2. 最小堆:每个节点的值都小于或等于其子节点的值。这意味着根节点的值是整棵树中的最小值。

f42e290f67094ff1a00041a33400ec79.png

后续二叉树顺序结构的实现我们主要来实现堆。

二叉树的实现(堆的实现)

堆的数据结构

#define INIT_CAPACITY 4 //初始化堆可容纳的数据个数

typedef int dataType;
typedef struct Heap
{
	dataType* a;//指向堆中实际存储数据的数组
	int size;//堆中当前存储的元素个数
	int capacity;//堆的容量,即堆最多可以存储多少个元素
}heap;

初始化堆

void initHeap(heap* pheap)
{
	assert(pheap);

	pheap->a = (dataType*)malloc(sizeof(dataType) * INIT_CAPACITY);
	if (pheap->a == NULL) {
		perror("malloc fail");
		return;
	}
	pheap->capacity = INIT_CAPACITY;
	pheap->size = 0;
	printf("The heap has been initialized.\n");
}

因为这里的堆是基于数组来实现的,所以它的初始化初始化跟前面顺序表的初始化很相似。

堆的插入

向上调整法

堆的插入需要用到向上调整法,以大顶堆为例,现在我们有一个数组模拟出来的大顶堆:

19ef638cd29f41f6b7f0af2fb53ca80e.png

接下来我们要往堆中插入数据80:

1b4cec447bd24ee7ab003f11ee94e12c.png

         ①                    ②                ③                 ④

我们先看图①,在这个大顶堆中,新插入的80比当前堆顶元素数据70大,所以要让80成为这个大顶堆的堆顶元素。从前面我们知道,通过父子结点之间的下标关系,我们很容易调整父子结点之间的位置。所以我们让80(下标为7)作为孩子节点,让它与它的父节点10(下标为3)比较大小,发现孩子节点比父节点大,所以我们交换两个节点的位置就达到了图②的效果。

图②中的80与10交换位置后,新插入节点的下标得到了更新:80节点的下标变为3,10节点的下标变为7,所以父子的关系也会改变。此时80节点的成为了10节点的父节点,80节点的父节点是下标为1的60节点。准备继续比较新插入节点与它更新下标后父节点的大小:由于80与它的父节点里的数据60比较后还是80大,所以还要进行调整,60节点与80节点交换位置,就达到了图③的效果。

以此类推,80与70交换位置,最终80成了堆顶元素。

向上调整法的时间复杂度是多少呢?我们预估最坏的情况,就是新插入的元素比当前堆中所有元素的数据都大,那他就要调整的次数就是h-1次(难以理解就配合上面例子中的图片去理解)h-1=log₂(n+1),所以它的时间复杂度就是log₂n。

代码

void swap(dataType* x, dataType* y)
{
	dataType tmp = *x;
	*x = *y;
	*y = tmp;
}
void adjustUp(dataType* a, int child)
{
	assert(a);
	int parent = (child - 1) / 2;//求新插入节点的父节点下标
	while (child > 0) //关于这个退出条件,后面会讲解
    {
        
		if (a[child] > a[parent]) 
		{
            //新插入的节点数据大于父节点数据的情况:
			swap(&a[parent], &a[child]);//交换父子结点的位置

            //更新新插入节点的下标,准备继续比较它与更新下标后父节点的大小
			child = parent;
			parent = (child - 1) / 2;
		}
		else {
            //新插入节点的数据小于或等于父节点的数据,符合大顶堆,直接退出循环
			break;
		}
	}
}
void pushHeap(heap* pheap, dataType x)
{
	assert(pheap);

	if (pheap->size == pheap->capacity) {
        //容量满了,要插入数据就得扩容
		dataType* tmp = (dataType*)realloc(pheap->a,sizeof(dataType) * pheap->capacity * 2);
		if (tmp == NULL) {
			perror("realloc fail");
			return;
		}
		pheap->a = tmp;
		pheap->capacity *= 2;
	}

	pheap->a[pheap->size] = x;//插入新数据
	adjustUp(pheap->a, pheap->size);//向上调整法

	++pheap->size;
}

为什么在adjustUp里面的循环退出条件是child<=0的时候呢?我们知道这里的while循环和里面的内容的主要作用是确保新插入堆中的元素能够“上浮”(也就是向上调整法)到其正确的位置,从而维护堆的性质。而这里的child就是新插入元素在刚插入的时候和以及不断更新位置后下标的位置。有两种情况:第一种情况,如果堆中有节点,且新插入的元素比堆中的任意一个节点的数据都大,那它的的下标位置在“上浮”过程中会更新到0,也就说明已经到了堆顶位置,不用再“上浮”了;第二种情况,如果堆本来就为空,那么新插入元素的下标就是0,也不需要“上浮”来调整位置。

总结

1.检查并扩容(如需):若堆满,则扩容。
2.末尾插入:将新元素添加到堆数组的末尾。
3.向上调整:
   从新元素开始,与其父节点比较。
   若新元素大,则交换位置,并继续向上比较直至不需交换或到达根节点。
4.更新大小:堆大小加1。

堆的删除

在堆的数据结构中,特别是二叉堆(包括大顶堆和小顶堆),堆顶元素总是具有特定的性质:在大顶堆中,堆顶是最大的元素;在小顶堆中,堆顶是最小的元素。因此,删除堆顶元素是堆操作中的一个核心功能,这一操作在很多堆的应用中都至关重要,所以我们主要讲解堆顶元素的删除。

向下调整法

堆的删除需要用到向下调整法,还是以大顶堆为例:

aa1c2df1d26e48aa9740b8919bb2a49b.png

我们要删除堆顶元素80:

4cf4f7eaf0ee48a8af792e2014cb19f3.png

        ①                 ②                   ③                  ④

我们看图①,我们先把要删除的堆顶元素80和堆中最后一个元素10交换位置,交换位置后,后面再调整过程中我们就不用在管这个末尾元素80了(这个末尾元素也就相当于被删除了)。

交换位置后,原来的大顶堆达到了图②的效果。我们发现此刚刚交换上来的元素10所在的位置不符合大顶堆的性质,元素10小于它的两个孩子节点70和30。我们需要把交换上来的元素10与孩子节点中最大的一个交换位置,而最大的孩子节点的数据是70。

交换位置后,达到了图③的效果。此时元素10的下标位置和原来在堆中对应的父子关系也发生了变化——元素10的孩子节点的数据为56和10。我们还是让元素10与孩子节点中最大的那个进行交换。

最终达到了图④的效果。

我们通过前面对向上调整法的时间复杂度的推断,很容易推断出这里的时间复杂度也是log₂n。

代码

int getMax(dataType* a, int x, int y)
{
	if (a[x] > a[y])
	{
		return x;
	}
	else {
		return y;
	}
}
void adjustDown(dataType* a, int parent, int size)
{
	assert(a);

	while (parent < size)//parent就是原来交换上面的最后一个元素。后续向下调整的过程中,它的下标不能超出当前堆中的最后一个元素的下标
	{
		int lChild = 2 * parent + 1, rChild = 2 * parent + 2;//更新parent的孩子节点的下标
        int swapChild;//用来记录parent最大的孩子节点的下标
		if (rChild < size) {
            //如果parent有两个孩子节点,求出最大的孩子节点的下标
			swapChild = getMax(a, lChild, rChild);
		}
		else {
            //如果此时parent只有一个孩子,那肯定是左孩子,swapChild直接保存左孩子的下标
			swapChild = lChild;
		}
		
		if (swapChild < size && a[parent] < a[swapChild]) 
        {
            //如果父节点小于其最大的孩子,则交换它们,并将parent更新为交换后的孩子下标,以便继续向下调整。
			swap(&a[parent], &a[swapChild]);
			parent = swapChild;
		}
		else {
            //如果父节点不小于其孩子,或者已经到达堆的底部,则停止调整。
			break;
		}
	}
}
void popHeap(heap* pheap)
{
	assert(pheap);

	if (pheap->size == 0) {
        //堆中无元素,就不能再删除元素了
		printf("The heap has been already emtied!\n");
		return;
	}

	swap(&pheap->a[0], &pheap->a[pheap->size - 1]);//先把要删除的堆顶元素和堆中最后一个元素交换位置
	--pheap->size;//交换位置后,删除此时最后一个元素
	adjustDown(pheap->a, 0, pheap->size);//向下调整法,使得交换位置后的堆符合大顶堆的性质

}

总结

1.检查非空:若堆为空,则直接返回。
2.交换并减小:将堆顶与末尾元素交换,然后减小堆大小。
3.向下调整:从新的堆顶开始,与其孩子比较并交换(如需),直至堆性质恢复或到达堆底。

判断堆是否为空

bool isEmpty(heap* pheap)
{
	assert(pheap);

	if (pheap->size == 0) {
		return true;
	}
	else {
		return false;
	}
}

获取堆顶元素

dataType topHeap(heap* pheap)
{
	assert(pheap);
	assert(!isEmpty(pheap));

	return pheap->a[0];
}

获取堆里的元素个数

int sizeHeap(heap* pheap)
{
	assert(pheap);

	return pheap->size;
}

测试案例

void test() {
	heap hp;
	initHeap(&hp);

	pushHeap(&hp, 80);
	pushHeap(&hp, 10);
	pushHeap(&hp, 56);
	pushHeap(&hp, 30);
	pushHeap(&hp, 60);
	pushHeap(&hp, 70);
	pushHeap(&hp, 25);
	pushHeap(&hp, 15);
	for (int i = 0; i < sizeHeap(&hp); i++) {
		printf("%d ", hp.a[i]);
	}
	printf("\n");
	while (!isEmpty(&hp)) {
		popHeap(&hp);
	}

	if (isEmpty(&hp)) {
		printf("The heap has been emptied!\n");
	}
	
	destroyedHeap(&hp);
}

 

 

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

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

相关文章

网络安全运行与维护 加固练习题

1. 提交用户密码的最小长度要求。 输入代码: cat /etc/pam.d/common-password 提交答案: flag{20} 2.提交iptables配置以允许10.0.0.0/24网段访问22端口的命令。 输入代码: iptables -A INPUT -p tcp -s 10.0.0.0/24 --dport 22 -j ACCEPT 提交答案: flag{iptables -A I…

【汇编语言】call 和 ret 指令(三) —— 深度解析汇编语言中的批量数据传递与寄存器冲突

文章目录 前言1. 批量数据的传递1.1 存在的问题1.2 如何解决这个问题1.3 示例演示1.3.1 问题说明1.3.2 程序实现 2. 寄存器冲突问题的引入2.1 问题引入2.2 分析与解决问题2.2.1 字符串定义方式2.2.2 分析子程序功能2.2.3 得到子程序代码 2.3 子程序的应用2.3.1 示例12.3.2 示例…

Java 泛型详细解析

泛型的定义 泛型类的定义 下面定义了一个泛型类 Pair&#xff0c;它有一个泛型参数 T。 public class Pair<T> {private T start;private T end; }实际使用的时候就可以给这个 T 指定任何实际的类型&#xff0c;比如下面所示&#xff0c;就指定了实际类型为 LocalDate…

Python语法基础(四)

&#x1f308;个人主页&#xff1a;羽晨同学 &#x1f4ab;个人格言:“成为自己未来的主人~” 高阶函数之map 高阶函数就是说&#xff0c;A函数作为B函数的参数&#xff0c;B函数就是高阶函数 map&#xff1a;映射 map(func,iterable) 这个是map的基本语法&#xff0c;…

【JavaEE初阶】应是天仙狂醉,乱把白云揉碎 - (重点)线程

本篇博客给大家带来的是线程的知识点, 由于内容较多分几天来写. &#x1f40e;文章专栏: JavaEE初阶 &#x1f680;若有问题 评论区见 ⭐欢迎大家点赞 评论 收藏 分享 ❤❤❤ 如果你不知道分享给谁,那就分享给薯条. 你们的支持是我不断创作的动力 . 1. 认识线程 1.1 概念 )1 …

构建鸿蒙5.0应用(一)

准备工作 1、开发工具 开发工具使用华为官方推荐的IDE&#xff1a;DevEco Studio &#xff0c;为鸿蒙应用开发提供了最全面的官方支持&#xff0c;包括最新的 SDK、API 和功能。 2、编译工具 开发鸿蒙应用需要安装Nodejs环境&#xff0c;为打包编译鸿蒙应用提供支持&#x…

【Linux】匿名管道通信场景——进程池

&#x1f525; 个人主页&#xff1a;大耳朵土土垚 &#x1f525; 所属专栏&#xff1a;Linux系统编程 这里将会不定期更新有关Linux的内容&#xff0c;欢迎大家点赞&#xff0c;收藏&#xff0c;评论&#x1f973;&#x1f973;&#x1f389;&#x1f389;&#x1f389; 文章目…

FUSU: 多源多时相土地利用变化分割数据集

FUSU是首个针对细粒度城市语义理解的多时态、多源地类变化分割数据集&#xff0c;其提供高分辨率双时态图像和每月时序观测&#xff0c;支持对城市动态变化的高频率监测。FUSU-Net是统一的时序架构&#xff0c;可同时进行变化检测和分割任务。结合光学和SAR数据&#xff0c;通过…

LLM学习笔记(13)分词器 tokenizer

由于神经网络模型不能直接处理文本&#xff0c;因此我们需要先将文本转换为数字&#xff0c;这个过程被称为编码 (Encoding)&#xff0c;其包含两个步骤&#xff1a; 使用分词器 (tokenizer) 将文本按词、子词、字符切分为 tokens&#xff1b;将所有的 token 映射到对应的 tok…

Unity中让光点跟图片填充区的末尾一起移动

一、实现效果展示 想要实现的效果如下,就是要让白色光点图片跟随绿色圆形图片填充区末尾一起移动。 二、代码如下: using UnityEngine; using System.Collections; using UnityEngine.UI; using DG.Tweening;public class IconCircle : MonoBehaviour {public float ti…

给定一个整数可能为正,0,负数,统计这个数据的位数.

题目描述 给定一个整数可能为正,0,负数,统计这个数据的位数. 例如1234567输出7位; -12345678输出8位;0输出1位 代码实现 int main() { long long m; long long n; scanf("%lld",&n); mn; int count0;//位数 do { count; n/10;//舍弃个位 }while(n!0); printf(&…

LLamafactory API部署与使用异步方式 API 调用优化大模型推理效率

文章目录 背景介绍第三方大模型API 介绍LLamafactory 部署API大模型 API 调用工具类项目开源 背景介绍 第三方大模型API 目前&#xff0c;市面上有许多第三方大模型 API 服务提供商&#xff0c;通过 API 接口向用户提供多样化的服务。这些平台不仅能提供更多类别和类型的模型…

【关闭or开启电脑自带的数字键盘】

目录 一、按数字键盘左上角的按键【NumLK Scroll】 二、修改注册表中数字键盘对应的数值【InitialKeyboardIndicators】 1、步骤&#xff1a; 2、知识点&#xff1a; 一、按数字键盘左上角的按键【NumLK Scroll】 这是最简单快捷的方法。 关闭后若想开启&#xff0c;再按一…

【FAQ】使用Node.js 镜像 构建本地项目

在nodejs官方并没有提供使用node.js构建本地项目的方法&#xff0c;但是通过阅读官方文档&#xff0c;可以发现&#xff0c;官方在包管理器界面提供了如下语句 所以node.js容器是可以执行语句的 下面通过docker 的 -w 、-v 参数设置容器工作目录和目录映射&#xff08;实现本…

深度学习 | pytorch + torchvision + python 版本对应及环境安装

Hi&#xff0c;大家好&#xff0c;我是半亩花海。要让一个基于 torch 框架开发的深度学习模型正确运行起来&#xff0c;配置环境是个重要的问题&#xff0c;本文介绍了 pytorch、torchvision、torchaudio 及 python 的对应版本以及环境安装的相关流程。 目录 一、版本对应 二…

4399大数据面试题及参考答案(数据分析和数据开发)

对数据分析的理解 数据分析是一个从数据中提取有价值信息以支持决策的过程。它涵盖了数据收集、清洗、转换、建模和可视化等多个环节。 首先&#xff0c;数据收集是基础。这包括从各种数据源获取数据&#xff0c;例如数据库、文件系统、网络接口等。这些数据源可以是结构化的数…

fastdds:编译、安装并运行helloworld

fastdds安装可以参考官方文档&#xff1a; 3. Linux installation from sources — Fast DDS 3.1.0 documentation 从INSTALLATION MANUAL这一节可以看出来&#xff0c;fastdds支持的操作系统包括linux、windows、qnx、MAC OS。本文记录通过源码和cmake的方式来安装fastdds的…

Istio笔记01--快速体验Istio

Istio笔记01--快速体验Istio 介绍部署与测试部署k8s安装istio测试istio 注意事项说明 介绍 Istio是当前最热门的服务网格产品&#xff0c;已经被广泛应用于各个云厂商和IT互联网公司。企业可以基于Istio轻松构建服务网格&#xff0c;在接入过程中应用代码无需更改&#xff0c;…

ipad项目 蓝湖宽度

ipad项目 横屏状态时 蓝湖宽度设置930px media screen and (orientation: portrait) {/* 竖屏时的样式 */ } media screen and (orientation: landscape) {/* 默认是 横屏时的样式 */ }

14、保存与加载PyTorch训练的模型和超参数

文章目录 1. state_dict2. 模型保存3. check_point4. 详细保存5. Docker6. 机器学习常用库 1. state_dict nn.Module 类是所有神经网络构建的基类&#xff0c;即自己构建一个深度神经网络也是需要继承自nn.Module类才行&#xff0c;并且nn.Module中的state_dict包含神经网络中…