数据结构——堆

2023-09-16 21:27:04

1. 什么是堆

树和二叉树一文中可以了解到:

堆通常是一个可以被看作一棵完全二叉树的数组对象。

  • 堆的逻辑结构是一棵完全二叉树
  • 堆的物理结构是一个数组

如图所示:


2. 堆的两个特性

  • 结构性:用数组表示的完全二叉树

  • 有序性:任意节点的关键值是其子树所有节点的最大值(或最小值)

    • ”最大堆(MaxHeap)“,也叫”大顶堆“:最大值,即每棵子树的根节点的值都大于等于其子节点的值
    • ”最小堆(MaxHeap)“,也叫”小顶堆“:最小值,即每棵子树的根节点的值都小于等于其子节点的值

我们通过一道例题来加深对堆的理解:

下列关键字序列中,序列 () 是堆
A.(16,72,31,23,94,53)
B.(94,23,31,72,16,53)
C.(16,53,23,94,31,72)
D.(16,23,53,31,94,72)

我们可以通过画图来分析:

通过分析我们可以知道,只有序列D符合堆的条件,其每个子树的父亲都小于它的孩子,因此这也是一个小堆。


3. 父子节点下标之间的关系

我们定义父亲为parent,左右孩子为leftchild, rightchild

  • 我们可以通过数组的下标来确定父子节点的关系,结论如下

leftchild = parent * 2 + 1

rightchild = parent * 2 + 2

parent = (child - 1) / 2


4. 堆的实现

由于堆的物理存储结构是一个数组,因此我们可以像定义顺序表一样定义堆的结构:

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	int size;	//记录当前数据个数
	int capacity;	//记录最大容量
}Heap;

4.1 堆的插入HeapPush

向堆中插入数据,在内存中也就相当于向一个数组插入数据,而为了确保效率,显然我们应该将新数据val插入到数组尾部

如图所示:

此时我们发现,数据10插入后,这一串数据构成的完全二叉树就不满足堆的性质了。因此为了确保堆结构的正确性,每插入一次数据,我们都要重新对堆进行调整

4.1.1 向上调整算法AdjustUp:

调整的整体原则是:

  • 如果原来是小堆,并且插入的数据val小于父亲parent,那么就将valparent调换位置。继续向根部向上调整,直到满足所有的父亲小于等于它的孩子。

  • 大堆同理。

例如:

在这里插入图片描述

4.1.2 AdjustUp代码实现(以大堆为例)

void AdjustUp(HPDataType* a, int child)
{
    //新插入数据的父亲
	int parent = (child - 1) / 2;

    //开始向上调整
	while (child > 0)
	{
        //如果孩子大于父亲,那就要交换
        //并继续向上调整
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);

			child = parent;
			parent = (parent - 1) / 2;
		}
		
        //否则说明已经符合堆的性质,退出循环
		else
			break;
	}
}

4.1.3 HeapPush实现

知道了如何向上调整,那么堆的插入就很简单了。只需要在插入新的数据后进行一次向上调整就行。

void HeapPush(Heap* hp, HPDataType x)
{
	//检查容量
	if (hp->size == hp->capacity)
	{
		hp->capacity *= 2;
		HPDataType* temp = (HPDataType*)realloc(hp->a, sizeof(HPDataType) * hp->capacity);
		if (NULL == temp)
		{
			perror("realloc");
			exit(-1);
		}
	}

	hp->a[hp->size] = x;
	hp->size++;

	AdjustUp(hp->a, hp->size - 1);
}

4.1.4 时间复杂度

对于每一次插入,向上调整时最多调整二叉树的高度次,假设二叉树有N个节点,那么它的高度h就是log2(N+1),即时间复杂度为O(logN)

4.2 堆的删除HeapPop

首先我们应该思考,对于构成堆的数组,我们应该删除数组的第一个元素还是最后一个元素?

可能有的小伙伴觉得为了使删除的效率最高,应该删除数组的最后一个元素,但是这种做法是没有任何意义的。

应该清楚,堆顶元素(即数组的首元素)要么是整个数组的最大值,要么是整个数组的最小值,因此对这一个数进行操作才能有较大的意义

正确的删除操作HeapPop应该是这样的:

  • 删除的数据应该是堆顶元素
  • 将堆顶元素(即数组首元素)和数组最后一个元素调换位置
  • 再删除最后一个元素

如图:

在这里插入图片描述

显然我们可以看到,和插入操作一样,删除操作后仍不能保证堆结构的正确性,因此我们仍需要重新对堆进行调整

4.2.1 向下调整算法AdjustDown

删除操作中我们将原来数组的最后一个元素放到了堆顶,从而导致堆顶数据不一定满足堆的条件,因此我们也要从堆顶开始向下调整

调整的整体原则是:

如果原来是小堆:将堆顶数据和左右孩子较小的那一个比较,如果堆顶数据大于较小值,那么就交换这两个数的位置。继续向下调整,直到满足堆的条件

大堆同理。

如图:

在这里插入图片描述

4.2.2 AdjustDown实现(大堆)

void AdjustDown(HPDataType* a, int parent, int n)
{
    //先默认较大的孩子为左孩子
	int child = parent * 2 + 1;

	while (child < n)
	{
        //如果右孩子存在并且右孩子大于左孩子
        //那么较大的孩子应该是右孩子
		if (child + 1 < n && a[child + 1] > a[child])
			child = child + 1;

		//如果父亲小于孩子,那就交换父亲和孩子
        //并继续向下调整
        if (a[parent] < a[child])
		{
			Swap(&a[parent], &a[child]);

			parent = child;
			child = child * 2 + 1;
		}
        //否则已经满足堆的条件,退出循环
		else
			break;
	}
}

4.2.3 HeapPop实现

和插入类似,只要每删除一次数据就进行一次向下调整,就可以保证HeapPop的正确了:

void HeapPop(Heap* hp)
{
    //交换堆顶元素和数组最后一个元素
	Swap(&(hp->a[0]), &(hp->a[hp->size - 1]));

    //更新大小
	hp->size--;

    //向下调整
	AdjustDown(hp->a, 0, hp->size);
}

4.2.4 时间复杂度

和向上调整一样,对于每一次删除,向下调整最多调整高度次,因此时间复杂度为O(logN)

4.3 堆的构建

应该清楚,执行堆的插入和删除之前,堆已经被建立。换一句话说,如果任意给一组数据,同时要对其进行堆的插入删除操作,首先就要确保这一组数据是堆。因此掌握如何建堆是十分重要的。

有两种方式建堆:

4.3.1 向上调整建堆

实现堆的插入时,我们用向上调整的方式来建堆。同样,对于任意一个给定的数组,我们也可以用类似“插入”的方法来建堆。

具体方法(以建大堆为例):

  • 遍历数组
  • 数组首元素可以默认为一个大堆
  • 从第二个元素开始遍历,可以看作每遍历一个数据,就将这个数据HeapPush到堆中
  • 直到遍历完整个数组
for (int i = 1; i < n; i++)
    AdjustUp(hp->a, i);
  • 时间复杂度:O(NlogN)

4.3.2 向下调整建堆

也可以采用分治的思想:如果构成二叉树的所有子树都是堆,那么这棵二叉树也就一定是堆。

那么我们就可以从最后一棵子树开始,用向下调整的方法建堆

具体方法:

  • 最后一个元素的下标为size - 1,其对应父亲parent的下标为(size - 1 - 1)/2
  • parent开始向前遍历数组,对每一棵子树进行向下调整
  • 直到遍历完整个数组
int parent = (hp->size - 1 - 1) / 2;
while (parent >= 0)
{
    AdjustDown(hp->a, parent, hp->size);

    parent--;
}
  • 时间复杂度:O(N)

5. 堆的应用

堆的主要用途有堆排序和处理TopK问题,感兴趣的小伙伴可以点击下面的链接进行进一步了解:

👉堆排序

👉TopK问题

更多推荐

React、Vue框架如何实现组件更新,原理是什么?

引言React和Vue都是当今最流行的前端框架,它们都实现了组件化开发模式。为了优化性能,两者都采用了虚拟DOM技术。当组件状态发生改变时,它们会使用虚拟DOM进行局部渲染比对,只更新必要的DOM节点,从而避免重新渲染整个组件树。本文将从React和Vue的组件更新原理入手,剖析两者虚拟DOMdifer算法的异同点。R

C语言指针详解

C语言指针详解字符指针1.如何定义2.类型和指向的内容3.代码例子指针数组1.如何定义2.类型和内容数组指针1.如何定义2.类型和指向类型3.数组名vs&数组名数组指针运用数组参数&指针参数一维数组传参二维数组传参一级指针传参二级指针传参函数指针1.如何定义2.类型和指向内容3.函数名vs&函数名4.两个有趣的代码函数

2023 电赛 E 题 K210 方案

第一章:K210介绍K210芯片是一款基于RISC-V架构的嵌入式人工智能芯片,具备低功耗、高性能的特点。它拥有强大的图像处理和机器学习能力,适用于边缘计算设备和物联网应用。为了方便开发者,K210芯片提供了丰富的外设接口,包括摄像头接口、显示接口、WiFi、蓝牙等,同时支持多种编程语言和开发环境,如MicroPyth

天气API强势对接

🤵‍♂️个人主页:@香菜的个人主页,加ischongxin,备注csdn✍🏻作者简介:csdn认证博客专家,游戏开发领域优质创作者,华为云享专家,2021年度华为云年度十佳博主🐋希望大家多多支持,我们一起进步!😄如果文章对你有帮助的话,欢迎评论💬点赞👍🏻收藏📂加关注+目录前言:墨迹天气数据接口1、找到目

【E题】2023年电赛运动目标控制与自动追踪系统方案

系统的设计和制作可以按照以下步骤进行:设计红色光斑位置控制系统:选择合适的红色激光笔,并将其固定在一个二维电控云台上。使用电机和编码器来控制电控云台的水平和垂直运动。设计一个控制电路,可以通过输入控制信号来控制电机的运动,从而控制红色光斑的位置。确保控制电路可以接收来自用户输入的目标位置信息,并将其转换为相应的电机控制

代客泊车对HUT功能交互规范

目录1.版本记录...72.文档范围和控制...82.1目的/范围...82.2文档冲突...82.3文档授权...82.4文档更改控制...83.系统组成...93.1IPAS系统(环视和超声波雷达)...93.2融合泊车系统(环视和泊车)...104.AVM与HUT系统交互...114.1系统框图...114.2接

MyBatis查询数据库1(概念+创建项目+基础交互)

目录1.MyBatis是什么?2.为什么学习MyBatis?3.怎么学MyBatis4.第⼀个MyBatis查询4.1添加MyBatis框架支持4.1.1老项目添加MyBatis4.1.2新项目添加MyBatis4.2配置连接字符串和MyBatis4.2.1配置连接字符串4.2.2配置MyBatis中的XML路径5.使

进程转态及其转换过程

一.进程转态及其转换过程在Linux操作系统中,进程的状态可以相互转换,下面是不同状态之间的相互转换:就绪态(ReadyState):当一个进程创建后,它被放入就绪态。此时,进程已经被加载到内存中,并准备好被CPU分配时间片来执行。运行态(RunningState):当就绪态的进程获得CPU时间片后,进程的状态会从就绪

国产手机芯片4G方案_紫光展锐安卓核心板虎贲4G智能模块方案定制

元器件清单即BOM物料清单,不同行业领域的BOM表侧重点不一样。安卓主板的BOM表则侧重点在于元器件物料的清单,也就是安卓电路板的PCBA清单,精密的安卓板有上千个物料,可以帮助我们估算物料成本,建立生产计划,控制库存等。紫光展锐核心板,基于Android操作系统,支持全国产项目快递部署及开发。新移科技研发的紫光展锐平

基于Java+微信小程序实现《电子点餐系统》

博主介绍:✌全网粉丝30W+,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌🍅文末获取源码联系🍅👇🏻精彩专栏推荐订阅👇🏻不然下次找不到哟2022-2024年最全的计算机软件毕业设计选题

网络安全(黑客)自学

前言作为一个合格的网络安全工程师,应该做到攻守兼备,毕竟知己知彼,才能百战百胜。娱乐圈:主要是初中生和高中生较多,玩网恋,人气,空间,建站收徒玩赚钱,技术高的也是有的,只是很少见。技术圈:这个圈子里面的黑客是为了能把黑客技术玩到极致的技术狂人,我最佩服的就是这群人,希望以后自己也能成为这样的人。职业圈:这里面的人群主要

热文推荐