【初探数据结构】堆的应用实例(堆排序与TopK问题)

💬 欢迎讨论:在阅读过程中有任何疑问,欢迎在评论区留言,我们一起交流学习!
👍 点赞、收藏与分享:如果你觉得这篇文章对你有帮助,记得点赞、收藏,并分享给更多对数据结构感兴趣的朋友

声明:本文建议在掌握“堆的实现”后学习。
🚀传送门:【初探数据结构】二叉树的顺序结构——堆的实现详解(上下调整算法的时间复杂度分析)


一、引言

堆是一种高效处理优先级问题的数据结构,尤其在动态数据流和排序场景中表现优异。本文将通过堆排序TopK问题两个经典案例,深入解析堆的实际应用,并提供清晰的代码实现与原理分析。


二、堆排序:将无序数组变为有序序列
1. 堆排序的核心思想
  • 利用最大堆的性质:堆顶元素始终为最大值。
  • 两步走策略
    1. 建堆
    • 升序 :建大堆
    • 降序:建小堆
    1. 利用堆删除思想来进行排序
    • 逐次提取堆顶:将堆顶元素(最大值)与数组末尾交换,缩小堆范围,重新调整堆。
2. 详细步骤图解(以升序排序为例)

在这里插入图片描述

初始数组[4, 10, 3, 5, 1]

  1. 构建最大堆
    • 从最后一个非叶子节点(索引5//2 -1 =1)开始调整。
    • 调整后得到最大堆:[10, 5, 3, 4, 1]

💡 建堆的两种方式

  1. 向上调整建堆法:
    时间复杂度O(nlogn),效率低,不推荐
  2. 向下调整建堆法
    用向下调整建堆 时间复杂度O(n),推荐
//建大堆
	//1.用向上调整建堆  时间复杂度O(nlogn),效率低,不推荐
	for (i = 0; i < n; i++) {
		AdjustUp(a, i);
	}
	//2.用向下调整建堆  时间复杂度O(n),推荐
	for (i = (n - 1 - 1)/2; i >= 0; i--) {
		AdjustDown(a, n, i);
	}

这里调整算法不再赘述。如有疑问,请学习“堆的实现”
或者与我交流哦😀

  1. 交换与调整
    • 交换堆顶10与末尾1 → 数组变为[1,5,3,4,10],有效堆范围减1。
    • 重新调整剩余元素[1,5,3,4]为最大堆 → [5,4,3,1]
    • 重复上述步骤,最终得到有序数组[1,3,4,5,10]
3. 代码实现
//升序排序,用大根堆;
//降序排序,用小根堆。
void HeapSort(HPDataType* a,int n)
{
	//排升序
	int i = 0;

	//建大堆
	//1.用向上调整建堆  时间复杂度O(nlogn),效率低,不推荐
	/*for (i = 0; i < n; i++) {
		AdjustUp(a, i);
	}*/
	//2.用向下调整建堆  时间复杂度O(n),推荐
	for (i = (n - 1 - 1)/2; i >= 0; i--) {
		AdjustDown(a, n, i);
	}

	//排序
	int end = n - 1;
	while (end > 0) {
		swap(a, &a[end]);
		AdjustDown(a, end, 0);
		end--;
	}
	for (i = 0; i < n; i++) {
		printf("%d ", a[i]);
	}
}
4. 时间复杂度与特点
  • 时间复杂度:O(n log n)(构建堆O(n) + 每次调整O(log n))。
  • 原地排序:无需额外空间,适合内存敏感场景。
  • 不稳定性:相同元素可能交换顺序。

三、TopK问题:海量数据中的高效筛选
1. 问题场景
  • 需求:从一亿个数中快速找到前100大的数。
  • 挑战:直接排序时间复杂度O(n log n),内存可能无法容纳全部数据。
2. 堆的解决方案
  • 最小堆策略(找最大的K个元素):

    1. 初始化:用前K个元素构建一个最小堆(堆顶为当前最小值)。
    2. 遍历剩余元素:若当前元素 > 堆顶,则替换堆顶并调整堆。
    3. 最终结果:堆中保留的K个元素即为TopK。
  • 为什么用最小堆?

    • 堆顶是K个元素中的最小值,遇到更大的值时替换,确保堆中始终保留更大的候选。
3.如何创建这么多数?
  1. 随机数生成:利用rand生成随机数
  2. 写入文件
void CreateNumber()
{
	int n = 10000;
	//时间种子
	srand((unsigned int)time(0));
	const char* file = "data.text";
	FILE* fp = fopen(file, "w");
	if (fp == NULL) {
		perror("fopen fail");
		return;
	}
	//写入文件
	for (size_t i = 0; i < n; ++i)
	{
		int x = rand() % 10000;
		fprintf(fp, "%d\n", x);
	}
	fclose(fp);
	fp = NULL;
}
3. 代码实现
void PrintTopK(const char* file, int k)
{
	int* topk = (int*)malloc(sizeof(int) * k);
	if (topk == NULL) {
		perror("fopen fail");
		return;
	}
	FILE* fp = fopen(file, "r");
	if (fp == NULL) {
		perror("fopen fail");
		return;
	}
	//建前k个数的小根堆
	for (int i = 0; i < k; i++) {
		fscanf(fp,"%d",&topk[i]);
	}
	for (int i = (k - 2) / 2; i >= 0; i--) {
		AdjustDown(topk, k, i);
	}
	//遍历后面的数,若比堆top大,就覆盖并向下调整
	int val = 0;
	int ret = fscanf(fp, "%d", &val);
	while (ret != EOF) {
		if (val > topk[0]) {
			topk[0] = val;
			AdjustDown(topk, k, 0);
		}
		ret = fscanf(fp, "%d", &val);
	}
	for (int i = 0; i < k; i++) {
		printf("%d ", topk[i]);
	}
	free(topk);
	fclose(fp);
	fp = NULL;
}
4. 时间复杂度与优化
  • 时间复杂度:O(n log K),远优于O(n log n)。
  • 处理数据流:逐个读取数据,内存仅需维护大小为K的堆。

四、对比与总结
应用场景核心思路时间复杂度空间复杂度优势
堆排序构建堆 + 交换堆顶O(n log n)O(1)原地排序,适合内存敏感
TopK问题维护大小为K的最小堆O(n log K)O(K)高效处理海量数据或数据流

五、实际应用拓展
  1. 优先队列:操作系统任务调度、医院急诊分诊。
  2. 实时排行榜:游戏积分实时更新前100名。
  3. 合并K个有序链表:利用堆高效选择最小节点。

六、常见问题解答

Q1:为什么堆排序不如快速排序常用?

  • 堆排序的常数因子较大,且不稳定,实际运行速度可能慢于快速排序。

Q2:TopK问题能否用最大堆解决?

  • 可以,但需维护大小为n-K的堆,空间复杂度更高,适合K接近n的场景。

七、结语

堆结构凭借其高效的插入删除和极致的空间利用率,在排序与筛选问题中占据独特地位。掌握堆排序与TopK的解法,能显著提升处理大规模数据的能力。理解原理后,可尝试手写堆实现或结合具体业务场景优化代码,进一步巩固知识。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值