堆的结构与实现

news2024/11/22 11:07:26

堆的结构与实现

  • 二叉树的顺序结构
  • 堆的概念及结构
  • 堆的实现
    • 堆的创建
      • 向上调整建堆
      • 向下调整建堆
      • 堆的操作链接

二叉树的顺序结构

堆其实是具有一定规则限制的完全二叉树。
普通的二叉树是不太适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树会更适合使用顺序结构进行存储。如下图:
在这里插入图片描述

堆的概念及结构

如果有一个关键码的集合 K = { k 0 , k 1 , k 2 , . . . , k n − 1 } K=\{k_0, k_1, k_2, ..., k_{n-1}\} K={k0,k1,k2,...,kn1},当把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并且满足: K i ≤ K 2 ∗ i + 1 K_i\leq K_{2*i+1} KiK2i+1 K i ≤ K 2 ∗ i + 2 K_i\leq K_{2*i+2} KiK2i+2( K i ≥ K 2 ∗ i + 1 K_i\ge K_{2*i+1} KiK2i+1 K i ≥ K 2 ∗ i + 2 K_i\ge K_{2*i+2} KiK2i+2),i=0, 1, 2, …,则称之为小堆(大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质

  • 堆中某个节点的值总是不大于或不小于其父节点的值
  • 堆总是一棵完全二叉树。
    在这里插入图片描述

堆的实现

堆的创建

下面给出一个数组,这个数组逻辑上可以看做一棵完全二叉树,但是还不满足堆的条件。现在我们可以通过算法,将它构建成一个堆。

int a[] = {27,15,19,18,28,34,65,49,25,37};

要将其构建成一个堆结构,有两种调整建堆的方法:一种是向上调整建堆,一种是向下调整建堆。

向上调整建堆

向上调整建堆主要是用于在堆数据结构中,堆在插入数据后,为了继续维持堆的结构,所进行的一种调整。
大致调整过程如下:
在这里插入图片描述
经过一次向上调整后,本来因为插入数据所导致的堆的结构的破坏就被恢复了。
有了这个思路,可以将过程实现如下:

//a - 堆的顺序存储数组
//child - 需要进行向上调整的孩子下标
void AdjustUp(HDataType* a, int child)
{
	assert(a != NULL);

	int parent = (child - 1) / 2;
	while (child > 0)//child为0时,child就到了堆顶,调整结束
	{
		//建小堆 - 孩子比双亲小,就互换位置
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);//交换函数
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

以上只是对一个数据进行的调整,要想实现堆的创建,需要将数组中的所有数据都进行一遍调整(堆顶元素除外),所以可以更进一步,完成一棵完全二叉树到堆的创建。

for (int i = 1; i < n; ++i)
{
	AdjustUp(a, i);
}

以上就是向上调整建堆的过程,我们可以分析一下它的时间复杂度。
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化,使用满二叉树来证明。(时间复杂度看的本来就是近似值,多几个节点并不影响最终结果)
在这里插入图片描述

假设树的高度为h
第1层, 2 0 2^0 20个节点,需要向上调整0层。
第2层, 2 1 2^1 21个节点,需要向上调整1层。
第3层, 2 2 2^2 22个节点,需要向上调整2层。
第4层, 2 3 2^3 23个节点,需要向上调整3层。

第h-1层, 2 h − 2 2^{h-2} 2h2个节点,需要向上调整h-2层。
第h层, 2 h − 1 2^{h-1} 2h1个节点,需要向上调整h-1层。

则需要移动节点总的移动步数为:
T ( n ) = 2 0 ∗ 0 + 2 1 ∗ 1 + 2 2 ∗ 2 + 2 3 ∗ 3 + . . . . . . + 2 ( h − 2 ) ∗ ( h − 2 ) + 2 ( h − 1 ) ∗ ( h − 1 ) T(n)=2^0*0+2^1*1+2^2*2+2^3*3+......+2^{(h-2)}*(h-2)+2^{(h-1)}*(h-1) T(n)=200+211+222+233+......+2(h2)(h2)+2(h1)(h1)
2 T ( n ) = 2 1 ∗ 0 + 2 2 ∗ 1 + 2 3 ∗ 2 + 2 4 + 3 + . . . . . . + 2 ( h − 1 ) ∗ ( h − 2 ) + 2 h ∗ ( h − 1 ) 2T(n)=2^1*0+2^2*1+2^3*2+2^4+3+......+2^{(h-1)}*(h-2)+2^h*(h-1) 2T(n)=210+221+232+24+3+......+2(h1)(h2)+2h(h1)
在这里插入图片描述
由错位相减得:
T ( n ) = − 2 0 − 2 1 − 2 2 − 2 3 − 2 4 − 2 ( h − 2 ) − 2 ( h − 1 ) + 2 h ∗ ( h − 1 ) T(n)=-2^0-2^1-2^2-2^3-2^4-2^{(h-2)}-2^{(h-1)}+2^h*(h-1) T(n)=20212223242(h2)2(h1)+2h(h1)
T ( n ) = 2 h ∗ ( h − 1 ) − ( 2 0 + 2 1 + 2 2 + 2 3 + 2 4 + 2 ( h − 2 ) + 2 ( h − 1 ) ) T(n)=2^h*(h-1)-(2^0+2^1+2^2+2^3+2^4+2^{(h-2)}+2^{(h-1)}) T(n)=2h(h1)(20+21+22+23+24+2(h2)+2(h1))
T ( n ) = 2 h ∗ ( h − 1 ) − ( 2 h − 1 ) = 2 h ∗ ( h − 2 ) + 1 T(n)=2^h*(h-1)-(2^h-1)=2^h*(h-2)+1 T(n)=2h(h1)(2h1)=2h(h2)+1
因为是满二叉树,所以节点数n与高度h之间存在如下关系:
n = 2 h − 1 n=2^h-1 n=2h1 h = l o g 2 ( n + 1 ) h=log_2(n+1) h=log2(n+1)
所以 T ( n ) = ( n + 1 ) ∗ ( l o g 2 ( n + 1 ) − 2 ) + 1 T(n)=(n+1)*(log_2(n+1)-2)+1 T(n)=(n+1)(log2(n+1)2)+1
因此,得出向上建堆的时间复杂度是O( n ∗ l o g 2 n n*log_2n nlog2n)。

向下调整建堆

堆的删除
向下调整建堆主要是用于在堆数据结构中,堆在删除数据后,为了继续维持堆的结构,所进行的一种调整。
这里要说明的是,堆的删除是指删除堆顶的数据。要删除堆顶的数据并不是像顺序表那样覆盖删除。虽然堆的物理存储结构是顺序结构,但他逻辑上是树形结构,如果直接覆盖删除,本来是兄弟节点的变成父子节点,关系会完全打乱。并不能保证最终覆盖删除之后的结构还是堆的结构。
所以,堆的删除有另一种巧妙的方法。先将堆顶的数据跟堆的最后一个数据进行交换,然后删除堆的最后一个数据,就将堆顶数据删除了。
在这里插入图片描述
但是,堆顶的数据删除了,并没有因此就万事大吉。因为和堆顶数据进行交换的数据,现在处于堆顶位置,并不代表它就是堆中最小或最大的数据,堆的结构可能通过交换后就此被破坏。所以,这里就要通过向下调整算法来恢复堆的结构。
大致调整过程如下:
在这里插入图片描述
要注意的是:向下调整算法有一个前提,左右子树必须都是堆,才能调整。
经过一次向下调整后,本来因为删除数据所导致的堆的结构的破坏就被恢复了。
有了这个思路,可以将过程实现如下:

//a - 堆的顺序存储数组
//size - 堆的顺序存储数组的数据个数
//parent - 需要进行向下调整的双亲下标
void AdjustDown(HDataType* a, int size, int parent)
{
	assert(a != NULL);

	//此时child代表左孩子
	int child = 2 * parent + 1;
	
	//孩子下标在数组范围内进入循环
	while (child < size)
	{
		//child+1代表右孩子
		//建小堆 - 右孩子存在的话,左右孩子谁更小,child存储的就是谁的下标变量
		if (child + 1 < size && a[child + 1] < a[child])
		{
			++child;
		}
		
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}

以上只是对一个数据进行的调整,要想实现堆的创建,这里我们从倒数第一个非叶子节点所形成的的子树开始调整,一直调整到根节点的树,就可以调整成堆。所以可以更进一步,完成一棵完全二叉树到堆的创建。

//n-1是最后一个数据的下标,((n-1)-1)/2是倒数第一个非叶子节点的下标
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
	AdjustDown(a, n, i);
}

以上就是向下调整建堆的过程,我们可以分析一下它的时间复杂度。
同上,采取满二叉树的结构来进行复杂度的计算。

假设树的高度为h
第1层, 2 0 2^0 20个节点,需要向下调整h-1层。
第2层, 2 1 2^1 21个节点,需要向下调整h-2层。
第3层, 2 2 2^2 22个节点,需要向下调整h-3层。
第4层, 2 3 2^3 23个节点,需要向下调整h-4层。

第h-1层, 2 h − 2 2^{h-2} 2h2个节点,需要向下调整1层。
第h层, 2 h − 1 2^{h-1} 2h1个节点,需要向下调整0层。

则需要移动节点总的移动步数为:
T ( n ) = 2 0 ∗ ( h − 1 ) + 2 1 ∗ ( h − 2 ) + 2 2 ∗ ( h − 3 ) + 2 3 ∗ ( h − 4 ) + . . . . . . + 2 ( h − 2 ) ∗ 1 + 2 ( h − 1 ) ∗ 0 T(n)=2^0*(h-1)+2^1*(h-2)+2^2*(h-3)+2^3*(h-4)+......+2^{(h-2)}*1+2^{(h-1)}*0 T(n)=20(h1)+21(h2)+22(h3)+23(h4)+......+2(h2)1+2(h1)0
2 T ( n ) = 2 1 ∗ ( h − 1 ) + 2 2 ∗ ( h − 2 ) + 2 2 ∗ ( h − 3 ) + 2 4 ∗ ( h − 4 ) + . . . . . . + 2 ( h − 1 ) ∗ 1 + 2 h ∗ 0 2T(n)=2^1*(h-1)+2^2*(h-2)+2^2*(h-3)+2^4*(h-4)+......+2^{(h-1)}*1+2^h*0 2T(n)=21(h1)+22(h2)+22(h3)+24(h4)+......+2(h1)1+2h0
在这里插入图片描述
由错位相减得:
T ( n ) = ( 1 − h ) + 2 1 + 2 2 + 2 3 + 2 4 + 2 ( h − 2 ) + 2 ( h − 1 ) T(n)=(1-h)+2^1+2^2+2^3+2^4+2^{(h-2)}+2^{(h-1)} T(n)=(1h)+21+22+23+24+2(h2)+2(h1)
T ( n ) = 2 0 + 2 1 + 2 2 + 2 3 + 2 4 + 2 ( h − 2 ) + 2 ( h − 1 ) − h T(n)=2^0+2^1+2^2+2^3+2^4+2^{(h-2)}+2^{(h-1)}-h T(n)=20+21+22+23+24+2(h2)+2(h1)h
T ( n ) = 2 h − 1 − h T(n)=2^h-1-h T(n)=2h1h
因为是满二叉树,所以节点数n与高度h之间存在如下关系:
n = 2 h − 1 n=2^h-1 n=2h1 h = l o g 2 ( n + 1 ) h=log_2(n+1) h=log2(n+1)
所以 T ( n ) = n − l o n g 2 ( n + 1 ) T(n)=n-long_2(n+1) T(n)=nlong2(n+1)
因此,得出向下建堆的时间复杂度是O(n)。
通过比较,向上建堆的时间复杂度是O( n ∗ l o g 2 n n*log_2n nlog2n),向下建堆的时间复杂度是O(n),所以采取向下建堆会更优一些。
以上过程都是对小堆的建立,如果要建大堆,只需要将判断条件略作更改即可:

//建小堆
if (a[child] < a[parent])
if (child + 1 < size && a[child + 1] < a[child])
if (a[child] < a[parent])

//建大堆:
if (a[child] > a[parent])
if (child + 1 < size && a[child + 1] > a[child])
if (a[child] > a[parent])

堆的操作链接

最后,对于堆的各种操作需求,可以参考阿顺的这篇堆(C语言实现)

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

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

相关文章

C++语法规则3(C++面向对象)

多态 C多态意味着调用成员函数时&#xff0c;会根据调用函数的对象的类型来执行不同的函数&#xff1b; 形成多态必须具备三个条件&#xff1a; 必须存在继承关系&#xff1b;继承关系必须有同名虚函数&#xff08;其中虚函数是在基类中使用关键字 virtual 声明的函数&#…

网络编程、通信

目录 网络通信三要素 三要素概述、要素一&#xff1a;IP地址 IP地址操作类-InetAddress 要素二&#xff1a;端口号 要素三&#xff1a;协议 UDP通信 UDP通信&#xff1a;快速入门 UDP通信&#xff1a;多发多收 UDP通信-广播、组播 TCP通信-快速入门 编写客户端代码 …

图文详解红黑树,还有谁不会?

前言在MySQL中&#xff0c;无论是Innodb还是MyIsam&#xff0c;都使用了B树作索引结构(这里不考虑hash等其他索引)。本文将从最普通的二叉查找树开始&#xff0c;逐步说明各种树解决的问题以及面临的新问题&#xff0c;从而说明MySQL为什么选择B树作为索引结构。目录一、二叉查…

深度学习 Day26——使用Pytorch实现猴痘病识别

深度学习 Day26——使用Pytorch实现猴痘病识别 文章目录深度学习 Day26——使用Pytorch实现猴痘病识别一、前言二、我的环境三、前期工作1、设置GPU导入依赖项2、导入猴痘病数据集3、划分数据集四、构建CNN网络五、训练模型1、设置超参数2、编写训练函数3、编写测试函数4、正式…

2023年江苏省职业院校技能大赛中职网络安全赛项试卷-教师组任务书

2023年江苏省职业院校技能大赛中职网络安全赛项试卷-教师组任务书 一、竞赛时间 9:00-12:00&#xff0c;12:00-15:00&#xff0c;15:00-17:00共计8小时。 二、竞赛阶段 竞赛阶段 任务阶段 竞赛任务 竞赛时间 分值 第一阶段 基础设施设置与安全加固、网络安全事件响应、数…

REDIS16_LRU算法概述、查看默认内存、默认是如何删除数据、缓存淘汰策略

文章目录①. LRU算法概述②. 查看默认内存③. 如何删除数据④. 缓存淘汰策略①. LRU算法概述 ①. LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的数据给予淘汰 (leetcode-cn.com/problems/lru-cache) ②. LRU算法题来源 ③.…

8大主流编程语言的适用领域,你可能选错了语言

很多人学编程经常是脑子一热然后就去网上一搜资源就开始学习了&#xff0c;但学到了后面发现目前所学的东西并不是自己最喜欢的&#xff0c;好像自己更喜欢另一个技术&#xff0c;感觉自己学错了&#xff0c;于是乎又去学习别的东西。 结果竹篮打水一场空&#xff0c;前面所付…

RxJava操作符变换过程

要使用Rxjava首先要导入两个包&#xff0c;其中rxandroid是rxjava在android中的扩展 implementation io.reactivex:rxandroid:1.2.1implementation io.reactivex:rxjava:1.2.0我们在使用rxjava的操作符时都觉得很方便&#xff0c;但是rxjava是怎么实现操作符的转换呢&#xff0…

不会编程也能搭建聊天机器人?ChatGPT + Notion AI助你一臂之力!

体验链接&#xff1a;Aitrend ChatBot (无需环境&#xff0c;无需账号&#xff0c;打开即用&#xff0c;完全免费&#xff0c;回答能力同ChatGPT) 背景介绍 本文主要谈关于如何使用ChatGPT&#xff08;实际使用主力工具为Notion AI&#xff09;&#xff0c;应用官方API接口&…

Seay代码审计工具

一、简介Seay是基于C#语言开发的一款针对PHP代码安全性审计的系统&#xff0c;主要运行于Windows系统上。这款软件能够发现SQL注入、代码执行、命令执行、文件包含、文件上传、绕过转义防护、拒绝服务、XSS跨站、信息泄露、任意URL跳转等漏洞&#xff0c;基本上覆盖常见PHP漏洞…

0109二分图-无向图-数据结构和算法(Java)

文章目录1 概念2 API3 分析和实现4 测试5 总结后记1 概念 二分图是一种能将所有结点分为两部分的图&#xff0c;其中图的每条边所连接的两个顶点都分别属于不同的部分。 2 API public classBipartiteBipartite(Graph G)预处理函数public booleanisBipartitle()是否是二分图pub…

【opensea】opensea-js 升级 Seaport v1.4 导致的问题及解决笔记

一、opensea 协议升级导致旧包不能使用了 我使用的是 “opensea-js”: "^4.0.12” 版本当SDK。于2023年3月9日之后&#xff0c;不能使用了&#xff0c;需要升级到 Seaport v1.4 协议的包。 报错如下: Error: API Error 400: Please provide an OPEN order type when us…

可逆神经网络的研究及其在图像中应用

一、摘要 可逆神经网络(INN)自被提出以来&#xff0c;就受到了广泛关注。由于其双射构造和高效可逆性&#xff0c;INN被用于各种推理任务&#xff0c;如图像隐藏、图像重缩放、图像着色、图像压缩和视频超分辨率等等。本文针对最新关于INN在图像方面应用的文献进行介绍&#x…

day30_JS

今日内容 上课同步视频:CuteN饕餮的个人空间_哔哩哔哩_bilibili 同步笔记沐沐霸的博客_CSDN博客-Java2301 零、 复习昨日 一、作业 二、BOM 三、定时器 四、正则表达式 零、 复习昨日 事件 事件绑定方式鼠标事件 onmouseoveronmouseoutonmousemove 键盘事件 onkeydownonkeyupon…

一文带你深入理解【Java基础】· Java反射机制(下)

写在前面 Hello大家好&#xff0c; 我是【麟-小白】&#xff0c;一位软件工程专业的学生&#xff0c;喜好计算机知识。希望大家能够一起学习进步呀&#xff01;本人是一名在读大学生&#xff0c;专业水平有限&#xff0c;如发现错误或不足之处&#xff0c;请多多指正&#xff0…

aws dynamodb 使用awsapi和PartiQL掌握dynamodb的CRUD操作

总结一下 dynamodb通常和java等后端sdk结合使用使用的形式可以是api或partiql语法调用dynamodb的用法不难&#xff0c;更重要的是维护成本&#xff0c;所需的服务集成&#xff0c;技术选型等和大数据结合场景下有独特优势 之后可能再看看java sdk中DynamoDBMapper的写法&…

登入vCenter显示503,证书过期解决办法

登入vCenter显示503 原因&#xff1a;当安全令牌服务 &#xff08;STS&#xff09; 证书已过期时&#xff0c;会出现这些问题。这会导致内部服务和解决方案用户无法获取有效令牌&#xff0c;从而导致无法按预期运行&#xff08;证书两年后就会过期&#xff09;。 解决办法&…

Yocto系列讲解[技巧篇]90 - toolchain交叉编译器SDK中安装的软件

By: fulinux E-mail: fulinux@sina.com Blog: https://blog.csdn.net/fulinus 喜欢的盆友欢迎点赞和订阅! 你的喜欢就是我写作的动力! 目录 问题背景toolchain生成回顾toolchain sdk安装方法1:安装libmyapi到SDK方法2:安装libmyapi到SDK演示过程返回总目录:Yocto开发讲解系…

Linux 学习笔记——二、主机规划与磁盘分区

一、Linux 与硬件的搭配 Linux 中所有设备均被视为文件&#xff0c;其命名规则如下&#xff1a; 设备文件名SCSI/SATA/USB 硬盘机/dev/sd[a-p]USB 闪存盘/dev/sd[a-p]&#xff08;与 SATA 相同&#xff09;Virtl/O 界面/dev/vd[a-p]&#xff08;用于虚拟机内&#xff09;软盘…

RabbitMQ高级特性

RabbitMQ高级特性 消息可靠性投递 Consumer ACK 消费端限流 TTL 死信队列 延迟队列 日志与监控 消息可靠性分析与追踪 管理 消息可靠性投递 在使用 RabbitMQ 的时候&#xff0c;作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ 为我们提供了两种方式用来控制…