从零开始手写堆:核心操作实现 + 堆排序 + TopK 算法 + 向上调整 vs 向下调整建堆的时间复杂度严密证明!
原创
文章标签:
C语言数据结构算法堆排序
写在前面: 堆是算法面试中的高频考点,也是实现优先级队列、TopK、堆排序的基础。本文将使用 C语言 从零手写堆的各个接口,并深入分析两种建堆方式的时间复杂度差异。本文基于实际代码编写,力求让读者能够真正理解堆的原理。
目录
一、什么是堆
➤ 一句话定义
堆(Heap) 是一种特别的完全二叉树,分为大根堆(大堆)和小根堆(小堆)。
- 小根堆(小堆) :所有父节点的值都 小于或等于 其子节点的值
- 大根堆(大堆) :所有父节点的值都 大于或等于 其子节点的值
➤ 堆的性质
-
堆中某个结点的值总是不大于或不小于其父结点的值
- 小根堆:父节点 ≤ 子节点
- 大根堆:父节点 ≥ 子节点
-
堆总是一棵完全二叉树
- 除了最后一层,其他层都是满的
- 最后一层节点从左到右依次排列
➤ 堆的存储
二叉树本质上都可以通过二叉链的形式存储。但是完全二叉树和满二叉树由于其特性,可以用数组存储。
为什么要用数组存储?
| 优点 | 说明 |
|---|---|
| 提高缓存命中率 | 数组是连续内存,CPU缓存预取更高效 |
| 快速定位节点 | 利用下标之间的数学关系,可以快速找到父子节点 |
| 节省空间 | 不需要额外的指针存储 |
一个非线性的数据结构以线性方式存储,这样的存储方式让现实与想象产生隔阂,让物理与逻辑发生分歧。但是出于效率需要,这样的操作是不可避免的。
下标关系(重要!):
c
// 假设当前节点下标为 index
左孩子下标 = index * 2 + 1
右孩子下标 = index * 2 + 2
父节点下标 = (index - 1) / 2
为什么父节点计算只有一个公式?
因为整数除法会自动向下取整:
- 左孩子 n = 2k+1:
(n-1)/2 = (2k)/2 = k - 右孩子 n = 2k+2:
(n-1)/2 = (2k+1)/2 = k
无论奇数偶数,都用 (index - 1) / 2 这一个公式即可。
二、如何实现一个堆
我们以 C语言 为例,实现一个通用类型的堆(支持任意数据类型),并提供基本接口。
堆的接口定义
c
// 堆的初始化
void HPInit(Heap* php);
// 堆的销毁
void HPDestory(Heap* php);
// 堆的插入
void HPPush(Heap* php, HPDataType x);
// 堆的删除
void HPPop(Heap* php);
// 取堆顶的数据
HPDataType HPTop(Heap* php);
// 堆的判空
bool HPEmpty(Heap* php);
2.1 堆的结构定义
c
// 数据类型重命名,方便修改
typedef int HPDataType;
// 堆的结构体
typedef struct Heap {
HPDataType* _a; // 指向数组的指针
int _size; // 当前元素个数
int _capacity; // 容量
} Heap;
说明:
- 指针
_a用来指向数组的位置 _size表示数组中已经有的数据个数_capacity表示数组的容量大小- 我们的堆是基于数组的,事实上我们也可以理解为顺序表
2.2 初始化和销毁
c
// 堆的初始化
void HPInit(Heap* php) {
assert(php);
php->_capacity = 0;
php->_a = NULL;
php->_size = 0;
}
// 堆的销毁
void HPDestory(Heap* php) {
assert(php);
free(php->_a);
php->_a = NULL;
php->_capacity = 0;
php->_size = 0;
}
2.3 交换函数
c
// 交换函数
void Swap(HPDataType* p1, HPDataType* p2) {
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
2.4 向上调整算法(AdjustUp)
作用: 当向堆中插入新元素时,需要将新元素向上调整到正确位置,维持堆的性质。
c
// 向上调整(小堆)
void AdjustUp(HPDataType* x, int child) {
assert(x);
int parent = (child - 1) / 2;
while (child > 0) {
// 小堆:孩子比父亲小就交换
if (x[child] < x[parent]) {
Swap(&x[child], &x[parent]);
child = parent;
parent = (child - 1) / 2;
} else {
break;
}
}
}
这个函数的核心逻辑如下:
- 输入的是新插入元素在数组中的下标,即"孩子"的位置
- 通过公式
父亲结点下标 = (孩子结点下标 - 1) / 2计算出其父节点的位置 - 然后比较父节点与子节点的值
- 以小根堆为例:如果父节点的值大于子节点的值,则违反了堆的性质
- 此时,交换父子节点的值,并将交换后的新父节点位置作为下一个待处理的"孩子"位置
- 继续向上进行比较与调整
- 这一过程不断重复,直到满足堆的性质(父节点 ≤ 子节点)或到达堆顶为止
由此实现了自动向上调整(AdjustUp)的机制,确保新插入的元素被正确地"上浮"到合适位置。
2.5 插入操作
c
// 堆的插入
void HPPush(Heap* php, HPDataType x) {
assert(php);
// 1. 扩容检查
if (php->_size == php->_capacity) {
int newcapacity = php->_capacity == 0 ? 4 : php->_capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->_a, newcapacity * sizeof(HPDataType));
if (tmp == NULL) {
perror("realloc::tmp");
exit(1);
}
php->_a = tmp;
php->_capacity = newcapacity;
}
// 2. 插入到末尾
php->_a[php->_size] = x;
php->_size++;
// 3. 向上调整
AdjustUp(php->_a, php->_size - 1);
}
插入过程分为两步:
- 扩容检查:确保有足够的空间
- 插入并调整:将新元素放入末尾,并向上调整至正确位置(维持堆的性质)
例如,在大根堆中,插入的新元素必须小于等于其父节点;在小根堆中,则必须大于等于其父节点。
2.6 向下调整算法(AdjustDown)
作用: 删除堆顶元素后,需要将新的堆顶向下调整到正确位置。
c
// 向下调整(小堆)
void AdjustDown(HPDataType* x, int n, int parent) {
assert(x);
int child = parent * 2 + 1; // 先假设左孩子最小
while (child < n) {
// 1. 找出左右孩子中较小的那个
// 注意:要先判断右孩子是否存在
if (child + 1 < n && x[child + 1] < x[child]) {
child++;
}
// 2. 比较孩子和父亲
// 如果孩子比父亲小,则交换
if (x[child] < x[parent]) {
Swap(&x[parent], &x[child]);
parent = child;
child = parent * 2 + 1;
} else {
break;
}
}
}
关键点解析
| 注意点 | 说明 |
|---|---|
| 为什么先假设左孩子最小? | 使用"假设法"更高效:先假设左孩子较小,再判断是否应改为右孩子,减少不必要的比较次数 |
为什么需要 child + 1 < n 判断? |
当 child + 1 >= n 时,说明右孩子不存在,无需比较;因此在比较前必须加上 child + 1 < n 的判断,防止越界 |
循环条件 child < n |
当 child >= n 时,说明当前节点没有子节点,无需继续调整 |
和向上调整算法不同,向下调整算法首先得找到父亲。用父亲找到左孩子和右孩子,以小堆为例。判断得到左右孩子中较小的那个,比较它和父亲的大小关系,如果它比父亲小,交换,然后让孩子的位置变成新的父亲反复操作直到找不到下一个孩子为止。
2.7 删除堆顶操作
c
// 堆的删除(删除堆顶)
void HPPop(Heap* php) {
assert(php);
assert(php->_size > 0);
// 1. 首尾交换
Swap(&php->_a[0], &php->_a[php->_size - 1]);
// 2. 逻辑删除最后一个元素
php->_size--;
// 3. 向下调整
AdjustDown(php->_a, php->_size, 0);
}
首先,删除操作前必须确保堆不为空,并且堆的指针有效(非空指针)。
若直接删除堆顶元素,会导致原本的第二个元素成为新的堆顶,但此时它可能违背堆的性质(例如在大根堆中比其子节点小),从而破坏整个堆的结构。这种破坏会引发连锁反应,使得所有父子和兄弟关系失序,堆不再满足定义。
为避免这一问题,我们采用一种巧妙的"间接删除法":
- 将堆顶元素与数组末尾的元素交换
- 然后将最后一个元素逻辑上移除(即 size--)
- 此时新堆顶可能不满足堆的性质,因此需要对它进行向下调整(AdjustDown),使其重新恢复堆的有序性
这样既保留了堆的结构完整性,又实现了高效删除。
2.8 获取堆顶和判空
c
// 获取堆顶元素
HPDataType HPTop(Heap* php) {
assert(php);
assert(php->_size > 0);
return php->_a[0];
}
// 堆的判空
bool HPEmpty(Heap* php) {
assert(php);
return php->_size == 0;
}
不用过多解释,最简单的部分。
三、堆排序
先说结论
堆排序的步骤:
- 建堆:用向下调整法建堆(正向排序建大堆,逆向排序建小堆)
- 排序 :首尾互换,然后在
[0, end)范围内向下调整 - 循环:重复上述过程
c
// 堆排序
void HeapSort(int* a, int n) {
// 1. 从最后一个非叶子节点开始,向下调整建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--) {
AdjustDown(a, n, i);
}
// 2. 首尾交换 + 向下调整
int end = n;
while (end > 0) {
Swap(&a[0], &a[end - 1]);
AdjustDown(a, --end, 0);
}
}
解释原理
1. 建堆
建堆分为从下往上调整建堆 和从上往下调整建堆。
- 从下往上调整建堆:从最后一个非叶子节点开始,向上遍历到根节点,对每个节点执行向下调整
- 从上往下调整建堆:先假设第一个节点已经在堆里面了,然后从第二个节点开始到最后一个节点,所有的结点全部做一次向上调整
| 方式 | 描述 | 时间复杂度 |
|---|---|---|
| 从下往上(向下调整) | 从最后一个非叶子节点开始向上,每个节点做向下调整 | O(N) ⭐ |
| 从上往下(向上调整) | 假设第一个节点是堆,从第二个节点开始向上调整 | O(N log N) |
2. 首尾交换,调整,从尾向首形成排序
- 建好小堆后,堆顶元素即为当前所有元素中的最小值
- 交换堆顶与末尾元素
- 将有效堆的边界指针 end 减 1,表示最小值已"归位",不再参与后续调整
- 对新的堆顶元素执行向下调整,使其重新满足小根堆的性质
- 重复上述过程
最终结果:
- 从后往前:从小到大的升序序列
四、TopK算法
1. 一句话定义
Top-K 算法:从海量数据中找出最大(或最小)的前 K 个元素,在大规模数据场景下具有显著优势。
2. 探索实现思路
很多人最先想到的 Top-K 解法是:先对全部数据建堆,再用类似堆排序的方式依次取出前 K 个最大(或最小)的元素。
但问题来了------
- 如果数据量是 100 万,还可以接受
- 可如果是 10 亿条数据 呢?
一个 int 占 4 字节,10 亿个整数就需要:
4 × 10⁹ 字节 ≈ 4 GB 内存
为找区区的 K 个数,而占用数 GB 内存,代价显然过高。
于是你可能会想:
"那我可以把数据分批处理!比如分 100 次,每次从硬盘读 1000 万条,分别找出每批的前 K 个,最后从这 100×K 个候选中再选 Top-K。"
这确实缓解了内存压力,但带来了新问题:
- 需要多次 I/O 操作,速度慢
- 最终仍需处理 100×K 的数据,若 K 很大,效率依然低下
- 时间复杂度和工程复杂度显著上升
那么,有没有一种既节省内存,又高效的方法?
答案是:有!那就是 top-k 经典解法------使用大小为 K 的堆。
3. 实现代码
步骤一:生成大量随机数
c
// 创造 n 个随机数据
void CreateData(const char* filename, int n) {
FILE* fp = fopen(filename, "w");
if (fp == NULL) {
perror("fopen fail");
return;
}
srand(time(0));
for (int i = 0; i < n; i++) {
int x = (rand() + i) % 10000;
fprintf(fp, "%d\n", x);
}
fclose(fp);
}
步骤二:实现 top-k
c
// TopK:找最大的 K 个数
void TestTopK(int k) {
const char* filename = "data.txt";
FILE* fp = fopen(filename, "r");
if (fp == NULL) {
perror("fopen fail");
return;
}
// 1. 读取前 k 个数,建立小堆
int* minHeap = (int*)malloc(sizeof(int) * k);
if (minHeap == NULL) {
perror("malloc fail");
fclose(fp);
return;
}
for (int i = 0; i < k; i++) {
fscanf(fp, "%d", &minHeap[i]);
}
// 向下调整建小堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--) {
AdjustDown(minHeap, k, i);
}
// 2. 遍历剩余数据
int x;
while (fscanf(fp, "%d", &x) > 0) {
// 如果比堆顶大,替换堆顶并调整
if (x > minHeap[0]) {
minHeap[0] = x;
AdjustDown(minHeap, k, 0);
}
}
// 3. 输出结果
printf("最大的 %d 个数是:\n", k);
for (int i = 0; i < k; i++) {
printf("%d ", minHeap[i]);
}
printf("\n");
free(minHeap);
fclose(fp);
}
4.Topk代码部分内容讲解
为什么用小堆找最大的 K 个数?
- 小堆的堆顶是当前 K 个数中的最小值
- 如果新数据比堆顶大,说明它有资格进入 TopK → 替换堆顶,向下调整
- 如果比堆顶小,直接丢弃
TopK 算法的优点:
| 指标 | 复杂度 |
|---|---|
| 空间复杂度 | O(K),远小于 O(N) |
| 时间复杂度 | O(N log K) |
TopK 时间复杂度计算
步骤1:初始化小根堆
-
先从数据中取前K个元素,构建一个大小为K的小根堆。
-
构建堆的时间复杂度为O(K)(因为向下调整建堆的时间复杂度为O(K))。
步骤2:遍历剩余元素
-
剩下的元素数量为 n-K 个,每个元素都需要和堆顶比较:
-
如果元素比堆顶大,就替换堆顶并进行一次向下调整,调整的时间复杂度为O(logK)(因为堆的高度为log K)。
-
如果元素比堆顶小,直接跳过。
-
这一步的时间复杂度为 (n-K) * O(log K),当n远大于K时,近似为O(n logK)。
-
(n-K) *O(log K)这是因为我们假设剩下的每一个都比堆顶大所以每一个都要进行向下比较。
步骤3:总时间复杂度
将两步的时间复杂度相加:
O(K) + O(n log K)
因为K <= n,所以O(K)可以被O(n log K)覆盖,最终时间复杂度为O(n logK)。
最终答案:TopK问题(小根堆法)的时间复杂度为O(n log K)
5.📂 文件操作函数详解
在实现 TopK 算法时,我们使用文件来模拟海量数据。下面详细讲解每个文件操作函数。
1. fopen - 打开文件
c
FILE* fp = fopen(filename, mode);
| 参数 | 说明 |
|---|---|
| filename | 文件名(含路径) |
| mode | 打开模式 |
常用模式:
| 模式 | 含义 |
|---|---|
"r" |
读取(文件必须存在) |
"w" |
写入(文件不存在则创建,存在则清空) |
"a" |
追加(文件不存在则创建) |
返回值:
- 成功:返回 FILE 指针
- 失败:返回 NULL,可用
perror()输出错误信息
2. fprintf - 写入文件
c
fprintf(fp, "%d\n", x);
类似 printf,但输出到文件而不是屏幕。
格式化说明:
| 格式 | 说明 |
|---|---|
%d |
整数 |
%f |
浮点数 |
%s |
字符串 |
%c |
字符 |
\n |
换行符 |
3. fscanf - 读取文件
c
fscanf(fp, "%d", &x);
类似 scanf,但从文件读取而不是键盘。
返回值:
- 成功读取的项数
- 读到文件末尾返回 EOF(-1)
注意: while (fscanf(fp, "%d", &x) > 0) 表示成功读取一个整数就读下去。
4. fclose - 关闭文件
c
fclose(fp);
为什么需要 fclose?
- 将缓冲区数据写入文件
- 释放 FILE 结构内存
- 重要:忘记 fclose 可能导致数据丢失!
五、数学证明:向下调整建堆比向上调整更优
这是本文的核心重点!我们在上文实际上埋藏了一个伏笔------为什么堆排序的建堆操作要使用向下建堆法?
5.1 向上调整建堆时间复杂度
向上调整建堆的时间复杂度分析(最坏情况)
我们考虑最坏情况:每个新插入的结点都需要从其初始位置一路向上调整至堆顶。
1. 堆的结构设定
设堆共有 h 层(根结点位于第 0 层)。
- 第 0 层有 2⁰ = 1 个结点
- 第 1 层有 2¹ = 2 个结点
- 第 2 层有 2² = 4 个结点
- ...
- 第 h-1 层有 2^(h-1) 个结点
总结点数为等比数列之和:
N = 2⁰ + 2¹ + 2² + ... + 2^(h-1) = 2^h - 1
由此可得堆的高度(层数):
h = log₂(N+1) // 因为 2^h = N+1,所以 h = log₂(N+1)
2. 单次调整代价
在向上调整建堆过程中,插入一个新结点时,它最多需要从当前位置一路向上调整到根结点。
- 位于第 1 层的结点,最多向上调整 1 次
- 位于第 2 层的结点,最多向上调整 2 次
- 位于第 i 层的结点,最多向上调整 i 次
- 位于第 h-1 层的结点,最多向上调整 h-1 次
因此,单次调整的时间复杂度为 O(logN)(因为树的高度约为 log₂N)。
3. 总体时间复杂度(最坏情况)
在最坏情况下,假设每一层的所有结点都需执行最大次数的向上调整:
| 层数 | 节点数 | 最多调整次数 | 总调整次数 |
|---|---|---|---|
| 第 0 层 | 2⁰ = 1 | 0 次 | 1 × 0 = 0 |
| 第 1 层 | 2¹ = 2 | 1 次 | 2 × 1 = 2 |
| 第 2 层 | 2² = 4 | 2 次 | 4 × 2 = 8 |
| 第 3 层 | 2³ = 8 | 3 次 | 8 × 3 = 24 |
| ... | ... | ... | ... |
| 第 h-1 层 | 2^(h-1) | h-1 次 | 2^(h-1) × (h-1) |
总操作次数:
T(n) = 2⁰·0 + 2¹·1 + 2²·2 + ... + 2^(h-1)·(h-1)
使用错位相减法求解:
设 S = 2¹·1 + 2²·2 + ... + 2^(h-1)·(h-1) ...... (1)
2S = 2²·1 + 2³·2 + ... + 2^h·(h-1) ...... (2)
用 (2) - (1):
2S - S = 2^h·(h-1) - [1 + 2 + ... + (h-1)]
S = 2^h·(h-1) - h(h-1)/2
= (h-1)·(2^h - h/2)
代入 2^h = N+1 和 h = log₂(N+1):
T(n) ≈ (log₂N) · (N - log₂N/2)
= N·log₂N - N·log₂N/2 - log²N + logN/2
= O(n log n)
为什么是 O(N log N)?
因为大部分节点位于底层(接近 h-1 层),它们需要较长的调整路径,底层节点数量约 N/2,每个约调整 logN 次,所以总复杂度约为 N/2 × logN = O(N log N)。
5.2 向下调整建堆时间复杂度
向下调整建堆的时间复杂度分析
设定同样的堆结构:
| 层数 | 节点数 | 最多向下调整次数 | 总调整次数 |
|---|---|---|---|
| 第 0 层 | 2⁰ = 1 | h-1 次 | 1 × (h-1) |
| 第 1 层 | 2¹ = 2 | h-2 次 | 2 × (h-2) |
| 第 2 层 | 2² = 4 | h-3 次 | 4 × (h-3) |
| 第 3 层 | 2³ = 8 | h-4 次 | 8 × (h-4) |
| ... | ... | ... | ... |
| 第 h-1 层 | 2^(h-1) | 0 次 | 2^(h-1) × 0 |
注意:向下调整和向上调整不同!
- 根节点在顶层,只需向下调整 h-1 次
- 底层节点(叶节点)无需向下调整,因为它们没有孩子
总操作次数:
T(n) = 2⁰·(h-1) + 2¹·(h-2) + 2²·(h-3) + ... + 2^(h-1)·0
使用错位相减法求解:
设 S = 2⁰·(h-1) + 2¹·(h-2) + ... + 2^(h-1)·0 ...... (1)
2S = 2¹·(h-1) + 2²·(h-2) + ... + 2^h·0 ...... (2)
用 (2) - (1):
2S - S = h - [2⁰ + 2¹ + ... + 2^(h-1)]
S = h - (2^h - 1)
= h - n
因为 h = log₂(n+1):
T(n) = log₂(n+1) - n ≈ O(n)
为什么是 O(N)?
因为大部分节点位于底层(占 N/2 个节点),它们只需要调整很少的次数(0-1 次);而根节点虽然调整次数最多(h-1 次),但根节点只有 1 个。
所以总调整次数 ≈ N/2 × 0 + N/4 × 1 + N/8 × 2 + ... ≈ 2N = O(N)。
这就是向下调整建堆的精妙之处:虽然根节点调整路径长,但只有 1 个;大部分节点虽然数量多,但调整路径短!
5.3 结论
| 建堆方法 | 时间复杂度 |
|---|---|
| 向上调整 | O(N log N) |
| 向下调整 | O(N) ⭐ |
直观理解:
- 向上调整:大部分节点在底层,需要长距离上浮,调整次数多
- 向下调整:大部分节点在底层,只需短距离下沉,调整次数少
这就是为什么堆排序中使用向下调整建堆!
完整测试代码
c
#include "Heap.h"
#include <time.h>
// 堆排序
void HeapSort(int* a, int n) {
// 从最后一个非叶子节点开始,向下调整建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--) {
AdjustDown(a, n, i);
}
int end = n;
while (end > 0) {
Swap(&a[0], &a[end - 1]);
AdjustDown(a, --end, 0);
}
}
// 创建测试数据
void CreateData() {
int n = 100;
const char* file = "data.txt";
FILE* fp = fopen(file, "w");
if (fp == NULL) {
perror("fopen fail");
return;
}
srand(time(0));
for (int i = 0; i < n; i++) {
int x = (rand() + i) % 10000;
fprintf(fp, "%d\n", x);
}
fclose(fp);
}
// 测试 TopK
void TestTopK() {
int k;
printf("请输入 k: ");
scanf("%d", &k);
const char* file = "data.txt";
FILE* fp = fopen(file, "r");
if (fp == NULL) {
perror("fopen error");
return;
}
// 1. 建立大小为 k 的小堆
int* minHeap = (int*)malloc(sizeof(int) * k);
for (int i = 0; i < k; i++) {
fscanf(fp, "%d", &minHeap[i]);
}
for (int i = (k - 1 - 1) / 2; i >= 0; i--) {
AdjustDown(minHeap, k, i);
}
// 2. 遍历剩余数据
int x;
while (fscanf(fp, "%d", &x) > 0) {
if (x > minHeap[0]) {
minHeap[0] = x;
AdjustDown(minHeap, k, 0);
}
}
// 3. 输出结果
printf("最大的 %d 个数是:\n", k);
for (int i = 0; i < k; i++) {
printf("%d ", minHeap[i]);
}
printf("\n");
free(minHeap);
fclose(fp);
}
int main() {
// 测试堆排序
int arr[] = {9, 1, 5, 3, 7, 2, 8, 4, 6, 0};
int n = sizeof(arr) / sizeof(arr[0]);
printf("排序前: ");
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
HeapSort(arr, n);
printf("排序后: ");
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
// 测试 TopK
CreateData();
TestTopK();
return 0;
}
Heap.h 头文件:
c
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int HPDataType;
typedef struct Heap {
HPDataType* _a;
int _size;
int _capacity;
} Heap;
void HPInit(Heap* php);
void HPDestory(Heap* php);
void Swap(HPDataType* p1, HPDataType* p2);
void AdjustUp(HPDataType* x, int child);
void AdjustDown(HPDataType* x, int n, int parent);
void HPPush(Heap* php, HPDataType x);
void HPPop(Heap* php);
HPDataType HPTop(Heap* php);
bool HPEmpty(Heap* php);
总结
- 堆是一种特别的完全二叉树,分为大根堆和小根堆
- 堆的存储:完全二叉树适合用数组存储,利用下标关系快速定位父子节点
- 堆的实现 核心在于向上调整 和向下调整两个算法
- 堆排序利用向下调整建堆,时间复杂度 O(N log N)
- TopK问题用大小为 K 的小堆解决,空间复杂度 O(K),时间复杂度 O(N log K)
- 向下调整建堆比向上调整更优,时间复杂度从 O(N log N) 优化到 O(N)
希望这篇详细的 C语言 版文章能帮助你真正理解堆的原理,如果觉得有收获,欢迎点赞、收藏并分享给更多人!