堆排序以及TOP-K问题

news2025/1/12 22:58:47

片头

嗨!小伙伴们,大家好!今天我们来深入理解堆这种数据结构,分析一下堆排序以及TOP-K问题,准备好了吗?我要开始咯!

一、堆排序

这里我们先假设要排成升序,也就是从左到右,结点的值依次增大

思路一:①先有这个数据结构,②给定一个数组arr, 我们可以把arr数组里面的元素全部拷贝到堆中,然后利用堆自身向下调整算法来进行排序,排成小堆,排好序后,再逐一拷贝回arr数组。

 向下调整算法有一个前提:左右子树必须是堆

采用向下调整算法,从第一个结点(下标为0)开始,逐个进行比较,如果子节点比父节点大,则交换

第一次:

第二次:

 

第三次:

好啦,了解完向下调整算法后,那什么是向下调整建堆呢?

举个例子,接下来的内容可要仔细听好咯~

假设我们需要建立大堆,我们可以保持最后一层不动,也就是叶子结点的那一层不变,调整它的上一层,也就是从倒数第一个叶子结点的父节点开始向下调整,比较父节点的左孩子和右孩子,如果孩子结点比父节点大,那么交换,然后比较下一个父节点和它的孩子结点。

第一次:最后一个节点的下标为size-1,那么它的父节点(倒数第一个非叶子结点)的下标为(size-1-1)/2 , 比较父节点的左孩子和右孩子

第二次:从倒数第一个非叶子结点依次往前找父节点,也就是 (size-1-1)/2 -1 ,然后比较它的左孩子和右孩子

此时我们比较“70”的左孩子“50”和右孩子“32”,发现左右孩子都比父节点的值小,因此我们不作处理,继续往前寻找父节点。

第三次:往前找父节点,也就是 (size-1-1)/2 -1 -1, 我们找到了“60”这个父节点,这里有一个隐藏的细节,不知道大家发现了没:“60”这个结点的左右子树都是大堆,这时,比较它的左孩子“70”和右孩子“100”,发现右孩子"100"比左孩子大,因此将父节点的值和子节点交换。

第四次:我们寻找“60”这个父节点的孩子结点,发现它只有左孩子结点,并且左孩子结点的值比父节点大,因此交换

OK啦,我们向下调整建堆就完成啦!

  代码如下:

//交换
void Swap(int* a, int* b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}

//向下调整算法(小堆)
void AdjustDown(ElemType* arr, int size, int parent) {
	assert(arr);
	int child = parent * 2 + 1;//假设左孩子比右孩子小
	while (child < size) 
	{		//还没有遍历到叶子结点的时候,进入循环
		if (child + 1 < size && arr[child + 1] < arr[child])
		{	//如果右孩子存在,并且右孩子的值小于左孩子
			child = child + 1;
		}
		if (arr[child] < arr[parent])
		{	//如果子节点小于父节点,交换
			Swap(&arr[parent], &arr[child]);
			parent = child;//将子节点赋给父节点
			child = parent * 2 + 1;//寻找下一个子节点
		}
		else
		{	//如果父节点小于子节点,退出循环
			break;
		}
	}
}

//堆的构建
void HeapCreate(Heap* hp, ElemType* a, int n) {
	//断言,防止传入空指针
	assert(hp);
	//断言,防止传入空指针
	assert(a);
	//将堆的动态数组arr开辟一个能存放n个元素的空间
	hp->arr = malloc(n * sizeof(ElemType));
	if (hp->arr == NULL) {	//如果内存不足,开辟失败
		perror("malloc fail!\n");
		exit(1);
	}
	//将a数组里面的所有元素拷贝到堆的动态数组中
	memcpy(hp->arr, a, n * sizeof(ElemType));
	//堆的容量为n
	hp->capacity = n;
	//堆的大小为n
	hp->size = n;

    //向上调整建堆
	//从下标为1的元素开始,一直到下标为size-1的元素结束
	/*for (int i = 1; i < hp->size; i++) {
		AdjustUp(hp->arr, i);
	}*/

	//向下调整建堆,将堆里面的所有元素调整成小堆
	//从最后一个结点的父节点开始,一直到根节点结束
	for (int i = (hp->size-1-1)/2 ; i >= 0; i--) {
		AdjustDown(hp->arr, hp->size, i);
	}
}

//堆的判空
int HeapEmpty(Heap* hp) {
	assert(hp);//断言,防止传入空指针
	return hp->size == 0;//判断堆的大小是否为0
}

//取堆顶的数据
ElemType HeapTop(Heap* hp) {
	assert(hp);//断言,防止传入空指针
	return hp->arr[0];//获取堆顶元素
}

//堆的删除
void HeapPop(Heap* hp) {
	assert(hp);//断言,防止传入空指针
	Swap(&hp->arr[0], &hp->arr[hp->size - 1]);//将堆顶元素和最后一个元素进行交换
	hp->size--;//堆的大小减一

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


//堆的销毁
void HeapDestroy(Heap* hp) {
	assert(hp);//断言,防止传入空指针
	if (hp->arr) 
	{	//如果堆的动态数组存在,那么就释放占用的内存空间
		free(hp->arr);
		hp->arr = NULL;//置空
	}
	hp->capacity = 0;//堆的容量为0
	hp->size = 0;//堆的大小为0
}

// 对数组进行堆排序
void HeapSort(int* a, int n) {
	assert(a);//断言,防止传入空指针
	Heap hp;//创建堆这个结构体
	HeapCreate(&hp, a, n);//堆的创建,将数组的元素全部拷贝到堆中,进行堆排序

	int i = 0;//数组下标从0开始
	while (!HeapEmpty(&hp)) 
	{	//将堆里面的数据依次拷贝到数组中
		a[i++] = HeapTop(&hp);
		HeapPop(&hp);//每拷贝完一次,堆就删除堆顶元素
	}
	HeapDestroy(&hp);//堆的销毁,防止内存泄漏
}

测试一下:

#include"Heap.h"
int main() {
	int arr[] = { 23,45,89,12,33,78,100 };
	HeapSort(arr, sizeof(arr) / sizeof(arr[0]));
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
		printf("%d ", arr[i]);
	}
	return 0;
}

运行结果为:

 23 45 12 33 78 89 100

思路一理解起来很简单,但是它有2个致命的缺陷:①必须要提供堆这种数据结构!②空间复杂度为O(N) , 那还有没有其他方法呢? 


 思路二:①直接对数组进行向下调整建堆,先排成大堆 ②再采用交换思想,逐步排成小堆  

 不过,有一个小问题:我想排成升序,为啥不能直接建小堆呢?

来,咱们举个例子~

我们现在需要获取次小的元素,于是我们把栈顶元素删除

因此,如果要排成升序,只能选择建大堆

还是arr数组,我们再来画一遍图~ 这次是建大堆,别忘记哈!

我们想要排成升序,该怎么做呢?

很简单~ 我们现在已知最大的元素是“9”,是堆顶元素,下标为0最小的元素是“0”,是堆底元素,下标为 n-1 (n代表数组arr的个数),我们已知最大元素和最小元素,那么就让它们交换,将最大的元素放在最后

接下来把最后一个数不看作堆里面,也就是说堆里面原本有n个数,现在把最后一个数“9”不看作堆里面,现在一共有n-1个数。然后我们再开始从根节点向下调整,继续调整成大堆。(因为之前已经创建好大堆了,因此不需要从倒数第一个非叶子结点开始向下调整) 

第一次:从下标为0的元素开始,比较它的左孩子和右孩子,如果其中一个子节点大于父节点,就进行交换。

第二次:继续比较父节点和它的子节点,如果其中一个子节点大于父节点,就进行交换。

第三次:继续比较父节点和它的子节点,如果其中一个子节点大于父节点,就进行交换。

完整过程如下:

OK,现在我们将剩余的元素又排成了大根堆,我们继续将堆顶元素“8”和堆底元素“4”进行交换~

第一次:

第二次:

 

第三次:

OK,此时已经符合大根堆,也就是堆中每一个父节点都大于子节点,左右子树都是大堆。

完整过程如下:

OK,现在我们将剩余的元素又排成了大根堆,我们继续将堆顶元素“7”和堆底元素“0”进行交换~

后面的过程和前面一样,这里就不画图了~

 代码如下:

//交换
void Swap(int* a, int* b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}

//向下调整算法(大堆)
void AdjustDown(ElemType* arr, int size, int parent) {
	assert(arr);
	int child = parent * 2 + 1;//假设左孩子比右孩子大
	while (child < size) 
	{		//还没有遍历到叶子结点的时候,进入循环
		if (child + 1 < size && arr[child + 1] > arr[child])
		{	//如果右孩子存在,并且右孩子的值大于左孩子
			child = child + 1;
		}
		if (arr[child] > arr[parent])
		{	//如果子节点大于父节点,交换
			Swap(&arr[parent], &arr[child]);
			parent = child;//将子节点赋给父节点
			child = parent * 2 + 1;//寻找下一个子节点
		}
		else
		{	//如果父节点大于子节点,退出循环
			break;
		}
	}
}

//堆排序
void HeapSort1(int* a, int n) {
	assert(a);//断言,防止传入空指针
	for (int i = (n - 1 - 1) / 2; i >= 0; i--) 
	{	//从最后一个结点的父节点开始,一直到根节点结束
		AdjustDown(a, n, i);//向下调整算法,调整成大堆
	}

	//这里的n-1有2层含义: 
	//①数组最后一个元素的下标为n-1
	//②数组总共有n个数,交换后将最后一个值不看作堆里面,共n-1个数
	int end = n - 1;
	while (end > 0) {
		Swap(&a[0], &a[end]);//将首尾元素交换
		AdjustDown(a, end, 0);//向下调整算法,从下标为0的元素开始
		end--;//每交换完一次,都要把最后一个数不看作堆里面
	}
}

好啦,堆排序的两种方法讲解完毕,接下来我们继续学习TOP-K问题

二、TOP-K问题

代码如下:

//交换
void Swap(int* a, int* b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}

//向下调整算法
void AdjustDown(ElemType* arr, int size, int parent) {
	assert(arr);
	int child = parent * 2 + 1;//假设左孩子比右孩子小
	while (child < size) 
	{		//还没有遍历到叶子结点的时候,进入循环
		if (child + 1 < size && arr[child + 1] < arr[child])
		{	//如果右孩子存在,并且右孩子的值小于左孩子
			child = child + 1;
		}
		if (arr[child] < arr[parent])
		{	//如果子节点小于父节点,交换
			Swap(&arr[parent], &arr[child]);
			parent = child;//将子节点赋给父节点
			child = parent * 2 + 1;//寻找下一个子节点
		}
		else
		{	//如果父节点小于子节点,退出循环
			break;
		}
	}
}

//文件中找TopK问题
void CreateNDate()
{
	// 造数据
	int n = 10000;
	srand(time(0));//生成随机数
	const char* file = "data.txt";
	//打开文件
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}

	for (size_t i = 0; i < n; ++i)
	{
		int x = rand() % 1000000;
		fprintf(fin, "%d\n", x);
	}
	//关闭文件
	fclose(fin);
}

void PrintTopK() {
	printf("请输入k :>");
	int k = 0;
	scanf("%d", &k);

	//打开文件
	const char* file = "data.txt";
	FILE* fout = fopen(file, "r");
	if (fout == NULL) {
		perror("fopen error");
		return -1;
	}
	
	int* minheap = malloc(sizeof(int) * k);//开辟空间
	if (minheap == NULL) //如果空间不足,则开辟失败
	{
		perror("malloc fail!\n");
		return -1;
	}
	for (int i = 0; i < k; i++) //往堆里面存入数据
	{
		fscanf(fout,"%d", &minheap[i]);
	}

	//建k个数据的小堆(倒数第一个非叶子结点开始向下调整)
	for (int i = (k - 1 - 1) / 2; i >= 0; i--) {
		AdjustDown(minheap, k, i);
	}

	//读取剩余的数据,比堆顶的值大,就替换它进堆
	//将剩余n-k个元素依次与堆顶元素交换,如果比堆顶的值大,就替换它进堆
	int x = 0;
	while (fscanf(fout, "%d", &x) != EOF) {
		
		if (x > minheap[0]) {
			minheap[0] = x;
			AdjustDown(minheap, k, 0);//从第一个结点开始向下调整
		}
	}

	for (int i = 0; i < k; i++) {
		printf("%d ", minheap[i]);
	}

	fclose(fout);
	fout = NULL;
}

int main(){
    PrintTopk();

    return 0;
}

片尾

今天我们学习了堆排序以及堆的TOP-K问题,希望看完这篇文章能对友友们有所帮助!!!

点赞收藏加关注! ! !

谢谢大家! ! !

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

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

相关文章

JSP简介——[JSP]1

希望你开心&#xff0c;希望你健康&#xff0c;希望你幸福&#xff0c;希望你点赞&#xff01; 最后的最后&#xff0c;关注喵&#xff0c;关注喵&#xff0c;关注喵&#xff0c;大大会看到更多有趣的博客哦&#xff01;&#xff01;&#xff01; 喵喵喵&#xff0c;你对我真的…

基于php+mysql+html图书管理系统(含实训报告)

博主介绍&#xff1a; 大家好&#xff0c;本人精通Java、Python、Php、C#、C、C编程语言&#xff0c;同时也熟练掌握微信小程序、Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我有丰富的成品Java、Python、C#毕设项目经验&#xff0c;能够为学生提供各类…

【C++】命名冲突了怎么办?命名空间来解决你的烦恼!!!C++不同于C的命名方式——带你认识C++的命名空间

命名空间 导读一、什么是C?二、C的发展三、命名空间3.1 C语言中的重名冲突3.2 什么是命名空间&#xff1f;3.3 命名空间的定义3.4 命名空间的使用环境3.5 ::——作用域限定符3.6 命名空间的使用方法3.6.1 通过作用域限定符来指定作用域3.6.2 通过关键字using和关键字namespace…

如何用 Redis 实现延迟队列?

延迟队列是一种常见的消息队列模式&#xff0c;用于处理需要延迟执行的任务或消息。Redis 是一种快速、开源的键值对存储数据库&#xff0c;具有高性能、持久性和丰富的数据结构&#xff0c;因此很适合用于实现延迟队列。在这篇文章中&#xff0c;我们将详细讨论如何使用 Redis…

51单片机两个中断及中断嵌套

文章目录 前言一、中断嵌套是什么&#xff1f;二、两个同级别中断2.1 中断运行关系2.2 测试程序 三、两个不同级别中断实现中断嵌套3.1 中断运行关系3.2 测试程序 总结 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 课程需要&#xff1a; 提示&#x…

Mysql基础(四)DML之insert语句

一 insert 语句 强调&#xff1a; 本文介绍的内容很基础,仅做记录用,参考价值较少 ① 总述 目的&#xff1a; 增加rows记录1、完整格式insert [into] 表名[字段名1[, 字段名2]] value[s](值1, 值2);备注&#xff1a;指定部分字段添加,没有被指定的字段要么会自动增长,要…

微信小程序demo-----制作文章专栏

前言&#xff1a;不管我们要做什么种类的小程序都涉及到宣传或者扩展其他业务&#xff0c;我们就可以制作一个文章专栏的页面&#xff0c;实现点击一个专栏跳转到相应的页面&#xff0c;页面可以有科普类的知识或者其他&#xff0c;然后页面下方可以自由发挥&#xff0c;添加联…

ensp 配置s5700 ssh登陆

#核心配置 sys undo info-center enable sysname sw1 vlan 99 stelnet server enable telnet server enable int g 0/0/1 port lin acc port de vlan 99 q user-interface vty 0 4 protocol inbound ssh authentication-mode aaa q aaa local-user admin0 password cipher adm…

结构分析的有限元法及matlab实现(徐荣桥)|【PDF教材+配套案例Matlab源码】

专栏导读 作者简介&#xff1a;工学博士&#xff0c;高级工程师&#xff0c;专注于工业软件算法研究本文已收录于专栏&#xff1a;《有限元编程从入门到精通》本专栏旨在提供 1.以案例的形式讲解各类有限元问题的程序实现&#xff0c;并提供所有案例完整源码&#xff1b;2.单元…

为何数据库推荐将IPv4地址存储为32位整数而非字符串?

目录 一、IPv4地址在数据库中的存储方式&#xff1f; 二、IPv4地址的存储方式比较 &#xff08;一&#xff09;字符串存储 vs 整数存储 &#xff08;二&#xff09;IPv4地址"192.168.1.8"说明 三、数据库推荐32位整数存储方式原理 四、存储方式对系统性能的影响…

服务器IP选择

可以去https://ip.ping0.cc/查看IP的具体情况 1.IP位置--如果是国内用&#xff0c;国外服务器的话建议选择日本&#xff0c;香港这些比较好&#xff0c;因为它们离这里近&#xff0c;一般延时低&#xff08;在没有绕一圈的情况下&#xff09;。 不过GPT的话屏蔽了香港IP 2. 企…

C++ | Leetcode C++题解之第64题最小路径和

题目&#xff1a; 题解&#xff1a; class Solution { public:int minPathSum(vector<vector<int>>& grid) {if (grid.size() 0 || grid[0].size() 0) {return 0;}int rows grid.size(), columns grid[0].size();auto dp vector < vector <int>…

WebAuthn 无密码身份认证

文章目录 WebAuthn简介工作原理组成部分架构实现注册认证应用场景案例演示 WebAuthn简介 WebAuthn&#xff0c;全称 Web Authentication&#xff0c;是由 FIDO 联盟&#xff08;Fast IDentity Online Alliance&#xff09;和 W3C&#xff08;World Wide Web Consortium&#x…

【跟马少平老师学AI】-【神经网络是怎么实现的】(八)循环神经网络

一句话归纳&#xff1a; 1&#xff09;词向量与句子向量的循环神经网络&#xff1a; x(i)为词向量。h(i)为含前i个词信息的向量。h(t)为句向量。 2&#xff09;循环神经网络的局部。 每个子网络都是标准的全连接神经网络。 3&#xff09;对句向量增加全连接层和激活函数。 每个…

【Web】CTFSHOW 新手杯 题解

目录 easy_eval 剪刀石头布 baby_pickle repairman easy_eval 用script标签来绕过 剪刀石头布 需要赢100轮&#x1f914; 右键查看源码拿到提示 一眼session反序列化 打PHP_SESSION_UPLOAD_PROGRESS 脚本 import requestsp1 a|O:4:"Game":1:{s:3:"log…

如何将 redis 快速部署为 docker 容器?

部署 Redis 作为 Docker 容器是一种快速、灵活且可重复使用的方式&#xff0c;特别适合开发、测试和部署环境。本文将详细介绍如何将 Redis 部署为 Docker 容器&#xff0c;包括 Docker 安装、Redis 容器配置、数据持久化、网络设置等方面。 步骤 1&#xff1a;安装 Docker 首…

NI CRIO 9045 LABVIEW2020

1.labview工程如果要访问CRIO&#xff0c;需要设置以下&#xff0c;否则在项目中连接失败。 2.项目中如果要传文件&#xff0c;需要安装WebDEV 3.使用WebDAV将文件传输到实时(RT)目标 https://knowledge.ni.com/KnowledgeArticleDetails?idkA03q000000YGytCAG&lzh-CN

34.Docker基本操作

镜像相关的命令 镜像名称分为两部分组成&#xff1a;[repository]:[tag],tag就是镜像的版本。如果tag没有指定默认就是latest,表示最新版本的镜像。 查看docker命令的帮助信息 docker --help 具体某条命令的帮助信息 docker images --help 案例一&#xff1a;从DockerHub中…

【Vue3】openlayers加载瓦片地图并手动标记坐标点

目录 一、创建Vue3项目 二、openlayers加载瓦片地图&#xff08;引js文件版&#xff09; 2.1 将以下的文件复制到public下 2.2 index.html引入ol脚本 2.3 删除项目自带的HelloWorld.vue&#xff0c;创建Map.vue 2.4 编码Map.vue 2.5 修改App.vue 2.6 启动项目测试 三、…

数据分析--客户价值分析RFM(K-means聚类/轮廓系数)

原数据 import os import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from sklearn import metrics ### 数据抽取&#xff0c;读⼊数据 df pd.read_csv("customers1997.csv") #相对路径读取数据 print(df.info()) pr…