目录
前面我已经学习过使用"堆排序"对数组排降序了,接下来再来看一个堆排序的应用场景。
一、什么是TOP-K问题
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大,即:N个数找最大的前K个。
二、解决思路
一般的正常思路:
把这N个数建成大堆,Pop弹出K次堆顶,即可找出最大的前K个数,但是有些场景这种思路解决不了,例如N非常大时,假设N是10亿,10亿个数建堆所需的空间我们来计算一下:
一个整型变量需要四个字节空间,10亿个整型数据需要40亿个字节,1G可以放10亿字节,所以我们需要 4G空间为10亿个整型数据建堆。
4G感觉不多的话,如果一百亿数据呢?一千亿呢?
内存无法承载这么大的空间时,数据会存储到磁盘上,磁盘的效率比内存慢很多,所以这种方法如果数据过多,就无法再内存上快速找到TOP-K。
最优的解决思路:
- 前K个数建小堆。
- 后面N-K个数,依次比较,如果比堆顶的数据大,就替换它进堆,进堆后向下调整。
- 最后这个小堆的值就是最大的前K个。
三、文件流中实践TOP-K方法
我们来在文件中实践一下这个方法:
创建包含足够多整数的文件:
cpp
void CreateNData()
{
int n = 100000;
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() % 10000000;
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
- 定义一个变量n,表示要生成的随机整数的数量为十万个。
- 使用srand函数设置随机数种子,time(0)返回当前时间的秒数,确保每次运行程序生成的随机数序列都不同。
- 定义一个文件名file,表示要生成的文件名。
- 使用fopen函数打开文件,以写入模式打开,如果没有文件,则创建一个,如果文件打开失败,输出一个错误信息并返回。
- 使用for循环生成n个随机整数,并使用fprintf函数将它们写入文件中。
- 使用fclose函数关闭文件。
找出最大的K个数
如果建小堆的方法---向下调整 忘记了,不妨看看这篇文章 向下调整的部分
cpp
void PrintTopK(int k)
{
const char* file = "data.txt";
FILE* fout = fopen(file, "r");
if (fout == NULL)
{
perror("fopen error");
return;
}
int* kminheap = (int*)malloc(sizeof(int) * k);
if (kminheap == NULL)
{
perror("malloc error");
return;
}
for (int i = 0; i < k; i++)
{
fscanf(fout, "%d", &kminheap[i]);
}
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(kminheap, k, i);
}
int val = 0;
while (!feof(fout))
{
fscanf(fout, "%d", &val);
if (val > kminheap[0])
{
kminheap[0] = val;
AdjustDown(kminheap, k, 0);
}
}
for (int i = 0; i < k; i++)
{
printf("%d ", kminheap[i]);
}
printf("\n");
}
代码较长进行分段讲解:
cppconst char* file = "data.txt"; FILE* fout = fopen(file, "r"); if (fout == NULL) { perror("fopen error"); return; } int* kminheap = (int*)malloc(sizeof(int) * k); if (kminheap == NULL) { perror("malloc error"); return; } for (int i = 0; i < k; i++) { fscanf(fout, "%d", &kminheap[i]); }
- 定义一个文件名file,表示要读取的文件名。
- 使用fopen函数打开文件,以读取模式打开,如果文件打开失败,输出一个错误信息并返回。
- 使用malloc函数动态分配一个大小为k的整数数组 kminheap,用于存储最大的 k 个数,如果内存分配失败,输出一个错误信息并返回。
- 使用for循环从文件中读取前k个整数,并将它们存储到kminheap数组中。
cppfor (int i = (k - 1 - 1) / 2; i >= 0; i--) { AdjustDown(kminheap, k, i); } int val = 0; while (!feof(fout)) { fscanf(fout, "%d", &val); if (val > kminheap[0]) { kminheap[0] = val; AdjustDown(kminheap, k, 0); } } for (int i = 0; i < k; i++) { printf("%d ", kminheap[i]); } printf("\n");
- 使用向下调整函数将kminheap数组构建成一个小堆(从小到大)。
- feof(fout)是一个C标准库函数,用于判断文件指针fout所指向的文件是否已经到达文件末尾。该函数的返回值为非零值表示已经到达文件末尾,返回值为0表示文件还没有到达末尾。
- 使用while循环从文件中读取剩余的N-K整数,如果某个整数比堆顶元素大,就将它替换堆顶元素,并使用AdjustDown函数将堆重新调整为小堆,
- 因为我们需要前K个最大的数,而建的小堆也是K个元素,所以这种操作可以得到由最大前K个元素构成的小堆。
- 使用for循环输出堆中的所有元素。
- 使用fclose函数关闭文件。
我们来测试一下,首先我在主函数调用CreateNData函数对文件data.txt写入文件。
cppint main() { CreateNData(); return 0; }
然后在文件中对五个数添加几位使其成为最大的五个数。
这是屏蔽掉 CreateNData 函数,防止重新生成数字覆盖我修改的数字,调用PrintTopK函数输出5个最大的数。
cppint main() { //CreateNData(); PrintTopK(5); return 0; }
输出结果为:
完整版代码:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustDown(HPDataType* a, int size, int parent)
{
int child = parent * 2 + 1;
while (child < size) {
if (child + 1 < size && a[child + 1] < a[child]) {
child++;
}
if (a[child] < a[parent]) {
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 - 1;
}
else {
break;
}
}
}
void CreateNData()
{
int n = 100000;
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() % 10000000;
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
void PrintTopK(int k)
{
const char* file = "data.txt";
FILE* fout = fopen(file, "r");
if (fout == NULL)
{
perror("fopen error");
return;
}
int* kminheap = (int*)malloc(sizeof(int) * k);
if (kminheap == NULL)
{
perror("malloc error");
return;
}
for (int i = 0; i < k; i++)
{
fscanf(fout, "%d", &kminheap[i]);
}
// 建小堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(kminheap, k, i);
}
int val = 0;
while (!feof(fout))
{
fscanf(fout, "%d", &val);
if (val > kminheap[0])
{
kminheap[0] = val;
AdjustDown(kminheap, k, 0);
}
}
for (int i = 0; i < k; i++)
{
printf("%d ", kminheap[i]);
}
printf("\n");
fclose(fout);
}
int main()
{
CreateNData();
PrintTopK(5);
return 0;
}