目录
[七、文件版本 TopK(工程思维🔥)](#七、文件版本 TopK(工程思维🔥))
一、TopK我到底在干嘛?
一句话:
<<在一堆数据里,找出最大的 K 个数(或最小的 K 个)>>
⚠️ 但关键不在"找",而在:
👉 数据可能很多,不能(或不想)全部排序
比如:
-
100万数据,只要前10个
-
日志文件、数据流
👉 这时候排序就是浪费
二、最直观但低级的方法(排序)
qsort(arr, n, sizeof(int), cmp);
然后取前 K 个
❗ 问题
-
时间复杂度:O(n log n)
-
你只要 K 个,却排了 n 个
👉 明显过度计算
🎯 qsort
qsort 的时间复杂度来自快速排序,其核心是"分治思想"。每一轮通过 partition 将数组划分为两部分,单层时间复杂度为 O(n),在平均情况下递归深度为 log n,因此总体复杂度为 O(n log n)。在极端情况下(如数据有序),可能退化为 O(n²)。
🧠 再给你一个更深的理解(加分)
快排快的本质不是"比较少",而是:
每一层都在减少问题规模
三、核心思想(这题灵魂🔥)
<<❗ 用一个"大小为 K 的堆",动态维护结果>>
🧠 关键理解(非常重要)
TopK不是一次性找,而是:
<<不断淘汰不合格的,只留下最好的 K 个>>
四、为什么用"小顶堆"?(必须搞透)
🎯 我要找:最大的 K 个数
👉 用:小顶堆
🔍 为什么??(重点)
堆里只放 K 个数:
-
堆顶 = 当前最小的
-
它是"最弱的那个"
💡 本质一句话
<<用最小的那个,卡住门槛>>
🧪 举个例子
K = 3
当前堆: [5, 8, 10]
👉 5 是最小(堆顶)
新来一个数:
情况1:x = 4
👉 4 < 5 ❌
👉 连门槛都没过 → 直接扔
情况2:x = 12
👉 12 > 5 ✅
👉 替换掉 5
👉 然后重新调整堆
---
🎯 总结本质
<<堆顶永远是"最该被淘汰的人">>
---
五、完整算法流程(必须背下来)
Step 1️⃣:拿前 K 个建小堆
cpp
for (int i = 0; i < k; i++)
{
heap[i] = arr[i];
}
Step 2️⃣:建堆
cpp
for (int i = (k - 2) / 2; i >= 0; i--)
{
AdjustDown(heap, k, i);
}
❗ 为什么从 (k-2)/2 开始?
👉 因为这是最后一个非叶子节点
Step 3️⃣:遍历剩下的数据
cpp
if (x > heap[0])
{
heap[0] = x;
AdjustDown(heap, k, 0);
}
❗ 为什么只用向下调整?
👉 因为:
<<只有堆顶被破坏,其余子树是正常的>>
六、代码实现(核心)
🔧 向下调整(小顶堆)
cpp
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++;
}
// 如果孩子比父亲小,就交换
if (a[child] < a[parent])
{
int tmp = a[child];
a[child] = a[parent];
a[parent] = tmp;
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
七、文件版本 TopK(工程思维🔥)
💡 为什么用文件?
<<❗ 因为数据可能大到放不进内存>>
完整代码
cpp
#include "Heap.h"
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// 创建随机数据写入文件
void CreateNDate()
{
int n = 1000;
srand(time(0));
FILE* fin = fopen("data.txt", "w");
if (fin == NULL)
{
perror("fopen error");
return;
}
for (int i = 0; i < n; ++i)
{
int x = (rand() + i) % 10000;
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
void text3()
{
CreateNDate();
int k = 10;
int* heap = (int*)malloc(sizeof(int) * k);
if (heap == NULL)
{
perror("malloc error");
return;
}
FILE* fout = fopen("data.txt", "r");
if (fout == NULL)
{
perror("fopen error");
return;
}
// 读前k个
for (int i = 0; i < k; i++)
{
fscanf(fout, "%d", &heap[i]);
}
// 建小堆
for (int i = (k - 2) / 2; i >= 0; --i)
{
AdjustDown(heap, k, i);
}
int x = 0;
while (fscanf(fout, "%d", &x) == 1)
{
if (x > heap[0])
{
heap[0] = x;
AdjustDown(heap, k, 0);
}
}
printf("TopK: ");
for (int i = 0; i < k; i++)
{
printf("%d ", heap[i]);
}
printf("\n");
fclose(fout);
free(heap);
}
八、文件操作(你这题用到的)
fopen
FILE* f = fopen("data.txt", "r");
模式| 含义
r| 读
w| 写(会清空)
fscanf(重点)
fscanf(f, "%d", &x);
👉 返回值:
-
1:成功
-
EOF:结束
❗ 正确写法
while (fscanf(f, "%d", &x) == 1)
fclose
fclose(fout);
👉 必须写,不然可能丢数据
九、随机数(你这次踩坑的点🔥)
rand 本质
<<❗ 伪随机数(不是完全随机)>>
srand 作用
srand(time(0));
👉 改变随机序列起点
❗ 重点结论
<<srand 不能避免重复,只是让每次结果不同>>
十、复杂度分析
方法| 复杂度
排序| O(n log n)
堆| O(n log k)
👉 当 k ≪ n 时:
<<堆是最优选择>>
十一、易错点(你必须记住🔥)
❌ 1. typedef 写反
typedef HPDateType int; ❌
👉 正确:
typedef int HPDateType; ✅
❌ 2. fscanf 写错
while (fscanf(...) > 0) ❌
👉 正确:
== 1
❌ 3. 忘记 fclose
👉 会导致数据没写入
❌ 4. 堆方向搞反
👉 TopK最大 → 小顶堆
👉 TopK最小 → 大顶堆
十二、面试官最爱问🔥
❓ 为什么用小顶堆?
👉 因为要淘汰最小的
❓ 为什么只维护 K 个?
👉 因为只关心前 K 个
❓ 为什么时间复杂度是 n log k?
👉 每次调整是 log k,总共 n 次
❓ 如果数据是流式的?
👉 仍然用小顶堆
十三、拓展(进阶)
⭐ 1. 数据流 TopK
👉 数据不断来:
-
每来一个判断一次
-
不需要全部存储
⭐ 2. 快速选择(QuickSelect)
👉 平均 O(n)
但:
-
不稳定
-
面试可能问
⭐ 3. TopK 频率问题
👉 用:
- 哈希表 + 堆
🧠 最后一段TomGo总结(必须记住)
<<TopK 的本质不是"找最大",而是:
用一个固定大小的数据结构,持续淘汰不合格元素>>
🎯 我自己的理解(复盘用)
**- 堆顶 = 当前最弱的
- 每次新数据都在"挑战门槛"
- 能留下的,才是 TopK**