目录
[1.1 堆的定义](#1.1 堆的定义)
[1.2 堆 vs 堆区](#1.2 堆 vs 堆区)
[1.3 应用场景](#1.3 应用场景)
[2.1 结构定义](#2.1 结构定义)
[2.2 初始化与销毁](#2.2 初始化与销毁)
[2.3 辅助函数:交换与扩容](#2.3 辅助函数:交换与扩容)
[2.4 上浮操作(用于插入)](#2.4 上浮操作(用于插入))
[2.5 插入操作](#2.5 插入操作)
[2.6 下沉操作(用于删除最大值)](#2.6 下沉操作(用于删除最大值))
[2.7 删除最大值(弹出堆顶)](#2.7 删除最大值(弹出堆顶))
[2.8 获取堆顶(不删除)](#2.8 获取堆顶(不删除))
[2.9 获取堆大小](#2.9 获取堆大小)
[七、堆的应用:Top K 问题](#七、堆的应用:Top K 问题)
一、堆的基本概念
1.1 堆的定义
堆是一种完全二叉树,分为两种:
-
大根堆:每个节点的值 ≥ 左右孩子节点的值
-
小根堆:每个节点的值 ≤ 左右孩子节点的值
用数组存储(下标从0开始):
-
节点 i 的左孩子:
2*i + 1 -
节点 i 的右孩子:
2*i + 2 -
节点 i 的父节点:
(i - 1) / 2
1.2 堆 vs 堆区
| 概念 | 含义 | 说明 |
|---|---|---|
| 堆(数据结构) | 一种树形结构,用于优先队列 | 本章讨论的内容 |
| 堆区(内存) | 程序运行时动态分配内存的区域 | malloc/free 管理的区域 |
两者是完全不同的概念,只是名字相同。
1.3 应用场景
| 场景 | 说明 |
|---|---|
| 优先队列 | 每次取出优先级最高的元素 |
| 堆排序 | O(n log n) 排序 |
| Top K 问题 | 找最大/最小的K个元素 |
| 合并 K 个有序链表 | 用堆选择最小值 |
二、堆的实现(大根堆)
2.1 结构定义
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define INIT_CAPACITY 10
typedef struct {
int *data; // 动态数组
int size; // 当前元素个数
int capacity; // 数组容量
} MaxHeap;
2.2 初始化与销毁
c
void initHeap(MaxHeap *heap) {
heap->data = (int*)malloc(INIT_CAPACITY * sizeof(int));
heap->size = 0;
heap->capacity = INIT_CAPACITY;
}
void destroyHeap(MaxHeap *heap) {
free(heap->data);
heap->data = NULL;
heap->size = 0;
heap->capacity = 0;
}
2.3 辅助函数:交换与扩容
c
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
void resize(MaxHeap *heap) {
heap->capacity *= 2;
heap->data = (int*)realloc(heap->data, heap->capacity * sizeof(int));
}
2.4 上浮操作(用于插入)
新插入的元素放在数组末尾,然后向上调整,直到满足堆性质。
c
void shiftUp(MaxHeap *heap, int index) {
while (index > 0) {
int parent = (index - 1) / 2;
if (heap->data[parent] >= heap->data[index]) {
break;
}
swap(&heap->data[parent], &heap->data[index]);
index = parent;
}
}
2.5 插入操作
c
void push(MaxHeap *heap, int value) {
if (heap->size >= heap->capacity) {
resize(heap);
}
heap->data[heap->size] = value;
shiftUp(heap, heap->size);
heap->size++;
}
2.6 下沉操作(用于删除最大值)
删除堆顶后,将最后一个元素放到堆顶,然后向下调整。
c
void shiftDown(MaxHeap *heap, int index) {
while (index * 2 + 1 < heap->size) {
int left = index * 2 + 1;
int right = index * 2 + 2;
int largest = left;
if (right < heap->size && heap->data[right] > heap->data[left]) {
largest = right;
}
if (heap->data[index] >= heap->data[largest]) {
break;
}
swap(&heap->data[index], &heap->data[largest]);
index = largest;
}
}
2.7 删除最大值(弹出堆顶)
c
int pop(MaxHeap *heap) {
if (heap->size == 0) {
printf("堆为空\n");
return -1;
}
int maxVal = heap->data[0];
heap->data[0] = heap->data[heap->size - 1];
heap->size--;
shiftDown(heap, 0);
return maxVal;
}
2.8 获取堆顶(不删除)
c
int top(MaxHeap *heap) {
if (heap->size == 0) {
printf("堆为空\n");
return -1;
}
return heap->data[0];
}
2.9 获取堆大小
c
int size(MaxHeap *heap) {
return heap->size;
}
int isEmpty(MaxHeap *heap) {
return heap->size == 0;
}
三、完整代码演示
c
#include <stdio.h>
#include <stdlib.h>
#define INIT_CAPACITY 10
typedef struct {
int *data;
int size;
int capacity;
} MaxHeap;
void initHeap(MaxHeap *heap) {
heap->data = (int*)malloc(INIT_CAPACITY * sizeof(int));
heap->size = 0;
heap->capacity = INIT_CAPACITY;
}
void destroyHeap(MaxHeap *heap) {
free(heap->data);
heap->data = NULL;
heap->size = 0;
heap->capacity = 0;
}
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
void resize(MaxHeap *heap) {
heap->capacity *= 2;
heap->data = (int*)realloc(heap->data, heap->capacity * sizeof(int));
}
void shiftUp(MaxHeap *heap, int index) {
while (index > 0) {
int parent = (index - 1) / 2;
if (heap->data[parent] >= heap->data[index]) {
break;
}
swap(&heap->data[parent], &heap->data[index]);
index = parent;
}
}
void shiftDown(MaxHeap *heap, int index) {
while (index * 2 + 1 < heap->size) {
int left = index * 2 + 1;
int right = index * 2 + 2;
int largest = left;
if (right < heap->size && heap->data[right] > heap->data[left]) {
largest = right;
}
if (heap->data[index] >= heap->data[largest]) {
break;
}
swap(&heap->data[index], &heap->data[largest]);
index = largest;
}
}
void push(MaxHeap *heap, int value) {
if (heap->size >= heap->capacity) {
resize(heap);
}
heap->data[heap->size] = value;
shiftUp(heap, heap->size);
heap->size++;
}
int pop(MaxHeap *heap) {
if (heap->size == 0) {
printf("堆为空\n");
return -1;
}
int maxVal = heap->data[0];
heap->data[0] = heap->data[heap->size - 1];
heap->size--;
shiftDown(heap, 0);
return maxVal;
}
int top(MaxHeap *heap) {
if (heap->size == 0) {
printf("堆为空\n");
return -1;
}
return heap->data[0];
}
int size(MaxHeap *heap) {
return heap->size;
}
int isEmpty(MaxHeap *heap) {
return heap->size == 0;
}
void printHeap(MaxHeap *heap) {
printf("堆内容(数组形式): ");
for (int i = 0; i < heap->size; i++) {
printf("%d ", heap->data[i]);
}
printf("\n");
}
int main() {
MaxHeap heap;
initHeap(&heap);
printf("=== 插入元素 ===\n");
int values[] = {10, 20, 15, 30, 40, 25, 5};
for (int i = 0; i < 7; i++) {
push(&heap, values[i]);
printf("插入 %d 后,堆顶: %d\n", values[i], top(&heap));
}
printHeap(&heap);
printf("堆大小: %d\n", size(&heap));
printf("\n=== 弹出元素 ===\n");
while (!isEmpty(&heap)) {
int val = pop(&heap);
printf("弹出: %d, 剩余堆大小: %d\n", val, size(&heap));
}
destroyHeap(&heap);
return 0;
}
运行结果:
text
=== 插入元素 ===
插入 10 后,堆顶: 10
插入 20 后,堆顶: 20
插入 15 后,堆顶: 20
插入 30 后,堆顶: 30
插入 40 后,堆顶: 40
插入 25 后,堆顶: 40
插入 5 后,堆顶: 40
堆内容(数组形式): 40 30 25 10 20 15 5
堆大小: 7
=== 弹出元素 ===
弹出: 40, 剩余堆大小: 6
弹出: 30, 剩余堆大小: 5
弹出: 25, 剩余堆大小: 4
弹出: 20, 剩余堆大小: 3
弹出: 15, 剩余堆大小: 2
弹出: 10, 剩余堆大小: 1
弹出: 5, 剩余堆大小: 0
四、堆排序(基于堆实现)
c
void heapSort(int arr[], int n) {
MaxHeap heap;
initHeap(&heap);
// 建堆
for (int i = 0; i < n; i++) {
push(&heap, arr[i]);
}
// 从大到小输出(大根堆弹出最大值)
for (int i = n - 1; i >= 0; i--) {
arr[i] = pop(&heap);
}
destroyHeap(&heap);
}
int main() {
int arr[] = {3, 5, 1, 8, 2, 9, 4, 7, 6};
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");
return 0;
}
运行结果:
text
原数组: 3 5 1 8 2 9 4 7 6
排序后: 1 2 3 4 5 6 7 8 9
五、小根堆的实现
将大根堆的 shiftUp 和 shiftDown 中的比较符号反转即可。
c
// 小根堆的上浮
void shiftUpMin(MaxHeap *heap, int index) {
while (index > 0) {
int parent = (index - 1) / 2;
if (heap->data[parent] <= heap->data[index]) { // 父 ≤ 子则停止
break;
}
swap(&heap->data[parent], &heap->data[index]);
index = parent;
}
}
// 小根堆的下沉
void shiftDownMin(MaxHeap *heap, int index) {
while (index * 2 + 1 < heap->size) {
int left = index * 2 + 1;
int right = index * 2 + 2;
int smallest = left;
if (right < heap->size && heap->data[right] < heap->data[left]) {
smallest = right;
}
if (heap->data[index] <= heap->data[smallest]) {
break;
}
swap(&heap->data[index], &heap->data[smallest]);
index = smallest;
}
}
六、复杂度分析
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 插入(push) | O(log n) | 上浮,最多树高次 |
| 删除最大值(pop) | O(log n) | 下沉,最多树高次 |
| 取堆顶(top) | O(1) | 直接返回数组[0] |
| 建堆(heapify) | O(n) | 从最后一个非叶子节点下沉 |
七、堆的应用:Top K 问题
找数组中最大的 K 个元素,用小根堆实现 O(n log K)。
c
int* findTopK(int arr[], int n, int k) {
if (k <= 0 || k > n) return NULL;
MaxHeap minHeap; // 实际用小根堆
// 注意:这里用大根堆取负值模拟小根堆,或单独实现小根堆
// 简化版:先插入k个元素
// 然后遍历剩余元素,比堆顶大则替换
// 完整实现略,原理相同
}
八、小结
这一篇我们实现了基于动态数组的大根堆:
| 操作 | 函数 | 核心算法 |
|---|---|---|
| 插入 | push | 上浮(shiftUp) |
| 删除最大值 | pop | 下沉(shiftDown) |
| 取堆顶 | top | O(1) |
关键点:
-
堆是完全二叉树,用数组存储
-
大根堆:父 ≥ 子,堆顶最大
-
上浮:新元素在末尾,向上比较交换
-
下沉:堆顶元素被删除,末尾元素补上,向下比较交换
注意区分:
-
数据结构堆(本文):树形结构,优先队列
-
内存堆区:malloc 管理的内存区域
下一篇我们讲跳跃表(Skip List)。
九、思考题
-
为什么堆用数组存储而不需要显式的左右指针?
-
建堆时,为什么从最后一个非叶子节点开始向下调整,而不是从根开始?
-
如何用堆实现一个优先队列,支持按优先级取出元素?
-
堆排序的空间复杂度是 O(1),本文的堆排序用了 O(n) 额外空间,如何优化?
欢迎在评论区讨论你的答案。