一、问题介绍
Top-K问题是一个堆结构的经典问题,要求我们对给出的一组数据选出其中最大或者最小的k个元素。在拿到这个问题时,第一反应肯定都是排序,然后取所需要的k个元素即可。但是Top-K问题的数据量一般都很大,对其进行排序只为找到那短短k个元素,这个性价比怎么想怎么不甘心。除此之外,我们所学的排序算法多为内排序,当数据量大到一定程度时甚至有可能内存中脸数据都放不下。那么有没有其他方法可以更好地解决这个问题呢?
这时,我们就可以搬出刚刚学过的堆来解决这个问题了。建立一个k个元素大小的堆来对数据进行入堆出堆判断,最后大浪淘沙得到的就是我们所需要的k个元素。
二、问题详解
1.构造模拟数据
因为我们需要大量的数据,所以我们可以使用随机数产生数据,同时由于数据量较大,我们采用文件的方式将其存储起来。我们在构造数据时,刻意将一些位置加上特定的值,使得他们的值一定位列最大者其中,这将便于我们对结果进行正误判断。
void CreateNDate()
{
// 造数据
int n = 10000;
srand(time(0));
const char* file = "data.txt";
FILE* fin = fopen(file, "w");
if (fin == NULL)
{
perror("fopen fail");
return;
}
for (size_t i = 0; i < n; ++i)
{
int x = rand() % 1000000;
if (i == 298)
{
x += 1000000;
}
if (i == 28)
{
x += 2000000;
}
if (i == 1298)
{
x += 3000000;
}
if (i == 2298)
{
x += 4000000;
}
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
2.堆的选择
在处理最大k个和最小k个这两个不同的问题时,我们对大堆和小堆的选择肯定是有所不同的。
在草率做出抉择之前,我们需要先明确我们要用堆来干什么。我们建堆的目的是筛选最大的k个值,那能否成为最大的k个值取决于谁呢?显然,能否是最大的k个值要看它和现在k个值中最小者的关系,如果它更大就可以挤掉这个最小者从而成为其中一员。所以当针对最大的k个值时,我们需要堆可以很明显的为我们展示出topk的门槛,即堆中的最小值,那么我们就毫无疑问地选择小堆了,因为小堆堆顶正是最小值。
同理,要选取最小的k个值,门槛在于堆中的最大值,所以需要使用大堆。
3.思路与代码
创建一个k个元素的数组作为堆,然后读取文件中前k个元素组成堆,这时采用向上调整建堆即可。然后对后续元素一一与堆顶比较,如果大于堆顶,则取代堆顶,再进行向下调整,保证堆的特性。否则就继续比较下一个元素。如此最后所得到的堆中的数据即为最大的k个数据。
void PrintTopK(int k)
{
FILE* fou = fopen("data.txt", "r");
if (fou == NULL)
{
perror("fopen fail");
return;
}
//建堆
int* a = (int*)malloc(sizeof(int) * k);
if (a == NULL)
{
perror("malloc fail");
exit(-1);
}
for (int i = 0; i < k; i++)
{
fscanf(fou, "%d", &a[i]);
AdjustUpMin(a, i);
}
int num = 0;
while (fscanf(fou, "%d", &num) != EOF)
{
if (num > a[0])
{
a[0] = num;
AdjustDownMin(a, k, 0);
}
}
//打印
for (int i = 0; i < k; i++)
{
printf("%d ", a[i]);
}
printf("\n");
fclose(fou);
}
int main()
{
CreateNDate();
PrintTopK(10);
return 0;
}