目录
1.3.以求N个数据中的前K个最大元素为例,阐述建堆来解决TOP-K的原理
2.类型1:数据量N较小,可以全部加载到内存中。求数据量N的前K个最大的元素或者最小的元素。
3.类型2:数据量N很大无法全部加载到内存中只能存进硬盘文件。求数据量N的前K个最大的元素或者最小的元素。
TOP-K问题
1.对TOP-K问题的理解
1.1.TOP-K问题定义
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
1.2.TOP-K问题的解决思路
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。
最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
1.3.以求N个数据中的前K个最大元素为例,阐述建堆来解决TOP-K的原理
当我们想要找到N个数据中前K个最大元素时,我们采用以下步骤:
-
建立小堆:首先,我们从数据集中取出前K个元素,并构建一个小堆。在这个堆中,堆顶元素是这K个元素中最小的。
-
遍历剩余元素:接着,我们遍历数据集中的剩余N-K个元素。对于每个元素:
- 如果它比堆顶元素大,那么它有可能属于最大的K个元素之一。因此,我们用这个新元素替换掉堆顶元素,并进行向下调整操作(也称为堆化)以重新维护最小堆的性质。
- 向下调整操作确保了新加入的较大元素能够“下沉”到堆的适当位置,而较小的元素则被“推”到堆顶或堆的上层。
-
维护小堆:通过这个过程,我们确保了堆中始终保存着当前遇到的最大K个元素。当遍历完所有元素后,堆中的K个元素即为所求的最大K个元素。
1.4.TOP-K问题的类型
(1)类型1:数据量N没那么大,K很小,这N个数可以放在内存中。求数据量N的前K个最大的元素或者最小的元素。
(2)类型2:数据量N很大,K很小,这N个数内存存不下,只能存放在硬盘文件中。求数据量N的前K个最大的元素或者最小的元素。
2.类型1:数据量N较小,可以全部加载到内存中。求数据量N的前K个最大的元素或者最小的元素。
2.1.求前K个最大的元素,建小堆。
(1)解决思路
- 在原数组中使用数组的前K个元素建立一个小堆。
- 遍历数组剩余的N-K个元素,并与堆顶元素比较。
- 如果当前元素大于堆顶元素,则替换堆顶元素,替换后并对此时的堆顶元素进行向下调整,通过向下调整保持数组前K个元素依然是个小堆。
- 当数组剩余的N-K个元素都遍历完了,则最终小堆中的元素即为所求的前K个最大元素(注意:这前K个最大元素不一定是有序的)。
(2)代码
//交换两个元素的函数
void swap(int* p1, int* p2)
{
//判断指针p1与p2是否是空指针
assert(p1 && p2);
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//建小堆的向下调整
void AdjustDown2(int* a, int n, int parent)
{
assert(a);
//找当前双亲的左孩子
int child = parent * 2 + 1;
while (child < n)
{
//让child始终指向当前双亲的左右孩子中最小的那个孩子
if (child + 1 < n && a[child + 1] < a[child])
child++;
if (a[child] < a[parent])//若当前双亲比最小的孩子还有大,则当前双亲就要向下进行调整
{
swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
//TOP-K问题
//类型1:数据量N较小,可以全部加载到内存中。建小堆求数据量N的前K个最大的元素。
void PrintArrMaxTopK(int* a, int n, int k)
{
// 1.用数组a中前k个元素建小堆来解决求N个数中最大的前K个元素。(注意:由于利用向下调整函
数建堆的时间复杂度是O(N),所以我们使用向下调整函数来建堆)
int parent = 0;
for (parent = (k - 1 - 1) / 2; parent >= 0; parent--)
AdjustDown2(a, k, parent);
// 2.将数组a中剩余n-k个元素依次与堆顶元素进行比较,若剩余n-k个元素有比小堆堆顶元素大的
则这两个元素就交换。交换完后对此时的堆顶元素(即数组首元素a[0])进行向下调整以此来要保持
数组a中前k个元素依然是个小堆。
int cmp = n - k;//cmp表示比较次数。
int i = k;//下标i是用来遍历数组a中剩余n-k个元素的。
while (cmp--)
{
//(1)堆顶元素a[0]与数组a剩余的n-k个元素进行比较。
//若堆顶元素a[0]比数组a剩余元素a[i]要小,则要交换且交换后数组前k个元素依然保持是个
小堆,然后继续往下比较。
if (a[i] > a[0])
{
//交换
swap(&a[0], &a[i]);
//交换完后利用向下调整函数保持数组a中前k个元素依然是个小堆。
AdjustDown2(a, k, 0);//对堆顶元素a[0]进行向下调整
//继续往下比较
i++;
}
else//若堆顶元素a[0]比数组a剩余元素a[i]要大,则要继续往下比较。
i++;
}
//此时数组a中的前k个元素组成的小堆中的所有元素都是数组a中前k个最大的数,所以此时打印数
组a中前k个最大元素。
for (i = 0; i < k; i++)
{
printf("%d ", a[i]);
}
//换行
printf("\n");
}
2.2.前K个最小的元素,建大堆。
(1)解决思路
- 在原数组中使用数组的前K个元素建立一个大堆。
- 遍历数组剩余的N-K个元素,并与堆顶元素比较。
- 如果当前元素小于堆顶元素,则替换堆顶元素,替换后并对此时的堆顶元素进行向下调整,通过向下调整保持数组前K个元素依然是个大堆。
- 当数组剩余的N-K个元素都遍历完了,则最终大堆中的元素即为所求的前K个最小元素(注意:这前K个最小元素不一定是有序的)。
(2)代码
//交换两个元素的函数
void swap(int* p1, int* p2)
{
//判断指针p1与p2是否是空指针
assert(p1 && p2);
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//建大堆的向下调整
void AdjustDown1(int* a, int n, int parent)
{
assert(a);
//找当前双亲的左孩子
int child = parent * 2 + 1;
while (child < n)
{
//让child始终指向当前双亲的左右孩子中最大一个孩子
if (child + 1 < n && a[child + 1] > a[child])
child++;
if (a[child] > a[parent])//若当前双亲比最大一个孩子还有小,则当前双亲就要进行向下调整。
{
swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
//TOP-K问题
//类型1:数据量N较小,可以全部加载到内存中。求数据量N的前K个最小的元素。
void PrintArrMinTopK(int* a, int n, int k)
{
// 1.用数组a中前k个元素建大堆来解决求N个数中最小的前K个元素。(注意:由于利用向下调整函
数建堆的时间复杂度是O(N),所以我们使用向下调整函数来建)
int parent = 0;
for (parent = (k - 1 - 1) / 2; parent >= 0; parent--)
AdjustDown1(a, k, parent);
// 2.将数组a中剩余n-k个元素依次与堆顶元素进行比较,若剩余n-k个元素有比大堆堆顶元素小的
则这两个元素就交换。交换完后要保持数组a中前k个元素依然是个大堆。交换完后对此时的堆顶元
素(即数组首元素a[0])进行向下调整以此来要保持数组a中前k个元素依然是个大堆。
int cmp = n - k;//cmp表示比较次数。
int i = k;//下标i是用来遍历数组a中剩余n-k个元素的。
while (cmp--)
{
//(1)堆顶元素a[0]与数组a剩余的n-k个元素进行比较。
//若堆顶元素a[0]比数组a剩余元素a[i]要大,则要交换且交换后数组前k个元素依然保持是个
大堆,然后继续往下比较。
if (a[i] < a[0])
{
//交换
swap(&a[0], &a[i]);
//交换完后利用向下调整函数保持数组a中前k个元素依然是个大堆。
AdjustDown1(a, k, 0);
//继续往下比较
i++;
}
else//若堆顶元素a[0]比数组a剩余元素a[i]要小,则要继续往下比较。
i++;
}
//此时数组a中的前k个元素组成的大堆中的所有元素都是数组a中前k个最小的数,所以此时打印数组a中前k个最小元素。
for (i = 0; i < k; i++)
{
printf("%d ", a[i]);
}
//换行
printf("\n");
}
2.3.类型1的时间复杂度和空间复杂度的计算过程
(1)时间复杂度O(N * logK)
- 利用向下调整函数把原数组初始化为堆(建堆)的时间复杂度是O(K),因为只需要对前K个元素进行堆化。
- 遍历剩余的N-K个元素与堆顶元素进行比较并判断是否要交换且最坏情况下要进行N-K次交换使得要对堆进行N-K次调整。每次调整堆的时间复杂度是O(logK),因为堆的高度是logK,进而使得最坏情况下调整堆的时间复杂度是 O((N-K) * logK)。
- 因此,总的时间复杂度是O(K) + O((N-K) * logK) = O(N * logK)。
(2)空间复杂度O(1)
- 由于所有操作都是在原数组上进行的,除了几个用于临时交换的变量外,不需要额外的存储空间。
- 因此,空间复杂度是O(1)。
注意: 论是求解前K个最大元素还是最小元素,时间复杂度和空间复杂度都是相同的。
3.类型2:数据量N很大无法全部加载到内存中只能存进硬盘文件。求数据量N的前K个最大的元素或者最小的元素。
3.1.求前K个最大的元素,建小堆。
图形解析:
(1)解决思路
- 额外创建一个大小为K的数组minHeap。
- 从文件中读取前K个元素存入数组minHeap中,并把数组minHeap构建成小堆。
- 继续从文件中读取剩余的N-K个元素,并与堆顶元素比较。
- 如果读取的文件数据val大于堆顶元素,则把读取到的文件数据val放置在堆顶位置,放置完后并对此时位于堆顶位置的文件数据val进行向下调整,通过向下调整保持数组minHeap中的K个元素依然是个小堆。
- 当文件所有元素都读取完毕即读取到文件末尾时,则最终小堆(即数组minHeap)中的所有元素即为所求的前K个最大元素(注意:在数组minHeap中的前K个最大元素不一定是有序的)。
(2)代码
//交换两个元素的函数
void swap(int* p1, int* p2)
{
//判断指针p1与p2是否是空指针
assert(p1 && p2);
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//建小堆的向下调整
void AdjustDown2(int* a, int n, int parent)
{
assert(a);
//找当前双亲的左孩子
int child = parent * 2 + 1;
while (child < n)
{
//让child始终指向当前双亲的左右孩子中最小的那个孩子
if (child + 1 < n && a[child + 1] < a[child])
child++;
if (a[child] < a[parent])//若当前双亲比最小的孩子还有大,则当前双亲就要向下进行调整
{
swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
//类型2:数据量N很大无法全部加载到内存中只能存进硬盘文件。建小堆求数据量N的前K个最大的元素。
void PrintFileMaxTopK(const char* file, int k)//注意:指针file指向文件名(字符串)
{
//1.创建k个元素的数组minHeap
int* minHeap = (int*)malloc(sizeof(int) * k);
if (minHeap == NULL)
{
perror("malloc fail");
exit(-1);
}
//1.1.以只读的方式打开文件
FILE* pf = fopen(file, "r");
//1.2从文件pf中依次读取k个数据到数组minHeap中
for (int i = 0; i < k; ++i)
{
fscanf(pf, "%d", &minHeap[i]);
}
//2.利用向下调整函数把数组minHeap中的k个元素建成小堆
for (int i = (k - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown2(minHeap, k, i);
}
//val用来临时存放文件中读取的数据
int val = 0;
//3.将文件pf中剩余n-k个元素依次与数组minHeap的堆顶元素进行比较,若剩余n-k个元素有比小
堆堆顶元素大的数据val则这个读取到的文件数据val放在堆顶位置。放置后要保持数组minHeap中
的k个元素依然是个小堆。
//注意:此时文件指针指向文件第K+1个数据,所以此时从文件的第k+1个数据开始依次遍历文件中
的数据并把遍历到的数据用临时变量val存放起来,当遍历到的文件数据val比堆顶元素要大时则要
把val放到堆顶位置中,然后对val进行向下调整操作使得文件中大的数不断存放在小堆的下面。
//利用while (fscanf(fout, "%d", &val) != EOF)不断一个一个读取文件pf中剩余n-k个元素。
while (fscanf(pf, "%d", &val) != EOF)
{
//3.1若遍历到的文件数据val比堆顶元素要大,则要把文件数据val放在堆顶位置中,并对此
时位于堆顶位置的文件数据val进行向下调整操作使得数组minHeap的k个元素依然保持是小堆。
if (val > minHeap[0])
{
minHeap[0] = val;//把文件数据val放在堆顶位置。
AdjustDown2(minHeap, k, 0);//把位于堆顶位置的文件数据val进行向下调整操作,进而
使得数组minHeap依然保持是个堆。
}
}
//此时数组minHeap中的k个元素就是文件存放的N个数据中的最大前K个元素。
//4.打印文件N个元素中前k个最大的数(即把数组minHeap中的所有元素打印出来)
for (int i = 0; i < k; ++i)
{
printf("%d ", minHeap[i]);
}
printf("\n");
//关闭文件
fclose(pf);
}
3.2.前K个最小的元素,建大堆。
图形解析:
(1)解决思路
- 额外创建一个大小为K的数组maxHeap。
- 从文件中读取前K个元素存入数组a中,并把数组maxHeap构建成大堆。
- 继续从文件中读取剩余的N-K个元素,并与堆顶元素比较。
- 如果读取到的文件数据val小于堆顶元素,则把读取到的文件数据val放置到堆顶位置,放置完后并对此时位于堆顶位置的文件数据val进行向下调整,通过向下调整保持数组maxHeap中的K个元素依然是个大堆。
- 当文件所有元素都读取完毕即读取到文件末尾时,则最终大堆(即数组maxHeap)中的所有元素即为所求的前K个最小元素(注意:在数组maxHeap中的前K个最小元素不一定是有序的)。
(2)代码
//交换两个元素的函数
void swap(int* p1, int* p2)
{
//判断指针p1与p2是否是空指针
assert(p1 && p2);
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//建大堆的向下调整
void AdjustDown1(int* a, int n, int parent)
{
assert(a);
//找当前双亲的左孩子
int child = parent * 2 + 1;
while (child < n)
{
//让child始终指向当前双亲的左右孩子中最大一个孩子
if (child + 1 < n && a[child + 1] > a[child])
child++;
if (a[child] > a[parent])//若当前双亲比最大一个孩子还有小,则当前双亲就要进行向下调整。
{
swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
//类型2:数据量N很大无法全部加载到内存中只能存进硬盘文件。建大堆求数据量N的前K个最小的元素。
void PrintFileMinTopK(const char* file, int k)//注意:指针file指向文件名字符串
{
//1.创建k个元素的数组maxHeap
int* maxHeap = (int*)malloc(sizeof(int) * k);
if (maxHeap == NULL)
{
perror("malloc fail");
exit(-1);
}
//1.1.以只读的方式打开文件
FILE* pf = fopen(file, "r");
//1.2.从文件pf中依次读取k个数据到数组maxHeap中
for (int i = 0; i < k; ++i)
{
fscanf(pf, "%d", &maxHeap[i]);
}
//2.利用向下调整函数把数组maxHeap中的k个元素建成大堆
for (int i = (k - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown1(maxHeap, k, i);
}
//val用来临时存放文件中读取的数据
int val = 0;
//3.将文件pf中剩余n-k个元素依次与数组maxHeap的堆顶元素进行比较,若剩余n-k个元素有比大
堆堆顶元素小的数据val则把这个读取到的文件数据val放置在堆顶位置,放置后保持数组maxHeap
中前k个元素依然是个大堆。
//注意:此时文件指针指向文件第k+1个数据,所以此时从文件的第k+1个数据开始依次遍历文件中
的数据并把遍历到的数据用临时变量val存放起来,当遍历到的文件数据val比堆顶元素要小时则要
把val放到堆顶中然后对val进行向下调整操作使得文件中小的数不断存放在大堆的下面。
//利用while (fscanf(fout, "%d", &val) != EOF)不断一个一个读取文件pf中剩余n-k个元素。
while (fscanf(pf, "%d", &val) != EOF)
{
//3.1若遍历到的文件数据val比堆顶元素要小,则要把文件数据val放在堆顶位置中,并对位
于堆顶位置的文件数据val进行向下调整操作使得数组maxHeap中的k个元素依然保持是个大堆。
if (val < maxHeap[0])
{
maxHeap[0] = val;//把文件数据val放在堆顶位置。
AdjustDown1(maxHeap, k, 0);//把位于堆顶位置的文件数据val进行向下调整操作,进而
使得数组maxHeap依然保持是个大堆。
}
}
//此时数组maxHeap中的k个元素就是文件存放N个元素中的最小K个元素。
//4.打印文件N个元素中前k个最小的元素(即打印数组maxHeap中的所有元素)
for (int i = 0; i < k; ++i)
{
printf("%d ", maxHeap[i]);
}
printf("\n");
//关闭文件
fclose(pf);
}
3.3.类型2的时间复杂度和空间复杂度的计算过程
求解前K个最大元素的时间复杂度和空间复杂度:
(1)时间复杂度O(N * logK)
- 读取前K个元素并利用向下调整函数构建小堆的时间复杂度是O(K),因为需要对这K个元素进行堆化。
- 接下来,需要遍历剩余的N-K个元素。
- 对于每个元素:
- 读取元素的时间复杂度是O(1)。
- 如果元素大于堆顶元素,则替换堆顶元素,并进行一次向下调整,以维护小堆。向下调整的时间复杂度是O(logK)。
- 因此,总的时间复杂度是O(K) + O((N-K) * logK) = O(N * logK)。
(2)空间复杂度O(K)
- 需要一个大小为K的数组来存储堆中的元素。
- 因此,空间复杂度是O(K)。
注意: 求解前K个最小元素的时间复杂度和空间复杂度的计算与求解前K个最大元素相同,因为算法的步骤和复杂度是一样的,只是构建的是大顶堆而不是小顶堆。
4.TOP-K问题的整个工程
4.1.TOP-K.h
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <assert.h>
#include <time.h>
//交换两个元素的函数
void swap(int* p1, int* p2);
//建堆的类型及所有建堆方式
//1.建大堆
//(1)建大堆的向上调整
void AdjustUp1(int* a, int child);
//(2)建大堆的向下调整
void AdjustDown1(int* a, int n, int parent);
//2.建小堆
//(1)建小堆的向上调整
void AdjustUp2(int* a, int child);
//(2)建小堆的向下调整
void AdjustDown2(int* a, int n, int parent);
//Tok问题
//类型1、N很小,内存中可以存到下N个数据。
//建小堆求数组N个数中最大的前K个元素
void PrintArrMaxTopK(int* a, int n, int k);
//建大堆求数组N个数中最小的前K个元素。
void PrintArrMinTopK(int* a, int n, int k);
//类型2、N很大,内存中存不下N个数据只能存在文件中。
//建小堆求文件N个数中最大的前K个元素。
void PrintFileMaxTopK(const char* file,int k);
//建大堆求文件N个数中最小的前K个元素。
void PrintFileMinTopK(const char* file, int k);
4.2.TOP-K.c
#include "TOP-K.h"
//交换两个元素的函数
void swap(int* p1, int* p2)
{
//判断指针p1与p2是否是空指针
assert(p1 && p2);
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//建堆的类型及所有建堆方式
//1.建大堆
//(1)建大堆的向上调整
void AdjustUp1(int* a, int child)
{
assert(a);
//找当前孩子的双亲
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] > a[parent])//若当前孩子比自己的双亲还有大,则当前孩子就要进行向上调整。
{
swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}
//(2)建大堆的向下调整
void AdjustDown1(int* a, int n, int parent)
{
assert(a);
//找当前双亲的左孩子
int child = parent * 2 + 1;
while (child < n)
{
//让child始终指向当前双亲的左右孩子中最大一个孩子
if (child + 1 < n && a[child + 1] > a[child])
child++;
if (a[child] > a[parent])//若当前双亲比最大一个孩子还有小,则当前双亲就要进行向下调整。
{
swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
//2.建小堆
//(1)建小堆的向上调整
void AdjustUp2(int* a, int child)
{
assert(a);
//找当前孩子的双亲
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] < a[parent])//若当前孩子比自己的双亲还有小,则当前孩子就要进行向上调整
{
swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}
//(2)建小堆的向下调整
void AdjustDown2(int* a, int n, int parent)
{
assert(a);
//找当前双亲的左孩子
int child = parent * 2 + 1;
while (child < n)
{
//让child始终指向当前双亲的左右孩子中最小的那个孩子
if (child + 1 < n && a[child + 1] < a[child])
child++;
if (a[child] < a[parent])//若当前双亲比最小的孩子还有大,则当前双亲就要向下进行调整
{
swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
//TOP-K问题
//类型1:数据量N较小,可以全部加载到内存中。建小堆求数据量N的前K个最大的元素。
void PrintArrMaxTopK(int* a, int n, int k)
{
// 1.用数组a中前k个元素建小堆来解决求N个数中最大的前K个元素。(注意:由于利用向下调整函
数建堆的时间复杂度是O(N),所以我们使用向下调整函数来建堆)
int parent = 0;
for (parent = (k - 1 - 1) / 2; parent >= 0; parent--)
AdjustDown2(a, k, parent);
// 2.将数组a中剩余n-k个元素依次与堆顶元素进行比较,若剩余n-k个元素有比小堆堆顶元素大的
则这两个元素就交换。交换完后对此时的堆顶元素(即数组首元素a[0])进行向下调整以此来要保持
数组a中前k个元素依然是个小堆。
int cmp = n - k;//cmp表示比较次数。
int i = k;//下标i是用来遍历数组a中剩余n-k个元素的。
while (cmp--)
{
//(1)堆顶元素a[0]与数组a剩余的n-k个元素进行比较。
//若堆顶元素a[0]比数组a剩余元素a[i]要小,则要交换且交换后数组前k个元素依然保持是个
小堆,然后继续往下比较。
if (a[i] > a[0])
{
//交换
swap(&a[0], &a[i]);
//交换完后利用向下调整函数保持数组a中前k个元素依然是个小堆。
AdjustDown2(a, k, 0);//对堆顶元素a[0]进行向下调整
//继续往下比较
i++;
}
else//若堆顶元素a[0]比数组a剩余元素a[i]要大,则要继续往下比较。
i++;
}
//此时数组a中的前k个元素组成的小堆中的所有元素都是数组a中前k个最大的数,所以此时打印数
组a中前k个最大元素。
for (i = 0; i < k; i++)
{
printf("%d ", a[i]);
}
//换行
printf("\n");
}
//类型1:数据量N较小,可以全部加载到内存中。求数据量N的前K个最小的元素。
void PrintArrMinTopK(int* a, int n, int k)
{
// 1.用数组a中前k个元素建大堆来解决求N个数中最小的前K个元素。(注意:由于利用向下调整函
数建堆的时间复杂度是O(N),所以我们使用向下调整函数来建)
int parent = 0;
for (parent = (k - 1 - 1) / 2; parent >= 0; parent--)
AdjustDown1(a, k, parent);
// 2.将数组a中剩余n-k个元素依次与堆顶元素进行比较,若剩余n-k个元素有比大堆堆顶元素小的
则这两个元素就交换。交换完后要保持数组a中前k个元素依然是个大堆。交换完后对此时的堆顶元
素(即数组首元素a[0])进行向下调整以此来要保持数组a中前k个元素依然是个大堆。
int cmp = n - k;//cmp表示比较次数。
int i = k;//下标i是用来遍历数组a中剩余n-k个元素的。
while (cmp--)
{
//(1)堆顶元素a[0]与数组a剩余的n-k个元素进行比较。
//若堆顶元素a[0]比数组a剩余元素a[i]要大,则要交换且交换后数组前k个元素依然保持是个
大堆,然后继续往下比较。
if (a[i] < a[0])
{
//交换
swap(&a[0], &a[i]);
//交换完后利用向下调整函数保持数组a中前k个元素依然是个大堆。
AdjustDown1(a, k, 0);
//继续往下比较
i++;
}
else//若堆顶元素a[0]比数组a剩余元素a[i]要小,则要继续往下比较。
i++;
}
//此时数组a中的前k个元素组成的大堆中的所有元素都是数组a中前k个最小的数,所以此时打印数组a中前k个最小元素。
for (i = 0; i < k; i++)
{
printf("%d ", a[i]);
}
//换行
printf("\n");
}
//类型2:数据量N很大无法全部加载到内存中只能存进硬盘文件。建小堆求数据量N的前K个最大的元素。
void PrintFileMaxTopK(const char* file, int k)//注意:指针file指向文件名(字符串)
{
//1.创建k个元素的数组minHeap
int* minHeap = (int*)malloc(sizeof(int) * k);
if (minHeap == NULL)
{
perror("malloc fail");
exit(-1);
}
//1.1.以只读的方式打开文件
FILE* pf = fopen(file, "r");
//1.2从文件pf中依次读取k个数据到数组minHeap中
for (int i = 0; i < k; ++i)
{
fscanf(pf, "%d", &minHeap[i]);
}
//2.利用向下调整函数把数组minHeap中的k个元素建成小堆
for (int i = (k - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown2(minHeap, k, i);
}
//val用来临时存放文件中读取的数据
int val = 0;
//3.将文件pf中剩余n-k个元素依次与数组minHeap的堆顶元素进行比较,若剩余n-k个元素有比小
堆堆顶元素大的数据val则这个读取到的文件数据val放在堆顶位置。放置后要保持数组minHeap中
的k个元素依然是个小堆。
//注意:此时文件指针指向文件第K+1个数据,所以此时从文件的第k+1个数据开始依次遍历文件中
的数据并把遍历到的数据用临时变量val存放起来,当遍历到的文件数据val比堆顶元素要大时则要
把val放到堆顶位置中,然后对val进行向下调整操作使得文件中大的数不断存放在小堆的下面。
//利用while (fscanf(fout, "%d", &val) != EOF)不断一个一个读取文件pf中剩余n-k个元素。
while (fscanf(pf, "%d", &val) != EOF)
{
//3.1若遍历到的文件数据val比堆顶元素要大,则要把文件数据val放在堆顶位置中,并对此
时位于堆顶位置的文件数据val进行向下调整操作使得数组minHeap的k个元素依然保持是小堆。
if (val > minHeap[0])
{
minHeap[0] = val;//把文件数据val放在堆顶位置。
AdjustDown2(minHeap, k, 0);//把位于堆顶位置的文件数据val进行向下调整操作,进而
使得数组minHeap依然保持是个堆。
}
}
//此时数组minHeap中的k个元素就是文件存放的N个数据中的最大前K个元素。
//4.打印文件N个元素中前k个最大的数(即把数组minHeap中的所有元素打印出来)
for (int i = 0; i < k; ++i)
{
printf("%d ", minHeap[i]);
}
printf("\n");
//关闭文件
fclose(pf);
}
//类型2:数据量N很大无法全部加载到内存中只能存进硬盘文件。建大堆求数据量N的前K个最小的元素。
void PrintFileMinTopK(const char* file, int k)//注意:指针file指向文件名字符串
{
//1.创建k个元素的数组maxHeap
int* maxHeap = (int*)malloc(sizeof(int) * k);
if (maxHeap == NULL)
{
perror("malloc fail");
exit(-1);
}
//1.1.以只读的方式打开文件
FILE* pf = fopen(file, "r");
//1.2.从文件pf中依次读取k个数据到数组maxHeap中
for (int i = 0; i < k; ++i)
{
fscanf(pf, "%d", &maxHeap[i]);
}
//2.利用向下调整函数把数组maxHeap中的k个元素建成大堆
for (int i = (k - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown1(maxHeap, k, i);
}
//val用来临时存放文件中读取的数据
int val = 0;
//3.将文件pf中剩余n-k个元素依次与数组maxHeap的堆顶元素进行比较,若剩余n-k个元素有比大
堆堆顶元素小的数据val则把这个读取到的文件数据val放置在堆顶位置,放置后保持数组maxHeap
中前k个元素依然是个大堆。
//注意:此时文件指针指向文件第k+1个数据,所以此时从文件的第k+1个数据开始依次遍历文件中
的数据并把遍历到的数据用临时变量val存放起来,当遍历到的文件数据val比堆顶元素要小时则要
把val放到堆顶中然后对val进行向下调整操作使得文件中小的数不断存放在大堆的下面。
//利用while (fscanf(fout, "%d", &val) != EOF)不断一个一个读取文件pf中剩余n-k个元素。
while (fscanf(pf, "%d", &val) != EOF)
{
//3.1若遍历到的文件数据val比堆顶元素要小,则要把文件数据val放在堆顶位置中,并对位
于堆顶位置的文件数据val进行向下调整操作使得数组maxHeap中的k个元素依然保持是个大堆。
if (val < maxHeap[0])
{
maxHeap[0] = val;//把文件数据val放在堆顶位置。
AdjustDown1(maxHeap, k, 0);//把位于堆顶位置的文件数据val进行向下调整操作,进而
使得数组maxHeap依然保持是个大堆。
}
}
//此时数组maxHeap中的k个元素就是文件存放N个元素中的最小K个元素。
//4.打印文件N个元素中前k个最小的元素(即打印数组maxHeap中的所有元素)
for (int i = 0; i < k; ++i)
{
printf("%d ", maxHeap[i]);
}
printf("\n");
//关闭文件
fclose(pf);
}
4.3.test.c测试函数
#include "TOP-K.h"
void Print(int* a, int n)
{
int i = 0;
for (i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
//换行
printf("\n");
}
//类型1测试
void TestPrintArrMaxTopK()
{
int n = 100;
int* a = (int*)malloc(sizeof(int) * n);
if (a == NULL)
{
perror("malloc fail");
exit(-1);
}
srand((unsigned int)time(NULL));
//产生1万个随机值
int i = 0;
for (i = 0; i < n; ++i)
{
//注意:rand函数产生随机值的范围是0 ~ 32767。
a[i] = rand() % 10000 + 1;//随机值的范围1~10000
}
//把数组a中的10个元素改成10001 ~ 10010。这样做的目的是:测试PrintArrMaxTopK函数是否正
确找出数组a中最大的10个元素。
a[5] = 10000 + 1;
a[12] = 10000 + 2;
a[53] = 10000 + 3;
a[98] = 10000 + 4;
a[15] = 10000 + 5;
a[35] = 10000 + 6;
a[99] = 10000 + 7;
a[76] = 10000 + 8;
a[43] = 10000 + 9;
a[66] = 10000 + 10;
//打印
Print(a, n);
printf("\n");
//找出n个数中前k = 10个最大的数,并打印前k = 10个最大的数。
PrintArrMaxTopK(a, n, 10);
printf("\n");
//打印整个数组a
Print(a, n);
printf("\n");
//释放动态数组a
free(a);
a = NULL;
}
void TestPrintArrMinTopK()
{
int n = 100;
int* a = (int*)malloc(sizeof(int) * n);
if (a == NULL)
{
perror("malloc fail");
exit(-1);
}
srand((unsigned int)time(NULL));
//产生1万个随机值
int i = 0;
for (i = 0; i < n; ++i)
{
//注意:rand函数产生随机值的范围是0 ~ 32767。
a[i] = rand() % 10000 + 11;//随机值的范围1~10000
}
//把数组a中的10个元素改成1 ~ 10。这样做的目的是:测试PrintArrMinTopK函数是否正确找出数
组a中最小的10个元素。
a[5] = 1;
a[12] = 2;
a[53] = 3;
a[98] = 4;
a[15] = 5;
a[35] = 6;
a[99] = 7;
a[76] = 8;
a[43] = 9;
a[66] = 10;
//打印
Print(a, n);
printf("\n");
//找出n个数中前k = 10个最小的数,并打印前k = 10个最小的数。
PrintArrMinTopK(a, n, 10);
printf("\n");
//打印整个数组
Print(a, n);
printf("\n");
//释放动态数组a
free(a);
a = NULL;
}
//类型2测试
//生成随机N个数据的文件+解决Tok问题:找N个数中前K个最大的数。
void TestPrintFileMaxTopK()
{
//一、写n个数据进文件中
int n, k;//n表示文件中的数据个数,k表示文件n个数中最大前k个的数
printf("请输入n和k:>");
//1.输入n、k
scanf("%d%d", &n, &k);
//2.打开文件data.txt
FILE* fin = fopen("data.txt", "w");
if (fin == NULL)
{
perror("fopen fail");
return;
}
//3.把n个数写入文件data.txt中
//(1)随机生成n-k个数据并把这n-k个数据写入指针fin指向的文件data.txt中。
int i = 0;
for (i = 0; i < n - k; i++)
{
int val = rand() % 10000 + 1;//产生随机数的范围1 ~ 10000
fprintf(fin, "%d\n", val);
}
//(2)把k个数据写入指针fin指向的文件data.txt中。
for (i = 10001; i < 10001 + k; i++)
fprintf(fin, "%d\n", i);
//关闭文件data.txt
fclose(fin);
//二、创建k个元素大小的小堆来解决topk问题
PrintFileMaxTopK("data.txt", k);
}
//生成随机N个数据的文件+解决Tok问题:找N个数中前K个最小的数。
void TestPrintFileMinTopK()
{
//一、写n个数据进文件中
int n, k;//n表示文件中的数据个数,k表示文件n个数中最大前k个的数
printf("请输入n和k:>");
//1.输入n、k
scanf("%d%d", &n, &k);
//2.打开文件
FILE* fin = fopen("data.txt", "w");
if (fin == NULL)
{
perror("fopen fail");
return;
}
//3.把n个数写入文件data.txt中
//(1)随机生成n-k个数据并把这n-k个数据写入指针fin指向的文件data.txt中。
int i = 0;
for (i = 0; i < n - k; i++)
{
int val = rand() % 10000 + 11;//产生随机数的范围11 ~ 10011
fprintf(fin, "%d\n", val);
}
//(2)把k个数据写入指针fin指向的文件data.txt中。
for (i = 1; i <= k; i++)
fprintf(fin, "%d\n", i);
//关闭文件
fclose(fin);
//二、创建k个元素大小的大堆来解决topk问题
PrintFileMinTopK("data.txt", k);//注意:"data.txt"是文件名字符串
}
int main()
{
srand((size_t)time(NULL));
//类型1的测试用例
//TestPrintArrMaxTopK();
TestPrintArrMinTopK();
//类型2的测试用例
//TestPrintFileMaxTopK();
//TestPrintFileMinTopK();
return 0;
}