数据结构-如果将堆结构应用到TOP-K问题上会怎样?


前言

本篇文章进行如何用堆结构解决TOP-K问题的讲解


一、TOP-K问题是什么?

TOP-k问题:即求一些数据中的前k个最大的数字或最小的数字,并且一般这些数据规模都很大

生活中也有许多常见的topks问题,比如在打游戏时的全服前几名玩家,世界五百强,专业前十名等等,这些都属于TOP-K问题

而当数据规模很大的时候,想要找出前k个最大值或最小值,普通排序是难以满足要求的,因为时间复杂度太高,比如冒泡排序,这就让我们联想到了一种排序方式——堆排序

因为我们想要找到前k个最大值或最小值,必然离不开排序,并且当数据规模极大的时候,我们不可能全部等待它加载出来,这时候我们常常借助堆这个结构来解决

举个例子,当我们需要找出100亿个整数的前k个最大值的时候,这100亿个整数我们不可能全部申请到内存中,我们计算一下,100亿个整数,相当于400亿个字节

1G = 1024MB = 1024 * 1024KB = 1024 * 1024 * 1024Byte,1个G,大约是10亿个字节,那么400亿个字节,就相当于40G的内存,注意,这是临时申请的内存,是运行内存,我们想想,一个普通的笔记本电脑运行内存才有多少,这一下子就占据了40G,除去电脑本身的操作系统等也需要使用占据的运行内存,我们的电脑内存能承受得住吗,而且我们申请完空间之后,还要借助cpu进行排序等操作,换句话说,cpu内心可能在哀嚎:皇上,臣妾做不到啊!

而这时候,就轮到了我们的同学出场了

二、如何用堆解决TOP-K问题

1.怎么建堆,建大堆还是小堆?

当我们想要取N个数据的前k个最大元素时,我们要建小堆

当取前k个最小元素时,我们要建大堆

可能有些同学会想到上一篇刚刚讲到的堆排序问题,堆排序,升序建大堆,降序建小堆,为什么到TOP-K问题就变了呢?

因为我们要解决的TOP-K问题,通常数据规模较大,并且它只要求我们找到前k个最大数据,并不要求对数据整体进行排序,因此我们并不一定要按照堆排序的方式进行,只需要进行类似于“排序”的操作,找出前k个最大值即可,对整体数据进行排序反而会让问题变得更加复杂,有些冗余

如果我们按照堆排序的想法进行,在N个数据中找到前k个最大元素,那么就要建N个大堆,然后再pop(删除)K次最后得到前k个最大元素,在这个过程中,整个数据的顺序也排成了升序,这未免有些冗杂,因此我们要采取另一种方式

我们以N个数据中取前K个最大元素为例,此时我们要建小堆,并且只用数据中的前k个元素进行建堆,当我们对数据中前k个元素建小堆之后,堆顶的元素则是前k个元素中最小的那一个元素剩下的N - K个元素逐个与堆顶元素进行比较如果剩下的元素比堆顶元素大的话,就将堆顶元素进行替换替换为更大的那个元素替换后将前k个数据建成的小堆进行向下调整,调整之后继续将堆顶元素和后面的元素进行比较,重复进行

这种方式会使得最开始的前k个元素中的最小值“沉底”,而替换上来的是一个大一点的元素,向下调整之后,堆顶的数据又是前k个元素中最小的那一个,再与剩下的元素进行比较,这样我们就不必担心大的数据会遗漏,因为我们建的是小堆,小堆的堆顶元素必然是最小的,大一些的元素都是堆顶元素的子结点,不必担心小的数据会进入导致出错

有的同学可能会问,既然这样,建大堆不行吗?

答案是不行!

如果我们以这种方式进行并且建大堆的话,那么堆顶元素存储的则是前k中最大的元素,我们无法确定堆顶元素的子结点与剩下的N - K个结点之间的大小,可能会造成大的元素无法进入,比如堆顶是100,子结点中有的数据是5、10、15等等,剩下的N-K个元素中如果有78,那么判定78 < 100,不替换,78就无法进入前k个元素建成的小堆,但是实际上78是大于5、10、15这些数的,这就会导致出错,甚至可能会导致前k中只有最大的元素和最小的k - 1个元素,因此当我们取前k个最大元素时,建大堆并不可取。

建大堆适合于取前k个最小元素时,这时候小于堆顶元素的就进行替换,替换之后向下调整,再重复,最后前k个元素自然就是最小的前k个元素

并且当我们如此操作时,我们只需要维护建堆的那k个空间即可,因为我们进行的是替换操作,不符合条件的堆顶元素直接就被替换,被丢弃掉,不需要再维护了,而且我们这种方式,不必担心被丢弃的元素会比后面的元素大,成为前k个最大元素之一,因为它既然是堆顶元素,必然是前k个元素建成的小堆中的最小的一个元素,被替换后,替换上的数据必然会比它要大,而堆中剩下的k -1 个元素也必然大于它,因此替换后的前k个元素都是比被替换的元素要大的,后面如果继续替换,说明它一定大于之前被替换的元素,就像b>a,a被替换,后面再比较,c > b,那么b被替换,此时c一定是大于a的,之后再进行重复比较、替换、调整,不会出现错误

2.代码实现

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//topk问题,找出最大的k个数,那么就要建小堆
void Swap(int* a, int* b)
{
	int tmp;
	tmp = *a;
	*a = *b;
	*b = tmp;
}
void AdjustDown(int*a ,int n,int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child + 1] < a[child])
		{
			child += 1;
		}
		if (a[parent] > a[child])
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
void Topk(int* a,int n,int k)
{
	assert(k > 0 && k <= n);
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, k, i);
	}
	for (int i = k; i < n; i++)
	{
		if (a[i] > a[0])
		{
			Swap(&a[0], &a[i]);
			AdjustDown(a, k, 0);
		}
	}
	for (int i = 0; i < k; i++)
	{
		printf("%d ", a[i]);
	}
}
int main()
{
	int n;
	int k;
	printf("请输入你要输入数字的个数:");
	scanf_s("%d", &n);
	printf("请输入你要查找的前k个最大数的个数k");
	scanf_s("%d", &k);
	int* a = (int*)malloc(sizeof(int) * n);
	if (a == NULL)
	{
		perror("malloc fail");
		return 0;
	}
	printf("请输入n个数字");
	for (int i = 0; i < n; i++)
	{
		scanf_s("%d", &a[i]);
	}
	Topk(a, n, k);
	free(a);
	a = NULL;
	return 0;
}

与堆排序类似,我们依旧是先进行向下调整模拟建堆(为什么采用向下调整模拟建堆详见上篇文章),之后进行比较、交换、调整,重复操作,当然,当数据规模过大的时候,我们一般不采取图中的主函数中依次输入后读取数据的方式,多采用造文件,从文件中读取数据的方式进行测试或者其他操作,具体详见文件操作篇

总结

本篇讲解了如何使用堆结构去解决TOP-K问题,关于TOP-K还有许多解法,比如随机选择等,后续会继续发布,敬请关注!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值