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

前言

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

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.rhkb.cn/news/480816.html

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

相关文章

qt QProxyStyle详解

1、概述 QProxyStyle是Qt框架中QStyle类的一个子类&#xff0c;它提供了一种代理机制&#xff0c;允许开发者在不直接修改现有样式&#xff08;QStyle&#xff09;实现的情况下&#xff0c;对样式行为进行定制或扩展。通过继承QProxyStyle&#xff0c;开发者可以重写其虚方法&…

STL基本算法之copy与copy_backward

copy 不论是对客端程序或对STL内部而言&#xff0c;copy()都是一个常常被调用的函数。由于copy进行的是复制操作&#xff0c;而复制操作不外乎应用assignment operator或者copy construct(copy 算法用的是前者)&#xff0c;但是某些元素型别拥有的是trivial assignment operato…

不可分割的整体—系统思考的微妙法则

不可分割的整体——系统思考的微妙法则 作为企业领导者&#xff0c;我们经常需要做出决策&#xff0c;但有时候&#xff0c;我们会忽略一个事实&#xff1a;每个决策都不是孤立的&#xff0c;它背后都是一个复杂系统的一部分。 无论是市场动态、团队协作&#xff0c;还是产品…

云计算基础-期末复习

第一章&#xff1a;云计算概论 一、云计算的定义与特征 1. 定义&#xff1a; 云计算是一种通过网络以按需、可扩展的方式获取计算资源和服务的模式。它将计算资源视为一种公用事业&#xff0c;用户可以根据需求动态获取和释放资源&#xff0c;而无需了解底层基础设施的细节。…

基于Java的小程序电商商城开源设计源码

近年来电商模式的发展越来越成熟&#xff0c;基于 Java 开发的小程序电商商城开源源码&#xff0c;为众多开发者和企业提供了构建个性化电商平台的有力工具。 基于Java的电子商城购物平台小程序的设计在手机上运行&#xff0c;可以实现管理员&#xff1b;首页、个人中心、用户…

【机器学习】机器学习的基本分类-监督学习-逻辑回归-对数似然损失函数(Log-Likelihood Loss Function)

对数似然损失函数&#xff08;Log-Likelihood Loss Function&#xff09; 对数似然损失函数是机器学习和统计学中广泛使用的一种损失函数&#xff0c;特别是在分类问题&#xff08;例如逻辑回归、神经网络&#xff09;中应用最为广泛。它基于最大似然估计原理&#xff0c;通过…

Milvus 2.5:全文检索上线,标量过滤提速,易用性再突破!

01. 概览 我们很高兴为大家带来 Milvus 2.5 最新版本的介绍。 在 Milvus 2.5 里&#xff0c;最重要的一个更新是我们带来了“全新”的全文检索能力&#xff0c;之所以说“全新”主要是基于以下两点&#xff1a; 第一&#xff0c;对于全文检索基于的 BM25 算法&#xff0c;我们采…

RHCE作业五-shell脚本

一要求&#xff1a; 通过shell脚本分析部署nginx网络服务 1.接收用户部署的服务名称 2.判断服务是否安装 ​ 已安装&#xff1b;自定义网站配置路径为/www&#xff1b;并创建共享目录和网页文件&#xff1b;重启服务 ​ 没有安装&#xff1b;安装对应的软件包 3.测试 判断服务…

分页查询日期格式不对

方式一:在属性上加入注解&#xff0c;对日期进行格式化 方式二:在 WebMvcConfiguration 中扩展Spring MVC的消息转换器&#xff0c;统一对日期类型进行格式化处理 /*** 统一转换处理扩展spring mvc* 后端返回前端的进行统一转化处理* param converters*/Overrideprotected voi…

深度学习3:数据预处理使用Pandas与PyTorch的实践

文章目录 导读一、主题与提纲1.1. 读取数据集1.2. 处理缺失值1.3. 转换为张量格式 二、结论 本文是经过严格查阅相关权威文献和资料&#xff0c;形成的专业的可靠的内容。全文数据都有据可依&#xff0c;可回溯。特别申明&#xff1a;数据和资料已获得授权。本文内容&#xff0…

Tülu 3:重新定义开源大模型的后训练范式

一、引言 在大型语言模型&#xff08;LLM&#xff09;的发展历程中&#xff0c;预训练阶段往往受到最多关注&#xff0c;动辄需要数百万美元算力投入和数万亿token的训练数据。然而&#xff0c;一个鲜为人知但同样关键的事实是&#xff1a;预训练完成的模型实际上并不能直接投…

【机器学习】机器学习的基本分类-监督学习-逻辑回归(Logistic Regression)

逻辑回归是一种分类算法&#xff0c;尽管名字中包含“回归”&#xff0c;但其主要用于解决二分类和多分类问题。它通过学习一个逻辑函数&#xff0c;预测输入属于某个类别的概率。 1. 逻辑回归的基本概念 目标 逻辑回归的目标是找到一个函数 h(x)&#xff0c;输出一个概率值 …

PyMOL操作手册

PyMOL 操作手册 The man will be silent, the woman will be tears. – itwangyang ​ 翻译整理&#xff1a;itwangyanng 2024 年 11月 29 日 目录 初识 PyMOL… 5 0.1 安装 PyMOL… 5 0.1.1 Windows 系统开源版 PyMOL 的安装… 5 0.1.2 教育版 PyMOL 的下载安装……

麒麟系统x86安装达梦数据库

一、安装准备前工作 操作系统&#xff1a;银河麒麟V10&#xff0c;CPU&#xff1a; x86_64 架构 下载地址&#xff0c;麒麟官网&#xff1a;https://www.kylinos.cn/ 数据库&#xff1a;dm8_20220915_x86_kylin10_64 下载地址&#xff0c;达梦数据库官网&#xff1a;https://…

Hot100 - 搜索二维矩阵II

Hot100 - 搜索二维矩阵II 最佳思路&#xff1a; 利用矩阵的特性&#xff0c;针对搜索操作可以从右上角或者左下角开始。通过判断当前位置的元素与目标值的关系&#xff0c;逐步缩小搜索范围&#xff0c;从而达到较高的效率。 从右上角开始&#xff1a;假设矩阵是升序排列的&a…

docker服务容器化

docker服务容器化 1 引言2 多个容器间网络联通2.1 单独创建关联2.2 创建时关联 3 服务搭建3.1 镜像清单3.2 容器创建 4 联合实战4.2 flink_sql之kafka到starrocks4.2 flink_sql之mysql到starrocks 5 文献借鉴 1 引言 ​ 利用docker可以很效率地搭建服务&#xff0c;本文在win1…

011变长子网掩码

变长子网掩码&#xff1a; 使用变长子网掩码&#xff08;VLSM&#xff09;优化地址分配 目标&#xff1a; 根据需求使用VLSM分配IP地址&#xff0c;减少浪费&#xff0c;并配置静态路由。 网络拓扑 创建一个包含三台路由器&#xff08;R1、R2、R3&#xff09;和五台PC&#x…

SpringBoot小知识(2):日志

日志是开发项目中非常重要的一个环节&#xff0c;它是程序员在检查程序运行的手段之一。 1.日志的基础操作 1.1 日志的作用 编程期调试代码运营期记录信息&#xff1a; * 记录日常运营重要信息(峰值流量、平均响应时长……) * 记录应用报错信息(错误堆栈) * 记录运维过程数据(…

大数据新视界 -- 大数据大厂之 Hive 数据安全:权限管理体系的深度解读(上)(15/ 30)

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

智能探针技术:实现可视、可知、可诊的主动网络运维策略

网络维护的重要性 网络运维是确保网络系统稳定、高效、安全运行的关键活动。在当今这个高度依赖信息技术的时代&#xff0c;网络运维的重要性不仅体现在技术层面&#xff0c;更关乎到企业运营的方方面面。网络运维具有保障网络的稳定性、提升网络运维性能、降低企业运营成本等…