堆排序以及TOP-K问题

片头

嗨!小伙伴们,大家好!今天我们来深入理解堆这种数据结构,分析一下堆排序以及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问题

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

比如:几十个,几百个,几千个甚至是上亿个数字中找到最大的前K个数字。

对于TOP-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(甚至无法将数据放入数组)。最佳的方式就是用来解决,基本思路如下:

1. 用数据集合中前k个来建堆

        *要找最大的前k个元素,建小堆

        *要找最小的前k个元素,建大堆

2. 用剩余的N - K个元素依次与栈顶元素来比较,如果比堆顶的值大,就替换它进堆,堆整体向下调整

将剩余N-K个元素依次与堆顶元素比较完毕后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素,本次topk示例中计算的是最大的前k个。

我们可以用文件操作的方法来写一个造数据的函数:

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 (int i = 0; i < n; i++) {
		int x = (rand() + i) % 1000000;//产生随机数
		fprintf(fin, "%d\n", x);//将产生的随机数填充到文件中
	}
	//关闭文件
	fclose(fin);
}

 这里将造出来的数据写入到data.txt文件中,运行完此函数后,当前目录下会多一个data.txt文件

打开此文本文件:

通过这个函数,我们已经成功造出了10000个数据了。 

接下来就是topk代码的实现:

#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<assert.h>

//交换
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() {        //这里的k是选出最大的前k个数
	printf("请输入k :>");
	int k = 0;
	scanf("%d", &k);

	//打开需要查找前K个数据的文件----data.txt
	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++) //往堆里面填充k个数据
	{
		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) {        //EOF是文件的结束标志,它的值是-1
		
		if (x > minheap[0]) {
			minheap[0] = x;
			AdjustDown(minheap, k, 0);//从第一个结点开始向下调整
		}
	}
    
    //打印前K个最大的数字
	for (int i = 0; i < k; i++) {
		printf("%d ", minheap[i]);
	}

	fclose(fout);
	fout = NULL;
}

int main(){
   //CreateNDate();
    PrintTopk();

    return 0;
}

片尾

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

点赞收藏加关注! ! !

谢谢大家! ! !

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

变量内存和存储单位

基本数据类型及其占位符 存储单位 内存中的数据存储单元是由一个一个的二进制组成的&#xff0c;每个二进制只能存储0 和1 科学家为了更加方便存储更多的数据&#xff0c;把内存中8个二进制分为一组&#xff0c;叫做一个字节&#xff0c;Byte字节是最小的存储单位。(重点⭐⭐⭐…

数据结构与算法-双向链表

1.简介 在使用带头节点的单向链表查找时查找的方向只能是一个方向&#xff0c;而双向链表可以向前或向后查找。例如单向链表只能查到一个节点的下一个节点&#xff0c;但是双向链表不仅可以查到下一个节点&#xff0c;还可以查到上一个节点。 在删除节点时&#xff0c;单向链…

C语言 | Leetcode C语言题解之第66题加一

题目&#xff1a; 题解&#xff1a; /*** Note: The returned array must be malloced, assume caller calls free().*/ int* plusOne(int* digits, int digitsSize, int* returnSize){for(int i digitsSize - 1; i > 0; --i){digits[i] digits[i] 1;//最后元素1判断是不…

怎么用CAPL与Python交互

怎么用CAPL与其他应用程序交互 怎么用CAPL与Python交互 怎么用CAPL与Python交互 怎么用CAPL与其他应用程序交互前言1、CAPL怎么调Python&#xff1f;1.1CAPL调Python的命令1.2CAPL调用Python实例 2、怎么把python运行的结果返回给CAPL2.1通过环境变量 3、CAPL调Python的输入参…

linux进入单用户模式指引

文章目录 引言I 通过GRUB进入单用户模式1.1 倒计时界面的操作1.2 GRUB1.3 内核参数编辑界面1.4 更多内核参数编辑界面II 预备知识:Linux用户模式引言 应用场景: root密码重置: 用passwd命令修改root修复登录相关的配置:/etc/pam.d/login 和 /etc/pam.d/sshd 案例:Centos6进…

Qt QImageReader类介绍

1.简介 QImageReader 是用于读取图像文件的类。它提供了读取不同图像格式的功能&#xff0c;包括但不限于 PNG、JPEG、BMP 等。QImageReader 可以用于文件&#xff0c;也可以用于任何 QIODevice&#xff0c;如 QByteArray &#xff0c;这使得它非常灵活。 QImageReader 是一个…

奥尔良

目录 一&#xff0c;核心规则 1&#xff0c;游戏回合 2&#xff0c;公共主版面 3&#xff0c;公共副版面 4&#xff0c;个人版面 二&#xff0c;规则细节 1&#xff0c;七种随从 2&#xff0c;渡船、马车、公会 3&#xff0c;得分 4&#xff0c;其他规则 奥尔良是一个…

【大模型学习】私有大模型部署(基础知识)

私有大模型 优点 保护内部隐私 缺点 成本昂贵 难以共享 难以更新 大模型底座 基础知识点 知识库 知识库是什么&#xff1f; 知识库的作用是什么&#xff1f; 微调 增强大模型的推理能力 AI Agent 代理&#xff0c;与内部大模型进行交互 开源 and 闭源 是否可以查…

[蓝桥杯2024]-PWN:fd解析(命令符转义,标准输出重定向,利用system(‘$0‘)获取shell权限)

查看保护 查看ida 这里有一次栈溢出&#xff0c;并且题目给了我们system函数。 这里的知识点没有那么复杂 方法一&#xff08;命令转义&#xff09;&#xff1a; 完整exp&#xff1a; from pwn import* pprocess(./pwn) pop_rdi0x400933 info0x601090 system0x400778payloa…

Redis教程——管道

在上篇文章我们学习了Redis教程——事务&#xff0c;这篇文章我们学习Redis教程——管道。 客户端向服务端发送命令分四步&#xff08;发送、排队、执行和返回结果&#xff09;&#xff0c;并监听Socket返回&#xff0c;通常以阻塞模式等待服务端响应&#xff0c;如下图所示&a…

探索和构建 LLaMA 3 架构:深入探讨组件、编码和推理技术(三)KV缓存

探索和构建 LLaMA 3 架构&#xff1a;深入探讨组件、编码和推理技术&#xff08;三&#xff09; KV缓存 在推理的每一步中&#xff0c;只对模型输出的最后一个标记感兴趣&#xff0c;因为已经有了之前的标记。然而&#xff0c;模型需要访问所有先前的标记来决定输出哪个标记&…

【算法】【单调栈】【leetcode】1019. 链表中的下一个更大节点

刷这题之前先看&#xff1a; 【算法】【OD算法】【单调栈】找朋友-CSDN博客 【算法】【单调栈】【leetcode】1475. 商品折扣后的最终价格-CSDN博客 【算法】【单调栈】【leetcode】901. 股票价格跨度-CSDN博客 【算法】【单调栈】每日温度-CSDN博客 题目地址&#xff1…

Linux MQTT智能家居(Linux下运行MQTT)

文章目录 前言一、下载源码编译1.编译出64位的库文件2.编译出ARM平台下的库文件 二、将lib库文件和include文件加入自己的工程1.ubuntu下测试2.ARM平台测试 总结 前言 本篇文章将带大家在Linux下运行MQTT库&#xff0c;我们首先会将MQTT库下载下来&#xff0c;然后进行编译&am…

3.4 无关、基和维度

这一节是关于子空间的真实大小。对于 m n m\times n mn 的矩阵&#xff0c;它有 n n n 个列&#xff0c;但是它真正的维数不一定为 n n n&#xff0c;维数可以由无关列的个数来得到。列空间的实际维度就是秩 r r r。 无关的概念是用于向量空间中的任意向量 v 1 , . . . ,…

匿名函数和箭头函数的使用场景

箭头函数和匿名函数其实是相同的使用场景 匿名函数通常在以下情况下使用&#xff1a; 作为回调函数&#xff1a; 当你需要将函数作为参数传递给另一个函数时&#xff0c;可以使用匿名函数。 array.map(item > item * 2);事件处理程序&#xff1a; 在事件处理程序中&#xf…

如何配置Jupyter Lab以允许远程访问和设置密码保护

如何配置Jupyter Lab以允许远程访问和设置密码保护 当陪你的人要下车时&#xff0c;即使不舍&#xff0c;也该心存感激&#xff0c;然后挥手道别。——宫崎骏《千与千寻》 在数据科学和机器学习工作流中&#xff0c;Jupyter Lab是一个不可或缺的工具&#xff0c;但是默认情况下…

【C++】深入剖析C++11中右值引用和左值引用

目录 一、左值引用 && 右值引用 二、左值引用于右值引用的比较 三、 右值引用使用场景和意义 1、函数返回值 ①移动赋值 ②移动构造 2、STL容器插入接口 ​3、完美转发 一、左值引用 && 右值引用 传统的C语法中就有引用的语法&#xff0c;而C11中新增了…

[基础] Unity Shader:顶点着色器(vert)函数

顶点着色器&#xff08;Vertex Shader&#xff09;是图形渲染的第一个阶段&#xff0c;它的输入来自于CPU。顶点着色器的处理单位是顶点&#xff0c;CPU输入进来的每个顶点都会调用一次顶点着色器函数&#xff0c;也就是我们在Shader代码里所定义的vert函数。本篇我们将会通过顶…

全球知名哲学家思想家颜廷利:唯物须防危屋,唯心不及为醒…

‘唯物’须防‘危屋’ ‘唯心’不及‘为醒’…&#xff08;升命学说&#xff09; 21世纪东方哲学家思想家、科学家、当代中国教育界知名教授、专业周易起名改名字、易经姓名学专家、目前比较有影响力的人物、现代国学大师泰斗杰出代表颜廷利教授在《升命学说》‘净化论’里面如…

Python中如何调用其他文件的类或函数

Python中如何调用其他文件的类或函数 在Python编程中&#xff0c;随着项目的扩大&#xff0c;代码通常会被分解为多个模块&#xff0c;以提高可读性和可维护性。模块通常是包含Python定义和声明的文件。了解如何从一个文件调用另一个文件中的类或函数是非常重要的&#xff0c;…