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

前言

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

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,一经查实,立即删除!

相关文章

代码随想录第31天

56. 合并区间 class Solution:def merge(self, intervals: List[List[int]]) -> List[List[int]]:intervals.sort(keylambda p: p[0]) # 按照左端点从小到大排序ans []for p in intervals:if ans and p[0] < ans[-1][1]: # 可以合并ans[-1][1] max(ans[-1][1], p[1…

MicroSoft Project2007 安装教程

一、安装教程 访问地址 二、安装链接 通过网盘分享的文件&#xff1a;Project2007CD 链接: https://pan.baidu.com/s/1Y8VnhVPiKjcmAEh8cIR5sQ?pwdp2hk 提取码: p2hk --来自百度网盘超级会员v6的分享

揭示Lyapunov方法的奥秘:控制理论中的稳定性之钥

揭示Lyapunov方法的奥秘&#xff1a;控制理论中的稳定性之钥 引言 在控制理论和动力系统的研究中&#xff0c;稳定性分析始终是一个核心问题。19世纪末&#xff0c;俄罗斯杰出的数学家亚历山大米哈伊洛维奇李雅普诺夫&#xff08;Aleksandr Mikhailovich Lyapunov&#xff09…

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

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

C#.Net筑基 - 常见类型

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

怎么选拔人才

1.引入案例&#xff1a;航天员的选拔 一、选拔&#xff1a; 1.测评和甄选的概念2.测评甄选的程序3.测评甄选原则&#xff1a;因事择人、人岗匹配&#xff1b;科学考核&#xff0c;平等竞争&#xff1b;德才兼备4.测评甄选的信效度&#xff1a;稳定系数、等值系数、内在一致系…

nginx-1.22.1 limit_req功能在C底层实现

##nginx.conf配置文件limit_req配置 ##nginx限制访问频率&#xff0c;限流 http { limit_req_zone $binary_remote_addr zoneone:30m rate1r/s; server { listen 8103; server_name localhost; location ~ ^/yym/ { …

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;还为不同行业的应用创造了广泛的可能性。其中在机器人灵巧…

【前端开发】老年模式:字体大小设置

导入插件&#xff1a;npm install postcss-px-to-viewport--save-dev postcss-px-to-viewport&#xff1a;将px单位转换为视口单位的 (vw, vh, vmin, vmax) 的 PostCSS 插件 在根目录下新建文件&#xff1a;postcss.config.js const path require(“path”); module.exports …

k8s集成skywalking

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

Card组件的用法

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

【人工智能】Python与强化学习:从零实现多臂老虎机(Multi-Armed Bandit)问题

《Python OpenCV从菜鸟到高手》带你进入图像处理与计算机视觉的大门! 强化学习是一种模仿生物行为的学习方法,在不确定环境中寻找最优策略。多臂老虎机(Multi-Armed Bandit, MAB)是强化学习的经典问题之一,模拟了在多个选择中如何平衡探索和利用,以获取最大的长期回报。…

Ubuntu WiFi检测

ubuntu检测到多个同名wifi&#xff0c;怎么鉴别假冒的wifi&#xff1f; 在Ubuntu中&#xff0c;如果检测到多个同名的Wi-Fi网络&#xff0c;可能存在假冒的Wi-Fi&#xff08;例如“蜜罐”攻击&#xff09;。以下是一些鉴别假冒Wi-Fi的方法&#xff1a; 检查信号强度&#xff1a…

【论文格式】同步更新中

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

cesium中的CallbackProperty

Cesium中的CallbackProperty函数解析 在Cesium中&#xff0c;CallbackProperty是一个非常有用的特性&#xff0c;它允许开发者动态更新实体&#xff08;Entity&#xff09;的属性&#xff0c;例如位置、方向、高度等。这种动态更新的能力使得Cesium在处理与时间相关的数据时变…

win10 docker启动报错virtual machine platform not enabled

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

【jvm】C1编译器和C2编译器的区别

目录 1. 说明2. 主要特点2.1 C1编译器2.2 C2编译器 3. 编译过程与优化策略3.1 C1编译器3.3 C2编译器 4. 分层编译与协作机制5. 性能影响与选择 1. 说明 1.JVM&#xff08;Java虚拟机&#xff09;中的C1编译器和C2编译器是两种不同的即时编译器&#xff08;JIT Compiler&#x…

【人工智能】使用Python实现序列到序列(Seq2Seq)模型进行机器翻译

解锁Python编程的无限可能:《奇妙的Python》带你漫游代码世界 序列到序列(Sequence-to-Sequence, Seq2Seq)模型是解决序列输入到序列输出任务的核心架构,广泛应用于机器翻译、文本摘要和问答系统等自然语言处理任务中。本篇文章深入介绍 Seq2Seq 模型的原理及其核心组件(…

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

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