如果 x > 堆顶(5):x 比我们 Top3 里最弱的还要大,有资格进 Top3 。把堆顶(5,最弱的候选)删掉,把 x 放进去。
如果 x <= 堆顶(5):x 连当前 Top‑K 里最弱的都比不过,直接抛弃。
堆顶相当于 "门槛",比门槛大就替换门槛。
❓那为什么不能用大顶堆?
如果用大顶堆存 K 个元素:大顶堆堆顶是堆里的最大值 。堆里面 K 个数字,堆顶是最大的,你根本不知道 K 个里面谁最小!新来一个数字,你没法快速判断这个数够不够资格进 TopK。
大顶堆只能知道谁最大,不知道 K 个里面的底线(最小值),做不到筛选。
当然你也可以建一个 n 大小的大顶堆,循环 pop K 次拿最大值。但复杂度是 O(nlogn)。而小顶堆只维护 K 个节点,每次操作 logK,总复杂度 O(nlogK)。当 n 很大(百万、海量数据),K 远小于 n 的时候,速度差距巨大。
举实例
数组:[2,7,3,9,1,8,4],找最大 3 个。K=3,小顶堆容量固定 3。
前 3 个入堆:2,7,3,小顶堆堆顶 = 2(门槛是 2)
下一个 x=9:9>2 → 弹出 2,压入 9;堆:3,7,9,门槛变成3
x=1:1<3,直接跳过
x=8:8>3 → 弹出 3,压入 8;堆:7,9,8,门槛变成7
x=4:4<7,直接跳过
遍历结束,堆内 7,9,8,就是最大 3 个元素,堆顶7就是第 3 大元素。
反向:求前 K 小,建大顶堆
找最小 K 个,堆内存 K 个候选,**大顶堆堆顶 = 堆内最大值(门槛)**新来数字比堆顶更小,就替换堆顶。
考试一句话答案(写卷子)
求前 K 个最大元素,构建大小为 K 的小顶堆。堆顶代表当前 K 个候选元素中的最小值,作为筛选门槛;若新元素大于堆顶,则说明该元素属于前 K 大,替换堆顶。堆的大小始终维持 K,时间复杂度O(nlogK)。
记忆技巧
要保留 K 个最好的,堆顶放这 K 个里面最差的,用来当门槛。
K 个最大的,K 里面最差的就是最小 → 小顶堆
K 个最小的,K 里面最差的就是最大 → 大顶堆
代码如下:
cpp复制代码
//topk
void CreateNDate()
{
// 造数据
int n = 100000;
srand(time(0));
const char* file = "data.txt";
FILE* fin = fopen(file, "w");
if (fin == NULL)
{
perror("fopen error");
return;
}
for (int i = 0; i < n; ++i)
{
int x = (rand() + i) % 1000000;
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
void TopK()
{
int k = 0;
printf("请输入K:");
scanf("%d", &k);
const char* file = "data.txt";
FILE* fout = fopen(file, "r");
if (fout == NULL)
{
perror("fopen fail!");
exit(1);
}
//找最大的前K个数据,建小堆
int* minHeap = (int*)malloc(sizeof(int) * k);
if (minHeap == NULL)
{
perror("malloc fail!");
exit(2);
}
for (int i = 0; i < k; i++)
{
fscanf(fout, "%d", &minHeap[i]);
}
//minHeap -- 向下调整建堆
for (int i = (k-1-1)/2; i >= 0; i--)
{
AdjustDown(minHeap, i, k);
}
//遍历剩下的n-k个数据,跟堆顶比较,堆顶小替换堆顶元素
int x = 0;
while (fscanf(fout,"%d",&x) != EOF)
{
//X minHeap-top
if (x < minHeap[0])
{
minHeap[0] = x;
AdjustDown(minHeap, 0, k);
}
}
for (int i = 0; i < k; i++)
{
printf("%d ", minHeap[i]);
}
fclose(fout);
}
int main()
{
//CreateNDate();
TopK();
return 0;
}