💬 欢迎讨论:在阅读过程中有任何疑问,欢迎在评论区留言,我们一起交流学习!
👍 点赞、收藏与分享:如果你觉得这篇文章对你有帮助,记得点赞、收藏,并分享给更多对数据结构感兴趣的朋友
文章目录
声明:本文建议在掌握“堆的实现”后学习。
🚀传送门:【初探数据结构】二叉树的顺序结构——堆的实现详解(上下调整算法的时间复杂度分析)
一、引言
堆是一种高效处理优先级问题的数据结构,尤其在动态数据流和排序场景中表现优异。本文将通过堆排序和TopK问题两个经典案例,深入解析堆的实际应用,并提供清晰的代码实现与原理分析。
二、堆排序:将无序数组变为有序序列
1. 堆排序的核心思想
- 利用最大堆的性质:堆顶元素始终为最大值。
- 两步走策略:
- 建堆
- 升序 :建大堆
- 降序:建小堆
- 利用堆删除思想来进行排序
- 逐次提取堆顶:将堆顶元素(最大值)与数组末尾交换,缩小堆范围,重新调整堆。
2. 详细步骤图解(以升序排序为例)
初始数组:[4, 10, 3, 5, 1]
- 构建最大堆:
- 从最后一个非叶子节点(索引
5//2 -1 =1
)开始调整。 - 调整后得到最大堆:
[10, 5, 3, 4, 1]
。
- 从最后一个非叶子节点(索引
💡 建堆的两种方式
- 向上调整建堆法:
时间复杂度O(nlogn),效率低,不推荐- 向下调整建堆法
用向下调整建堆 时间复杂度O(n),推荐
//建大堆
//1.用向上调整建堆 时间复杂度O(nlogn),效率低,不推荐
for (i = 0; i < n; i++) {
AdjustUp(a, i);
}
//2.用向下调整建堆 时间复杂度O(n),推荐
for (i = (n - 1 - 1)/2; i >= 0; i--) {
AdjustDown(a, n, i);
}
这里调整算法不再赘述。如有疑问,请学习“堆的实现”
或者与我交流哦😀
- 交换与调整:
- 交换堆顶
10
与末尾1
→ 数组变为[1,5,3,4,10]
,有效堆范围减1。 - 重新调整剩余元素
[1,5,3,4]
为最大堆 →[5,4,3,1]
。 - 重复上述步骤,最终得到有序数组
[1,3,4,5,10]
。
- 交换堆顶
3. 代码实现
//升序排序,用大根堆;
//降序排序,用小根堆。
void HeapSort(HPDataType* a,int n)
{
//排升序
int i = 0;
//建大堆
//1.用向上调整建堆 时间复杂度O(nlogn),效率低,不推荐
/*for (i = 0; i < n; i++) {
AdjustUp(a, i);
}*/
//2.用向下调整建堆 时间复杂度O(n),推荐
for (i = (n - 1 - 1)/2; i >= 0; i--) {
AdjustDown(a, n, i);
}
//排序
int end = n - 1;
while (end > 0) {
swap(a, &a[end]);
AdjustDown(a, end, 0);
end--;
}
for (i = 0; i < n; i++) {
printf("%d ", a[i]);
}
}
4. 时间复杂度与特点
- 时间复杂度:O(n log n)(构建堆O(n) + 每次调整O(log n))。
- 原地排序:无需额外空间,适合内存敏感场景。
- 不稳定性:相同元素可能交换顺序。
三、TopK问题:海量数据中的高效筛选
1. 问题场景
- 需求:从一亿个数中快速找到前100大的数。
- 挑战:直接排序时间复杂度O(n log n),内存可能无法容纳全部数据。
2. 堆的解决方案
-
最小堆策略(找最大的K个元素):
- 初始化:用前K个元素构建一个最小堆(堆顶为当前最小值)。
- 遍历剩余元素:若当前元素 > 堆顶,则替换堆顶并调整堆。
- 最终结果:堆中保留的K个元素即为TopK。
-
为什么用最小堆?
- 堆顶是K个元素中的最小值,遇到更大的值时替换,确保堆中始终保留更大的候选。
3.如何创建这么多数?
- 随机数生成:利用
rand
生成随机数 - 写入文件
void CreateNumber()
{
int n = 10000;
//时间种子
srand((unsigned int)time(0));
const char* file = "data.text";
FILE* fp = fopen(file, "w");
if (fp == NULL) {
perror("fopen fail");
return;
}
//写入文件
for (size_t i = 0; i < n; ++i)
{
int x = rand() % 10000;
fprintf(fp, "%d\n", x);
}
fclose(fp);
fp = NULL;
}
3. 代码实现
void PrintTopK(const char* file, int k)
{
int* topk = (int*)malloc(sizeof(int) * k);
if (topk == NULL) {
perror("fopen fail");
return;
}
FILE* fp = fopen(file, "r");
if (fp == NULL) {
perror("fopen fail");
return;
}
//建前k个数的小根堆
for (int i = 0; i < k; i++) {
fscanf(fp,"%d",&topk[i]);
}
for (int i = (k - 2) / 2; i >= 0; i--) {
AdjustDown(topk, k, i);
}
//遍历后面的数,若比堆top大,就覆盖并向下调整
int val = 0;
int ret = fscanf(fp, "%d", &val);
while (ret != EOF) {
if (val > topk[0]) {
topk[0] = val;
AdjustDown(topk, k, 0);
}
ret = fscanf(fp, "%d", &val);
}
for (int i = 0; i < k; i++) {
printf("%d ", topk[i]);
}
free(topk);
fclose(fp);
fp = NULL;
}
4. 时间复杂度与优化
- 时间复杂度:O(n log K),远优于O(n log n)。
- 处理数据流:逐个读取数据,内存仅需维护大小为K的堆。
四、对比与总结
应用场景 | 核心思路 | 时间复杂度 | 空间复杂度 | 优势 |
---|---|---|---|---|
堆排序 | 构建堆 + 交换堆顶 | O(n log n) | O(1) | 原地排序,适合内存敏感 |
TopK问题 | 维护大小为K的最小堆 | O(n log K) | O(K) | 高效处理海量数据或数据流 |
五、实际应用拓展
- 优先队列:操作系统任务调度、医院急诊分诊。
- 实时排行榜:游戏积分实时更新前100名。
- 合并K个有序链表:利用堆高效选择最小节点。
六、常见问题解答
Q1:为什么堆排序不如快速排序常用?
- 堆排序的常数因子较大,且不稳定,实际运行速度可能慢于快速排序。
Q2:TopK问题能否用最大堆解决?
- 可以,但需维护大小为n-K的堆,空间复杂度更高,适合K接近n的场景。
七、结语
堆结构凭借其高效的插入删除和极致的空间利用率,在排序与筛选问题中占据独特地位。掌握堆排序与TopK的解法,能显著提升处理大规模数据的能力。理解原理后,可尝试手写堆实现或结合具体业务场景优化代码,进一步巩固知识。