【数据结构】详解堆

news2024/12/27 1:16:55

一、堆的概念

堆(Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵 完全二叉树的 数组对象。 堆是非线性数据结构,相当于一维数组,有两个直接后继。
如果有一个关键码的集合K = { k₀,k₁,k₂ ,k₃ ,…,kₙ₋₁  },把它的所有元素按完全二叉树的顺序存储方式存储,在一个一维数组中,并满足:Kᵢ  <= K₂ *ᵢ₊₁  且 Kᵢ  <= K₂ *ᵢ₊₂  (Kᵢ  >= K₂ *ᵢ₊ ₁ 且 Kᵢ  >= K₂ *ᵢ₊₂ ) i = 0,1,2…,则称为小堆 (或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

【大根堆和小根堆】:

根结点最大的堆叫做大根堆,树中所有父亲都大于或等于孩子。

根结点最小的堆叫做小根堆,树中所有父亲都小于或等于孩子。

共同特点:父亲 =(孩子-1)/2

大堆小堆有什么特点呢?

我们购物平台中,我想选择销量大的前k家,这个时候,我们不需要对所有的数据进行排序,只需要取出前k家最大的值就可以。而最值常常出现在0号位,我们就可以利用Topheap解决,大大减少了我们的时间复杂度;

特点:

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树
  • 每层节点个数为2^(h-1)个

二、堆的创建

1、头文件的声明:

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

void HeapInit(Heap* hp);
// 堆的销毁
void HeapDestory(Heap* hp);

void swap(HPDataType* p1, HPDataType* p2);

void AdjustUp(HPDataType* a, int child);
void AdjustDown(HPDataType* a, int n, int parent);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);

2、代码实现:

2.1堆的初始化与堆的摧毁

//堆的初始化
void HeapInit(Heap* hp) {
	assert(hp);
	hp->a = NULL;
	hp->capacity = hp->size = 0;
}

//堆的摧毁
void HeapDestory(Heap* hp) {
	assert(hp);
	free(hp->a);
	hp->a = NULL;
	hp->capacity = hp->size = 0;
}

2.2堆的插入

下面给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的 向下调整算法 可以把它调整成一个小堆 。向下调整算法有一个前提:左右子树必须是一个堆( 包括大堆和小堆) ,才能调整。

具体步骤如下:

1.将新插入的元素放置在堆的最后一个位置(通常是数组的末尾)。

2.将该元素与其父节点进行比较。

3.若该元素大于(或小于,具体根据堆是最大堆还是最小堆而定)其父节点的值,则交换该元素和其父节点的位置。 

4.继续向上对比和交换,直到满足堆的性质或达到堆的根节点。

// 堆的插入
void HeapPush(Heap* hp, HPDataType x) {
	assert(hp);
    //与顺序表的开辟类似
	if (hp->size == hp->capacity)
	{
		int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
        //使用三目操作符开辟空间大小
		HPDataType* tmp = (HPDataType*)realloc(hp->a, newcapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}

		hp->a = tmp;
		hp->capacity = newcapacity;
	}
	hp->size++;
	hp->a[hp->size] = x;
//向上调整法,因为每次的插入是在数组末尾
//每次插入需要与父亲比较大小交换
	AdjustUp(hp->a, hp->size - 1);
}
2.2.1向上调整法 

//向上调整法,因为每次的插入是在数组末尾
//每次插入需要与父亲比较大小交换
    AdjustUp(hp->a, hp->size - 1);

我们每次插入末尾的位置,相当于孩子,我们需要找到该孩子的父亲与之比较大小,这个时候就要利用堆的特点:父亲 =(孩子-1)/2
 向上调整法:

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 = (child - 1) / 2;
		}
		else{
			break;
		}
	}
}

 交换函数:

因为在堆的实现中我们会经常使用父子交换

void swap(HPDataType* p1, HPDataType* p2) {
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
    //这里也可以回想前面学习
    //不使用第三个变量,利用换位与实现交换
}

2.4堆的删除

如果我们直接删除堆顶数据,将数组数据整体向前移动,这样会导致堆的乱序;

删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法

2.4.1向下调整法

void AdjustDown(HPDataType* a, int n, int parent) {
    //注:这里的parent为0,而数组a则是首尾交换的
	int child = parent * 2 + 1;
	//假设左孩子小

	while (child < n) {
        //while循环直到超出数组长度
		//左孩子大
		if (a[child] > a[child + 1]) {
			child++;
		}
		if (a[child]>a[parent]) {
			swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
	}
}

这样我们只需要完成交换,传指就可以了

void HeapPop(Heap* hp) {
	assert(hp);
	assert(hp->size);
	swap(&hp->a[0], &hp->a[hp->size - 1]);
	hp->size--;

	AdjustDown(hp->a, hp->size, 0);
}

3、总结

升序:建大堆

降序:建小堆

(1)升序:建大堆

【思考】排升序,建小堆可以吗?-- 可以(但不推荐)。

首先对 n 个数建小堆,选出最小的数,接着对剩下的 n-1 个数建小堆,选出第二小的数,不断重复上述过程。

【时间复杂度】建 n 个数的堆时间复杂度是 O(N),所以上述操作时间复杂度为 O(N²),效率太低,关系变得杂乱,尤其是当数据量大的时候,效率就更低。同时堆的价值也没有被体现出来,这样不如用直接排序。

排升序,因为数字依次递增,需要找到最大的数字,得建大堆。

首先对 n 个数建大堆。将最大的数(堆顶)和最后一个数交换,把最大的数放到最后。前面 n-1 个数的堆结构没有被破坏(最后一个数不看作在堆里面的),根节点的左右子树依然是大堆,所以我们进行一次向下调整成大堆即可选出第 2 大的数,放到倒数第二个位置,然后重复上述步骤。

【时间复杂度】:建堆时间复杂度为 O(N),向下调整时间复杂度为 O(log₂N),这里我们最多进行N-2 次向下调整,所以堆排序时间复杂度为 O(N*log₂N),效率相较而言是很高的。

因为在堆的实现中我们会经常使用父子交换

void swap(HPDataType* p1, HPDataType* p2) {
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
    //这里也可以回想前面学习
    //不使用第三个变量,利用换位与实现交换
}

我相信接下来对你来说简直轻而易举

// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);

4.1取堆顶数据

HPDataType HeapTop(Heap* hp) {
	assert(hp);
	assert(hp->size);
	return hp->a[0];
}

4.2 堆的数据个数

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

4.3堆的判空

int HeapEmpty(Heap* hp) {
	assert(hp);
	assert(hp->size>0);
	//为NULL,返回1,不为NULL,返回0;
	return hp->size == 0 ? 1 : 0;
}

5、堆的时间复杂度

5.1建堆的时间复杂度

5.1.1向下调整法建堆
// 堆的插入
void HeapPush(Heap* hp, HPDataType x) {
	assert(hp);
    //与顺序表的开辟类似
	if (hp->size == hp->capacity)
	{
		int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
        //使用三目操作符开辟空间大小
		HPDataType* tmp = (HPDataType*)realloc(hp->a, newcapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
 
		hp->a = tmp;
		hp->capacity = newcapacity;
	}
	hp->size++;
	hp->a[hp->size] = x;
//向下调整法,因为每次的插入是在数组末尾
//每次插入需要与父亲比较大小交换
	AdjustUp(hp->a, hp->size - 1);
}

  因此:建堆的时间复杂度为O(N)

等比数列求和公式: 
建堆要从倒数第一个非叶子节点开始调整,也即是从倒数第二层开始调,可得出时间复杂度公式:T ( n ) = ∑ ( 每 层 节 点 数 ∗ ( 堆 的 高 度 − 当 前 层 数 ) ) 
建堆的时间复杂度为O(N)。(向下调整算法)

为何使用向下调整建堆而不使用向上调整建堆

5.1.2向上调整法建堆

可以看出结点数多的层, 调整次数也多, 结点数少的层, 调整次数少, 时间复杂度为O(N*logN), 所以一般建堆都采用向下调整建堆法. 

6、TOPK问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大

比如:专业前十、世界500强、销量最高的前十、富豪榜等等

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能

数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

1. 用数据集合中前K个元素来建堆

前k个最大的元素,则建小堆

前k个最小的元素,则建大堆

2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素

将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

 我们常常会想到冒泡排序( O(N^2) )
对于少量的数据是可以拿捏的,但面对100000的数据就显得有点吃力,而堆排序O(N*logN)则觉得轻而易举

 

7、堆排序  

void HeapSort(int* a, int n)
{
	// 降序,建小堆
	// 升序,建大堆
	// 向上调整建堆 O(N*logN)
	/*for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}*/
	// 向下调整建堆 O(N)
	for (int i = (n-1-1)/2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}
	// O(N*logN)
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

哇呜!点个赞走吧!!!

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

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

相关文章

统信UOS

统信UOS 如何检测是否为适配的cpu 打开国产系统或linux系统的终端&#xff0c; 输入&#xff1a; uname -m 如&#xff1a; 软件安装 把下载的压缩包解压到当前目录 如果您确认电脑cpu为ARM64架构CPU:【飞腾、鲲鹏、海思麒麟、瑞芯微】&#xff0c; 请双击com.agicall.ub-ph…

JVM--双亲委派模型

1.双亲委派模型 定义&#xff1a; 站在Java虚拟机的角度来看&#xff0c;只存在两种不同的类加载器&#xff1a;一种是启动类加载器&#xff08;Bootstrap ClassLoader&#xff09;&#xff0c;这个类加载器使用C语言实现&#xff0c;是虚拟机自身的一部分&#xff1b;另外一种…

【C++】一、Visual Studio 2017使用教程:内存窗口、预处理文件、obj文件,调试优化

文章目录 概述编译期&#xff08;Compile&#xff09;查看预处理后的文件查看obj文件开启编译器调试优化 链接期&#xff08;Linking&#xff09;报错信息概述自定义入口点 调试内存窗口值转16进制查看查看汇编代码 注意 概述 记录一下Cherno的vs配置下载地址 https://thecher…

Kubernetes集群安装步骤

前言&#xff1a;本博客仅作记录学习使用&#xff0c;部分图片出自网络&#xff0c;如有侵犯您的权益&#xff0c;请联系删除 一、安装要求 在开始之前&#xff0c;部署Kubernetes集群集群需要满足以下几个条件&#xff1a; 一台多多台机器&#xff0c;操作系统CentOS.x-86_x…

分布式锁、Lua脚本、redisson、运行lua脚本优化代码

20240721 一、分布式锁1. 什么是分布式锁2. 分布式锁的实现3. 基于redis的分布式锁4 总结 二、对于lua脚本可以保证事务&#xff0c;要么成功要么失败。1. 在redis中调用lua脚本 三、Redisson1 步骤2. Redisson的总结3. 几种分布式锁的区别 三、优化我们的秒杀1. 我们在创建优惠…

Mybatis-Plus代码生成器配置方法

Mybatis-Plus网址&#xff1a;https://baomidou.com/pages/779a6e/#%E4%BD%BF%E7%94%A8 第一步&#xff1a;引入依赖 <!-- 代码生成器 --> <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId>…

IFIX6.5变量创建进阶-标签组用法

IFIX6.5变量创建进阶-标签组用法 普通变量创建 https://blog.csdn.net/hmxm6/article/details/140507111 OPC数据连接 项目介绍和应用场景 在实际项目中可能有好几个相同标签变量属于一类的 比如现在有三台风机&#xff0c;都有启动停止风量的三个数据 数据库里面需要创…

EI成稿丨明年上检索

基于xxxx嵌入式系统的无线传感网络路由优化算法研究基于网络安全xxxx全球互联网经济发展趋势分析基于深度学习的大数据xxxxx基于全IP物联网网络xxxxx安全管理中的优化研究xxxxx农业可持续发展评价及升级路径研究xxxxx分层轮廓数字化转型风险评估与防范策略基于Kano模型与xxxxx公…

防火墙--内容安全

目录 概述 IAE引擎流程 DPI深度包检测 基于特征字的检测技术 基于应用网关的检测技术 基于行为模式的检测技术 DFI深度流检测 基于数据流进行识别检测的技术 DPI和DFI对比 IDS&#xff08;入侵检测&#xff09; IPS&#xff08;入侵防御&#xff09; 优势 入侵检测…

使用百度语音技术实现文字转语音

使用百度语音技术实现文字转语音 SpringBootVue前后端分离项目 调用api接口需要使用AK和SK生成AccessToken,生成getAccessToken的接口有跨域限制,所以统一的由后端处理了 部分参数在控制台->语音技术->在线调试里面能找到 Controller RestController RequestMapping(&q…

测试——Junit

内容大纲: 常用的五个注解 测试用例顺序指定 参数化 测试套件 断言 1. 常用的五个注解 1.1 Test 通常情况下,我们输入要写在main方法下,此时我想直接输出: Test void Test01(){System.out.println("第一个测试用例"); } 1.2 BeforeAll AfterAll BeforeALL在Tes…

Unity: TextMeshPro生成中文字体(附3.5k,7k,2w常用字集)

免费常用3千5&#xff0c;7千字&#xff0c;2万字中文字体包 1.选择Window/TextMeshPro/Font Asset Creator 注&#xff1a;准备字体&#xff1a;从字体库或其他来源获取中文字体文件&#xff0c;通常为.ttf、.otf或.ttc格式。最简单的方式是从Windows系统文件的Font文件夹里…

NV Switch 深度解析与性能剖析

NV Switch 深度解析与性能剖析 在当今高性能计算领域&#xff0c;英伟达&#xff08;NVIDIA&#xff09;的GPU技术犹如璀璨明星般闪耀。随着人工智能和机器学习技术的迅猛发展&#xff0c;对计算能力的需求不断攀升&#xff0c;实现GPU间的高效互联互通变得至关重要。正因如此&…

C# 实现跨进程条件变量

C# 进程通信系列 第一章 共享内存 第二章 条件变量&#xff08;本章&#xff09; 第三章 消息队列 文章目录 C# 进程通信系列前言一、关键实现1、用到的主要对象2、初始化区分创建和打开3、变量放到共享内存4、等待和释放逻辑 二、完整代码三、使用示例1、同步控制2、跨进程控…

通义千问大模型API调用示例

通义千问是由阿里云自主研发的大语言模型&#xff0c;用于理解和分析用户输入的自然语言。 模型概览 模型名称模型简介模型输入/输出限制qwen-turbo通义千问超大规模语言模型&#xff0c;支持中文、英文等不同语言输入模型支持8k tokens上下文&#xff0c;为了保证正常的使用…

Python数据可视化------动态柱状图

一、基础柱状图 # 基础柱状图 # 导包 from pyecharts.charts import Bar from pyecharts.options import *# 构建柱状图 bar Bar() # 添加数据&#xff08;列表&#xff09; x_list ["张三", "李四", "王五", "赵六"] y_list [50,…

vue引用js html页面 vue引用js动态效果

要引用的index.html页面&#xff1a;&#xff08;资源来自网络&#xff09;在pubilc下建一个static文件放入js文件 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><title>数字翻转</title><meta con…

基于SpringBoot+Vue的校园志愿者管理系统(带1w+文档)

基于SpringBootVue的校园志愿者管理系统(带1w文档) 基于SpringBootVue的校园志愿者管理系统(带1w文档) 本次设计任务是要设计一个校园志愿者管理系统&#xff0c;通过这个系统能够满足管理员和志愿者的校园志愿者信息管理功能。系统的主要功能包括首页、个人中心、志愿者管理、…

多线程应用

并发与并行 计算机操作系统对于并发性和并行性的概念给出的定义是&#xff1a; 并行性是指两个或多个事件在同一时刻发生&#xff1b; 并发性是指两个或多个事件在同一时间段内发生。 并发是指多个任务(线程)都请求运行&#xff0c;如果系统只有一个CPU,CPU只能按受一个任务&…

java学习--断点调试

可进入调用的方法里看源码