当前位置: 首页 > news >正文

堆的应用(堆排序TopK问题)

文章目录

  • 1. 堆排序
    • 1)建堆的思考(时间复杂度计算)
    • 2)利用堆删除思想来进行排序
    • 3)实现堆排序(升序-大堆)
    • 4)运行效果
    • 5)分析建堆的时间复杂度
      • 一)向上调整算法【最终近似O(NlogN)】
      • 二)向下调整算法【最终近似O(N)】
      • 三)选数字排序【O(NlogN)】
  • 2. TOP-K问题(小堆)
    • 1)基本思路
    • 2)实现代码,要建立小堆
    • 3)运行效果

1. 堆排序

堆排序可以帮助选数,本质是选择排序。

1)建堆的思考(时间复杂度计算)

利用向下调整算法和向上调整算法完成建堆,第一个数作为一个堆,后面的数依次进行插入,本质是模拟堆插入的过程

升序:建大堆;降序:建小堆

为什么是这样子的呢?拿升序建大堆为例进行讲解:

假设给出一个数组,要使用堆排序实现升序,如果这时建小堆,那么数组第一个数是最小的,那后面的数就需要重新建堆,而一次建堆的时间复杂度本身就是 O(NlogN) ,还需要重复 n 次这样的操作,即使看上去高大上,但实际效率十分低。

小堆升序时间复杂度高

而采用大堆进行升序时,能够确定最大的数,这样就可以对数组首尾交换,从而实现固定最大的数字。这样原本的堆再利用堆删除的思想,就可以保证堆的关系不会乱,时间复杂度此时变成了:建堆 O(NlogN) ,选数 O(N-1)logN ,总的时间复杂度就是相加,就变成了 O(NlogN) ,这个算法的量级也就下降了(时间复杂度是判定算法的量级,即使相同时间复杂度的算法,他们的量级也会有区别)

大堆升序

那还有更高效的建堆方法吗?答案就是直接利用向下调整算法!

从倒数第一个非叶子,即最后一个节点的父亲开始调整,这样可以减少一般的时间复杂度,而且调整的方式也会改变,从而使其更快

向下

建堆的两个时间复杂度将在最后进行分析。

2)利用堆删除思想来进行排序

建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序,下面将根据上述的思路进行升序

堆排序

3)实现堆排序(升序-大堆)

依托上面堆实现的代码,但要修改向上调整算法和向下调整算法的判定条件为大堆。

#include "Heap.h"

void HeapSort(int* a, int n)
{
	// 建大堆
	 把a[0]当作一个大堆,其他数据插入
	 O(NlogN)
	//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(NlogN)
	int end = n - 1;
	while (end > 0)
	{
		// 先排序大的数据,大的往后面先排好序
		Swap(&a[0], &a[end]);
		// 修改判定条件为大堆,然后调整堆
		AdjustDown(a, end, 0);
		end--;
	}
}

int main()
{
	int a[] = { 2,7,0,21,56,786,1,3 };
	HeapSort(a, sizeof(a) / sizeof(int));
	for (int i = 0; i < sizeof(a) / sizeof(int); i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");

	return 0;
}

4)运行效果

堆排序

5)分析建堆的时间复杂度

首先要知道满二叉树有以下结论:

2^h-1  ->  h=log(N+1)

满二叉树结论

对于建堆的两种办法:

一)向上调整算法【最终近似O(NlogN)】

向上时间复杂度

二)向下调整算法【最终近似O(N)】

向下调整算法

三)选数字排序【O(NlogN)】

选数字

2. TOP-K问题(小堆)

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。 比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中),因此最佳的方式就是用堆来解决

1)基本思路

思路1: N个数插入大堆里面, Pop k次,时间复杂度是 O(N*logN+K*logN) -> O(N*logN) ,当N远大于k,且N很大时,空间占用会很高,内存不够用,因此方案不太行

思路2: 首先读取前k个值,建立k个数的小堆,再一次读取后面的值,跟堆顶比较;如果比堆顶大,替换堆顶进展,再向下调整。此时的时间复杂度就变成了 O(N*logK)

下面将模拟实现思路2的场景:10亿个随机数字找出最大的前10个。首先向文件写入随机数字,用 fprint 写入文件,再用 fscanf 读取建立小堆,之后开始遍历。为了检验是否有错误,手动进入文件修改5个数字,使其大于10000000000(一百亿),如果5个数都大于10000000000,就证明思路实现正确。

2)实现代码,要建立小堆

#include "Heap.h"

void CreateNDate()
{
	// 造数据
	int n = 10000000000;
	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)
	{
        // +i可以防止过多数据重复
		int x = (rand() + i) % 10000000000;
		fprintf(fin, "%d\n", x);
	}

	fclose(fin);
}

void PrintTopK(const char* file, int k)
{
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen fail\n");
		exit(-1);
	}

	// 要先对向上/向下调整算法判定条件修改
	// 开辟小堆空间
	int* minHeap = (int*)malloc(sizeof(int) * k);
	if (minHeap == NULL)
	{
		perror("malloc fail\n");
		exit(-1);
	}

	// 读取前k个数建一个k个数小堆
	for (int i = 0; i < k; ++i)
	{
		fscanf(fout, "%d", &minHeap[i]);
		AdjustUp(minHeap, i);
	}

    // 找大的数字进堆
	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]);
	}
	printf("\n");

	// 使用完就释放空间
	free(minHeap);
	minHeap = NULL;
	// 要记得关闭文件
	fclose(fout);
}

int main()
{
    // 一百亿个数据要跑很久,可以将数据量减少
	CreateNDate();
	PrintTopK("data.txt", 6);

	return 0;
}

3)运行效果

topk
以上就是堆的全部内容啦!!

相关文章:

  • mysql存储引擎、索引、事务---java
  • 【工具】C#游戏防沉迷小工具
  • docker桌面版启动redis,解决无法连接
  • 大数据技术之Spark优化
  • SaaS 系统业务逻辑处理方式探讨
  • 【PyCharm2024】一些好用的小功能
  • 大模型学习笔记------Llama 3模型架构之旋转编码(RoPE)
  • Redis 源码分析-内部数据结构 quicklist
  • peach模糊测试工具中,stateModel模块中的type的作用
  • DeepLabv3+改进10:在主干网络中添加LSKBlock|动态调整其大型空间感受野,助力小目标识别
  • Ai文章改写出来的文章,怎么过Ai检测?控制指令,测试的一点心得,彻底疯了!
  • 14.使用各种读写包操作 Excel 文件:辅助模块
  • 蓝桥杯Python赛道备赛——Day3:排序算法(二)(归并排序、堆排序、桶排序)
  • 【解锁机器学习:探寻数学基石】
  • Springboot项目修改端口
  • kali之msf
  • MySQL 衍生表(Derived Tables)
  • 使用Composer实现自动加载类
  • 静态内部类和非静态内部类的区别
  • CSS:使用内边距时,解决宽随之改变问题
  • 习近平向2025年上海合作组织减贫和可持续发展论坛致贺信
  • 上海蝉联全国中小企业发展环境评估综合排名第一
  • 女巫的继承者们
  • 1至4月国家铁路发送货物12.99亿吨,同比增长3.6%
  • AI创业者聊大模型应用趋势:可用性和用户需求是关键
  • LPR名副其实吗?如果有所偏离又该如何调整?