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

前言

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

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);
}

 

 

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

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

相关文章

【娱乐项目】基于cnchar库与JavaScript的汉字查询工具

Demo介绍 利用了 cnchar 库来进行汉字相关的信息查询&#xff0c;并展示了汉字的拼音、笔画数、笔画顺序、笔画动画等信息用户输入一个汉字后&#xff0c;点击查询按钮&#xff0c;页面会展示该汉字的拼音、笔画数、笔画顺序&#xff0c;并绘制相应的笔画动画和测试图案 cnchar…

C#.Net筑基 - 常见类型

01、结构体类型Struct 结构体 struct 是一种用户自定义的值类型&#xff0c;常用于定义一些简单&#xff08;轻量&#xff09;的数据结构。对于一些局部使用的数据结构&#xff0c;优先使用结构体&#xff0c;效率要高很多。 可以有构造函数&#xff0c;也可以没有。因此初始化…

Android 混淆问题

我的安卓混淆只需要在gradle里面开启就行了。 buildTypes {release {minifyEnabled trueshrinkResources truezipAlignEnabled trueproguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro}} minifyEnabled true 这个就是开启方法&#xf…

XELA - uSkin 三轴触觉传感器:为机器人赋予敏锐触感

XELA Robotics 的 uSkin 触觉传感器以其创新性在机器人技术中备受关注。它凭借高密度设计和三轴力测量能力&#xff0c;大幅提升了机器人的触觉感知能力&#xff0c;这种技术不但增强了机器人的智能化和柔性&#xff0c;还为不同行业的应用创造了广泛的可能性。其中在机器人灵巧…

k8s集成skywalking

如果能科学上网的话&#xff0c;安装应该不难&#xff0c;如果有问题可以给我留言 本篇文章我将给大家介绍“分布式链路追踪”的内容&#xff0c;对于目前大部分采用微服务架构的公司来说&#xff0c;分布式链路追踪都是必备的&#xff0c;无论它是传统微服务体系亦或是新一代…

Card组件的用法

文章目录 1 概念介绍2 使用方法3 示例代码我们在上一章回中介绍了DataTable的排序功能相关的内容,本章回中将介绍Card Widget.闲话休提,让我们一起Talk Flutter吧。 1 概念介绍 我们在本章回中介绍的Card Widget是一种容器类组件,它可以包含其它的组件,它的大小随着被包含…

【论文格式】同步更新中

1横向和纵向坐标的坐标密度不能太大&#xff0c;显示太多看起来不好看&#xff0c;本课题组采用emf&#xff0c;目前使用页面内紧凑&#xff0c;600dpi 2Force(kN):k小写 3涉及到变量的&#xff0c;变量本身斜体

win10 docker启动报错virtual machine platform not enabled

Docker启动报错virtual machine platform not enabled。原因是windows未启动虚拟化平台。 跟着下面的操作基本都能解决问题。 1.打开控制面板 2.在启用或关闭windows功能中开启Hyper-V和虚拟机平台 这个虚拟机平台也一定要勾选&#xff0c;我看晚上很多帖子是没说需要勾选这个…

【弓箭傳說 2 角色遊戲攻略】—詳細指南

《弓箭傳說2》是一款經典的Roguelike風格手遊&#xff0c;玩家將透過獨特的技能組合和角色選擇&#xff0c;挑戰複雜的關卡和強大的敵人。弓箭傳說 2 儲值 - 本攻略將從角色選擇、技能搭配、裝備強化及天賦升級等方面&#xff0c;全面解讀如何在遊戲中取得最佳表現。 一、遊戲…

数字IC后端实现之PR工具中如何避免出现一倍filler的缝隙?

在数字IC后端实现中&#xff0c;由于有的工艺foundary不提供Filler1&#xff0c;所以PR工具Innovus和ICC2在做标准单元摆放时需要避免出现两个标准单元之间的缝隙间距是Filler1。为了实现这个目的&#xff0c;我们需要给PR工具施加一些特殊的placement constraint&#xff08;典…

C# HandyControl 官方示例 保姆级从0到1搭建学习流程

HandyControl 保姆级从0到1官网示例搭建学习流程 GitHub https://github.com/HandyOrg/HandyControl 技术交流 https://join.slack.com/t/handycontrol/shared_invite/zt-sw29prqd-okFmRlmETdtWhnF7C3foxA 码云 https://gitee.com/handyorg/HandyControl/ 中文文档 http…

矩阵sum,prod函数

s u m sum sum表示求和, p r o d prod prod表示求乘积 s u m sum sum函数 对于矩阵&#xff0c;可以对某一行或某一列求和&#xff0c;也可以对矩阵整体求和 s u m ( a , 1 ) sum(a,1) sum(a,1)计算每一列的和 s u m ( a , 2 ) sum(a,2) sum(a,2)计算每一行的和 计算矩阵整体…

TongWeb78处理应用自身JAR包冲突思路

通常应用运行时报错如果出现java.lang.NoClassDefFoundError&#xff0c;那么首先需要看报错日志下方的Caused by内容是什么&#xff0c;如果是ClassNotFoundException&#xff0c;那么原因就是缺少某个类。如果仍然是NoClassDefFoundError&#xff0c;那么很大可能是类冲突导致…

K8S版本和istio版本的对照关系

版本对照关系 下载地址1 下载地址2

Apifox 11月更新|支持发布多个文档站、文档站支持 Algolia 搜索配置、从返回响应直接设置断言

Apifox 新版本上线啦&#xff01;&#xff01;&#xff01; 在API管理和自动化测试的领域&#xff0c;工具的每一次更新&#xff0c;都意味着开发者和测试人员的工作效率将进一步提升。而11月的Apifox更新&#xff0c;再次为我们带来了几个重磅的新功能&#xff0c;助力提升文档…

uniapp App端在renderjs层渲染echarts获取不到service层id的问题

报错信息&#xff1a;Cannot read properties of undefined (reading id) at app-view.js 这样的写法App端有时在renderjs视图层获取不到server逻辑层的数据 server层 renderjs层 解决方法&#xff1a;需要把数据(id)通过server层向renderjs层传值 server层 renderjs层

AI智算-正式上架GPU资源监控概览 Grafana Dashboard

下载链接 https://grafana.com/grafana/dashboards/22424-ai-gpu-20241127/

【Visual Studio Code】--- 连接 SSH 服务器终端 直接开发

在编程的艺术世界里&#xff0c;代码和灵感需要寻找到最佳的交融点&#xff0c;才能打造出令人为之惊叹的作品。而在这座秋知叶i博客的殿堂里&#xff0c;我们将共同追寻这种完美结合&#xff0c;为未来的世界留下属于我们的独特印记。 【Visual Studio Code】--- 连接 SSH 服务…

2024年首届数证杯 初赛wp

“数证杯”电子数据取证分析大赛致力于成为全国第一大电子数据取证分析大赛&#xff0c;面向所有网络安全从业人员公开征集参赛选手。参赛选手根据所属行业报名参赛赛道&#xff0c;比赛设置冠军、亚军、季军奖。所涉及行业包括能源、金融、通信、取证、安全等企业以及各类司法…

基于智能物联网关的车辆超重AI检测应用

超重超载是严重的交通违法行为&#xff0c;超重超载车辆的交通安全风险极高&#xff0c;像是一颗行走的“不定时炸弹”&#xff0c;威胁着社会公众的安全。但总有一些人受到利益驱使&#xff0c;使超重超载的违法违规行为时有发生。 随着物联网和AI技术的发展&#xff0c;针对预…