【数据结构】由完全二叉树引申出的堆的实现

news2024/11/25 3:00:53

【数据结构】由完全二叉树引申出的堆的实现

  • 一、什么是堆
  • 二、目标
  • 三、实现
    • 1、初始化工作
    • 2、堆的插入(堆的创建)
      • 2.1、向上调整建堆
        • 2.1.1、向上调整算法原理解析
        • 2.1.2、代码实现
      • 2.2、向下调整建堆
        • 2.2.1、向下调整算法原理解析
        • 2.2.2、代码实现
      • 2.3、“向上”和“向下”复杂度的差异
    • 3、堆的删除
      • 3.1、原理解析
      • 3.2、代码实现
    • 4、返回堆顶数据
    • 5、判断堆是否为空
    • 6、返回堆中数据个数
    • 7、堆的销毁

一、什么是堆

关于“堆”,百度百科上是这么说的:

在这里插入图片描述
——————————引自百度百科

由上面可知,我们可以将堆理解成一个数组,也可以理解成一个完全二叉树。
其实由于完全二叉树的特殊性,其本身就可以使用一个数组来存储。
在之前的二叉树的实现中,我们已经知道完全二叉树的最后一层的叶子节点一定是连续的:
在这里插入图片描述
而之所以完全二叉树能用数组存储,是因为完全二叉树的以下这些性质:

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

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

我们可以对上面那可完全二叉树进行编号试试:
在这里插入图片描述
大家可以自行验证一下,就会发现是完全符合上面的性质的。
而我们把这些序号理解成下标,我们就会发现堆其实就可以存储在数组中:
在这里插入图片描述
(这里假设数数组的下标从1开始)

其实堆在计算机界有着相当广泛的应用,比较容易理解的就是Topk问题和堆排序,Topk就是一个求排名前几的问题,堆排序就是对一个序列进行排序。
而其他更高深的应用得以后更深入的学习时才能有所体会,我们现在得先把原理搞明白。

好,那就让我们进入堆的实现吧。

二、目标

今天要实现的堆主要有以下接口:

// 堆的初始化
void HeapInit(Heap* php);
// 堆的插入(堆的创建)
void HeapPush(Heap* php, HPDataType x);
// 堆的删除,删除堆顶数据
void HeapPop(Heap* php);
// 返回堆顶数据
HPDataType HeapTop(Heap* php);
// 判断堆是否为空
bool HeapEmpty(Heap* php);
// 返回堆的数据个数
int HeapSize(Heap* php);
// 堆的销毁
void DestroyHeap(Heap* php);

接口不是很多,但是有一两个接口并不是那么好实现的……

三、实现

1、初始化工作

首先我们来定义堆的结构,有上面的介绍我们可知堆的存储结构其实就是一个数组,是数组就要进行扩容,所以在我们的结构定义中就应该有capacity和size:

typedef int HPDataType;

typedef struct Heap {
	HPDataType* data;
	int size;
	int capacity;
} Heap;

然后我们还需要一个接口,对堆进行初始化:

void HeapInit(Heap* php) {
	assert(php);
	php->data = NULL;
	php->size = 0;
	php->capacity = 0;
}

其实就是对各个成员赋初始值而已。

2、堆的插入(堆的创建)

其实将数据插入堆中,就是创建堆的过程。
而建堆也分为两种方案,分别是向上调整建堆和向下调整建堆,其核心主要涉及到两个算法,即向上调整算法和向下调整算法。

2.1、向上调整建堆

2.1.1、向上调整算法原理解析

其实向上调整建堆就是通过向上调整算法,将每次新加入的数据调整到它该去的位置,即经过调整后整个数组还符合堆的结构。
例如我们现在要建一个小堆,在已有的小堆基础上,再添加入一个新的数据:
在这里插入图片描述
如上图标红色的那个节点就是我们新加入的节点,而我们知道小堆要符合的条件是任意一个父节点都要比它的左右孩子都要小。如果如上面这样就结束了,明显就不符合小堆的要求了。
所以我们需要通过向上调整算法将新加入的节点移动到正确地位置,方法就是,如果新加入的节点不满足其节点小于它,就一直和父节点进行交换,过程如下:
在这里插入图片描述
补充一个想上调整算法的前提条件,那就是在新节点的上面一定要是一个堆,这有这样才能保证,向上调整算法执行完后整个堆依然符合堆的结构。

因为是向上调整,所以算法开始时的定位就是新加入的节点,然后我们再通过上面提到的公式:
在这里插入图片描述
来计算出新节点的双亲节点的下标,我们定义新节点的下标为child,那其双亲节点的下标就为 parent == (child - 1) / 2
在这里插入图片描述
如果parent的值不小于child的值,我们就将两者交换。然后将两者迭代,即再执行child = parent ,parent = (child - 1) / 2,再次进行判断:
在这里插入图片描述
如此反复,直到parent小于child或者child不再大于0就结束,因为我们最多就调整到根节点:
在这里插入图片描述

2.1.2、代码实现

有了以上思路,那我们写起代码来也就水到渠成了:

void AdjustUp(Heap* php, int child) {
	assert(php);
	int parent = (child - 1) / 2;
	while (child > 0) {
		if (php->data[child] < php->data[parent]) {
			Swap(&php->data[child], &php->data[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else {
			break;
		}
	}
}

有了向上调整算法,那我们完成向上调整建堆也就很简单了,只需要对每次加入的新节点执行一次向上调整算法即可:

void HeapPush(Heap* php, HPDataType x) {
	assert(php);
	
	// 先检查是否需要增容
	if (php->size == php->capacity) {
		int newCapacity = php->capacity == 0 ? 10 : 2 * php->capacity;
		HPDataType* temp = (HPDataType*)realloc(php->data, newCapacity * sizeof(HPDataType));
		if (NULL == temp) {
			perror("realloc fail!\n ");
			exit(-1);
		}
		php->data = temp;
	}
	// 向上调整插入
	php->data[php->size] = x;
	
	AdjustUp(php, php->size);
	php->size++;
}

只不过在正式插入之前还要检查一下是否需要增容,不过我觉得这已经是基本操作了。

2.2、向下调整建堆

建堆的另个方式就是向下调整建堆。
经过上面的线上调整建堆的讲解,可能大家已经明白,如果插入的新节点不能保证堆还符合堆的规定,那一定是要将新节点往上移动的。
其实向下调整建堆的本质也是将新节点向上移动,只不过实现的原理不同了。

2.2.1、向下调整算法原理解析

向下调整算法的使用前提是要保证被调整的节点的下面(子树)是堆。
我们先来说说向下调整算法的思路,向下调整算法和向上调整刚好是反过来。向上调整算法是通过迭代的方法持续将孩子节点移动到上边,而向下调整算法是通过迭代的方法持续将双亲节点向下移动。
例如我们现在将堆顶的数据改成一个更大的数:
在这里插入图片描述
这很明显就不符合小堆的规定了,所以我们要使用向下调整算法来将堆顶数据调到下方。
和向上调整算法不同,这里的向下调整算法还涉及到一个“二选一”的操作,因为小堆的规定时间双亲节点一定要小于左右孩子,所以这里实际是要将左右孩子中较小的那个节点和双亲节点交换:
在这里插入图片描述
然后就是如向上调整算法一样,一直迭代parent和child,直到跳到最底层或者满足双亲小于孩子,例如这里的18最终调到的位置如下:
在这里插入图片描述
但问题来了,我们这里是每次在数组末尾,也就是数的底层加入数据,我们这里是“向下”调整算法,那该怎么把底层的节点调到上面呢?
在这里插入图片描述
其实我们这里要使用到一个“反向”的思维,就是将新节点的双亲节点向下调以达到新节点向上调的目的,例如这里我们就可以对0节点的双亲节点4执行向上调整,调整完之后就是这样:
在这里插入图片描述
而这里只是因为到了底层所以只调整了一次,但这显然还没完全调整完毕。所以我们还要继续调整。
而后我们在对parent进行自减1,因为堆本身就是使用数组来存储的,所以减了一个1也就到了相邻的左边的一个节点:
在这里插入图片描述
再继续执行向下调整算法,很明显这里是不需要调整的,而当parent自减到节点3时,就需要调整了,对接点3指向完向下调整算结果如下:
在这里插入图片描述
如此反复,直到调整到根节点或者parent小于左右孩子:
在这里插入图片描述

2.2.2、代码实现

有了以上思路,那我们写起代码来也就水到渠成了:
先实现向下调整算法:

void AdjustDown(Heap* php, int parent) {
	assert(php);
	int child = 2 * parent + 1; // 默认左孩子为较小的那个
	while (child < php->size) {
		if (child + 1 < php->size && php->data[child + 1] < php->data[child]) {
			child++;
		}
		if (php->data[child] < php->data[parent]) {
			Swap(&php->data[child], &php->data[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else {
			break;
		}
	}
}

然后就可以实现向下调整版的建堆了:

void HeapPush(Heap* php, HPDataType x) {
	assert(php);

	// 先检查是否需要增容
	if (php->size == php->capacity) {
		int newCapacity = php->capacity == 0 ? 10 : 2 * php->capacity;
		HPDataType* temp = (HPDataType*)realloc(php->data, newCapacity * sizeof(HPDataType));
		if (NULL == temp) {
			perror("realloc fail!\n ");
			exit(-1);
		}
		php->data = temp;
	}
	// 向下调整插入
	php->data[php->size] = x;
	php->size++;
	int parent = (php->size - 1 - 1) / 2;
	while (parent >= 0) {
		AdjustDown(php, parent);
		parent--;
	}
}

2.3、“向上”和“向下”复杂度的差异

那我们有了向上和向下两种建堆方式,我们应该选用哪一种更优呢?
这就得比较一下向上和向下两种建堆方式的复杂度了。
我们先来分析一下向上调整建堆的复杂度:
要计算调整算法的复杂度其实和完全二叉树的每层节点数和高度有关,所以我们首先得知道关于二叉树的三个性质:

在这里插入图片描述

而我们知道,满二叉树其实是一个特殊的完全二叉树,完全二叉树相比于与同高度的满二叉树也只是最后一层少了若干个节点。
而时间复杂度的计算计算的是一个估值,所以我们完全可以将完全二叉树的复杂度计算近似成满二叉树的计算,也就是忽略最后一层缺少的节点不计。
假设我们现在已经有了一个满二叉树,他已经是一个通过向上调整算法建出来的堆:
在这里插入图片描述
在最坏的情况下,对于每一个新插入的节点我们都要将它调整到根节点的位置(也就是调整高度减1次),所以对于每一层的调整次数我们就可以用刚才的公式来算了:
在这里插入图片描述
所以总共的移动次数算起来就是:
在这里插入图片描述
这其实就是一个典型的等差乘等比的数列求和,可能大家看到这个式子很快就会从美好的记忆中想起来这个式子应该用“错位相减法”来解决。
置于错位相减法,我这里不不具体展开计算了,毕竟这不是数学课,我就直接给出计算好的结果了:
在这里插入图片描述
因为时间复杂度算的是估值,所以我们可以再进一步忽略常数,则式子就变成了h * 2^h,而我们再把h的计算方法:在这里插入图片描述
代入公式,并整理,忽略常数就可以算出最终的时间复杂度的量级为nlogn
故对于有n个节点的堆来说,使用向上调整算法建堆的时间复杂度为O(nlogn)。

然后我们再来看看向下调整算法的时间复杂度分析:
也是类似的分析方式,但因为向下调整的对象只能是双亲节点,所以我们只需算到第h - 1层即可,因为最后一层都是叶子结点,不需要调整。
最坏情况下,还是每一个节点都需要调整:
在这里插入图片描述
所以总计的次数是:
在这里插入图片描述
同样用错位相减法得出来的结果是:
在这里插入图片描述
再代入计算h的公式最后的出结果为n - log(n + 1),再进一步省略掉常数和对数,最后的时间复杂度近似为n。
故向下调整建堆的时间复杂度为O(n)。

通过上面的比较,我们可以得出结论:向下调整建堆的时间复杂度要比向上调整的更优,所以我们以后要建堆的话就优先选择向下调整建堆。

3、堆的删除

就像栈和队列有它们的规定的删除和插入一样,堆也有自己的规定,那就是插入数据只能在最后面插入,而删除数据只能在堆顶删除。

3.1、原理解析

因为对实际上使用数组存储的,有的朋友可能就认为删除就很简单,不就是删除第一个元素然后再把后面的元素再往前移动一位吗:
在这里插入图片描述
其实这样就大错特错了,光光这样挪动数据是不能保证挪动后的数据还是一个堆的,上面的这个例子在挪动后还是堆值是一个特例,我再举一个例子就能证明了:
在这里插入图片描述
很明显,挪动后面的数据覆盖了第一个0之后,再将数组化成堆,就不符合堆得规定了。

所以我们正确地做法应该是像下面这样:
第一步: 先将第一个数据和最后一个数据交换:
在这里插入图片描述
第二步: size–(相当于删除)
在这里插入图片描述
第三步: 通过向下调整算法将堆顶数据移动到正确的位置:
在这里插入图片描述
这样做的好处是,既不破坏大部分的堆结构,而且向下调整的复杂度也较低。

3.2、代码实现

有了以上思路,那我们写起代码来也就水到渠成了:

void HeapPop(Heap* php) {
	assert(php);

	// 先交换堆顶和最后一个叶子结点
	Swap(&php->data[0], &php->data[php->size - 1]);
	php->size--;
	// 再向下调整
	AdjustDown(php, 0);
}

4、返回堆顶数据

其实堆最难实现的就是上面的这三个接口,剩下的这些接口基本就不用动脑子了。

返回堆顶数据,堆顶数据其实就是数组的首元素,所以我们返回首元素即可:

HPDataType HeapTop(Heap* php) {
	assert(php);
	assert(!HeapEmpty(php));
	return php->data[0];
}

5、判断堆是否为空

我们直接返回size是否等于0的结果即可:

bool HeapEmpty(Heap* php) {
	assert(php);
	return php->size == 0;
}

6、返回堆中数据个数

其实就是直接返回size:

int HeapSize(Heap* php) {
	assert(php);
	return php->size;
}

7、堆的销毁

void DestroyHeap(Heap* php) {
	assert(php);
	free(php->data);
	php->data = NULL;
	php->size = 0;
	php->capacity = 0;
}

其实在我个人看来,堆的实现是要比链式二叉树要难得,因为链式二叉树的实现只需要死死抓住递归这个特性即可。而堆,虽然接口不多,但是可能光是理解两个向上向下调整算法就能让很多朋友抓耳挠腮。而且向上和向下的实现思路也是有点变形的,这对于基础比较薄弱的朋友来说可能是比较难的。
所以还是建议大家:一定要自己多实现几遍!自己多实现几遍!自己多实现几遍!
重要的事情要说三遍。

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

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

相关文章

初识网络安全

目录 HTML前置基础知识 1、id和class区别&#xff1a; 2、一些常用的属性&#xff1a; 3、HTML字符编码和实体编码 4、URL介绍 网址的组成部分&#xff1a; TTL值 DNS工作原理和资源记录及其种类&#xff1a; 5、正确区分“加密”和“签名” 6、状态码 1xx &#xf…

如何安装pycharm

PyCharm是JetBrains公司推出的一款Python集成开发环境&#xff08;IDE&#xff09;&#xff0c;可以提供高效的Python代码编写、调试和测试。以下是一些PyCharm的主要功能&#xff1a; 代码智能提示和自动补全功能&#xff1b;支持调试和测试Python代码&#xff1b;完整的Pyth…

基于Springboot+Vue的幼儿园管理系统设计与实现

博主介绍&#xff1a; 大家好&#xff0c;我是一名在Java圈混迹十余年的程序员&#xff0c;精通Java编程语言&#xff0c;同时也熟练掌握微信小程序、Python和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我擅长在JavaWeb、SSH、SSM、SpringBoot等框架下…

汽车相关知识及术语

1 汽车构造与制造流程 1.1 汽车构造 汽车可以分为四大部分 车身&#xff1a; 骨架、车身钣金件以及座椅、仪表、天窗、车外后视镜等车身附件 动力系统&#xff1a; 发动机和变速器 底盘&#xff1a; 传动系统、悬架系统、转向系统、制动系统和车轮轮胎 电气电子系统&#…

《Apollo 智能驾驶进阶课程》三、无人车自定位技术

1. 什么是无人车自定位系统 相对一个坐标系来确定无人车的位置和姿态 定位的指标要求大概分为三个部分&#xff1a;精度、鲁棒性、场景 定位精度必须控制在10厘米以内&#xff0c;才能使行驶中的自动驾驶车辆避免出现碰撞/车道偏离的情况。鲁棒性一般情况下用最大值来衡量。…

Java IO流详细教程

目录 一、IO介绍 IO流体系 字节流 字节输出流&#xff1a;FileoutputStream 字节输入流FilelnputStream 字符流 字符输入流 字符输出流 缓冲流 字节缓冲流 字符缓冲流 序列化、反序列化流 序列化/对象操作输出流 反序列化/对象操作输入流 打印流 字节打印流 字…

firewalld与iptables练习

1、禁止一个IP访问 iptables -I INPUT -s $ip -j REJECT 2、清空默认的防火墙默认规则&#xff1a; iptables -F 3、保存清空后的防火墙规则表 service iptables save 4、firewall-cmd --list-all #查看防火墙规则&#xff08;只显示/etc/firewalld/zones/public.xml中防火墙…

投票活动链接创建微信链接视频投票线上免费投票链接

近些年来&#xff0c;第三方的微信投票制作平台如雨后春笋般络绎不绝。随着手机的互联网的发展及微信开放平台各项基于手机能力的开放&#xff0c;更多人选择微信投票小程序平台&#xff0c;因为它有非常大的优势。 1.它比起微信公众号自带的投票系统、传统的H5投票系统有可以图…

从零手写操作系统之RVOS协作式多任务切换实现-03

从零手写操作系统之RVOS协作式多任务切换实现-03 任务&#xff08;task&#xff09;多任务 &#xff08;Multitask&#xff09;任务上下文&#xff08;Context&#xff09;多任务系统的分类协作式多任务 创建和初始化第 1 号任务切换到第一号任务执行协作式多任务 - 调度初始化…

字典树算法(C/C++)

目录 一、字典树算法的概念介绍 二、字典树算法的实现 三、例题 &#xff08;注&#xff1a;借鉴蓝桥杯国赛特训营&#xff09; 一、字典树算法的概念介绍 首先我们看下字典的组织方式 Trie 的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效…

实训总结-----Scrapy爬虫

1.安装指令 pip install scrapy 2.创建 scrapy 项目 任意终端 进入到目录(用于存储我们的项目) scrapy startproject 项目名 会在目录下面 创建一个以 项目名 命名的文件夹 终端也会有提示 cd 项目名 scrapy genspider example example.com 3.运行爬虫指令 scrapy craw…

ffmpeg之AVFormatContext结构体详细解释

AVFormatContext 作用 AVFormatContext主要起到了管理和存储媒体文件相关信息的作用。它是一个比较重要的结构体&#xff0c;在FFmpeg中用于表示媒体文件的格式上下文&#xff0c;其中包含了已经打开的媒体文件的详细信息&#xff0c;包括媒体文件的格式、媒体流的信息、各个媒…

【笔记】使用电脑连接树莓派 并在电脑屏幕上显示树莓派桌面(无需额外为树莓派购买显示器)

一、前言 想在树莓派上跑 yolo5&#xff0c;为了方便地看到代码的检测结果&#xff0c;需要为树莓派外接显示器&#xff0c;但是手头并没有额外的显示器&#xff0c;于是想在电脑屏幕上显示树莓派的桌面&#xff0c;对解决的过程作一些记录。 二、基本流程 树莓派系统的烧录…

c++11 标准模板(STL)(std::bitset)(三)

定义于头文件 <bitset> template< std::size_t N > class bitset; 类模板 bitset 表示一个 N 位的固定大小序列。可以用标准逻辑运算符操作位集&#xff0c;并将它与字符串和整数相互转换。 bitset 满足可复制构造 (CopyConstructible) 及可复制赋值 (CopyAssign…

【SpringMVC】请求与响应

1&#xff0c;PostMan工具的使用 1. PostMan简介 代码编写完后&#xff0c;我们要想测试&#xff0c;只需要打开浏览器直接输入地址发送请求即可。发送的是GET请求可以直接使用浏览器&#xff0c;但是如果要发送的是POST请求呢? 如果要求发送的是post请求&#xff0c;我们就…

基于前推回代法的连续潮流计算研究【IEEE33节点】(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

从零搭建微服务-网关中心(一)

写在最前 如果这个项目让你有所收获&#xff0c;记得 Star 关注哦&#xff0c;这对我是非常不错的鼓励与支持。 源码地址&#xff1a;https://gitee.com/csps/mingyue 文档地址&#xff1a;https://gitee.com/csps/mingyue/wikis 新建 mingyue-gateway 在 【从零搭建微服务…

09 Redis与MySQL数据双写一致性工程落地案例

canal 是什么 canal [kə’nl]&#xff0c;中文翻译为 水道/管道/沟渠/运河&#xff0c;主要用途是用于 MySQL 数据库增量日志数据的订阅、消费和解析&#xff0c;是阿里巴巴开发并开源的&#xff0c;采用Java语言开发&#xff1b;历史背景是早期阿里巴巴因为杭州和美国双机房…

23种设计模式之职责链模式(Chain of Responsibility Pattern)

前言&#xff1a;大家好&#xff0c;我是小威&#xff0c;24届毕业生&#xff0c;在一家满意的公司实习。本篇文章将23种设计模式中的访问者模式&#xff0c;此篇文章为一天学习一个设计模式系列文章&#xff0c;后面会分享其他模式知识。 如果文章有什么需要改进的地方还请大佬…

集群化环境前置准备

集群化环境前置准备 介绍 需要完成集群化环境的前置准备&#xff0c;包括创建多台虚拟机&#xff0c;配置主机名映射&#xff0c;SSH免密登录等等。 部署 配置多台Linux虚拟机 安装集群化软件&#xff0c;首要条件就是要有多台Linux服务器可用。 我们可以使用VMware提供的…