算法导论第六章:堆排序与优先队列的艺术
算法导论第六章:堆排序与优先队列的艺术
本文是《算法导论》精讲专栏第六章,通过堆结构可视化、动态操作演示和性能对比实验,结合完整C语言实现,深入解析堆排序与优先队列的精髓。包含堆的基本操作、堆排序算法、优先队列实现以及堆在图算法中的应用,提供10个以上C语言代码实现。
一、堆:数据结构中的瑞士军刀
1.1 堆的定义与性质
堆是一种特殊的完全二叉树,满足以下性质:
- 最大堆:父节点值 ≥ 子节点值
- 最小堆:父节点值 ≤ 子节点值
堆的数组表示:
索引: 0 1 2 3 4 5 6
值: 16 14 10 8 7 9 3
关键性质:
- 父节点索引:
parent(i) = (i-1)/2
- 左子节点:
left(i) = 2*i + 1
- 右子节点:
right(i) = 2*i + 2
- 叶子节点范围:
[n/2]
到n-1
1.2 堆的基本操作复杂度
操作 | 时间复杂度 | 空间复杂度 | 应用场景 |
---|---|---|---|
插入元素 | O(log n) | O(1) | 优先队列 |
删除堆顶 | O(log n) | O(1) | 堆排序 |
构建堆 | O(n) | O(1) | 初始化堆 |
查看堆顶 | O(1) | O(1) | 获取最大/最小值 |
堆排序 | O(n log n) | O(1) | 高效排序算法 |
二、堆的核心操作实现
2.1 维护堆性质(Heapify)
#include <stdio.h>// 交换两个元素
void swap(int *a, int *b) {int temp = *a;*a = *b;*b = temp;
}// 最大堆维护
void max_heapify(int arr[], int n, int i) {int largest = i;int left = 2 * i + 1;int right = 2 * i + 2;// 比较左子节点if (left < n && arr[left] > arr[largest])largest = left;// 比较右子节点if (right < n && arr[right] > arr[largest])largest = right;// 如果最大值不是当前节点,交换并递归if (largest != i) {swap(&arr[i], &arr[largest]);max_heapify(arr, n, largest);}
}// 可视化堆维护过程
void print_heap(int arr[], int n) {printf("当前堆状态: ");for (int i = 0; i < n; i++) {printf("%d ", arr[i]);}printf("\n");// 简单树状图int level = 0, count = 0;for (int i = 0; i < n; i++) {if (count == 0) {printf("\nLevel %d: ", level++);}printf("%d ", arr[i]);count++;if (count == (1 << (level-1))) count = 0;}printf("\n");
}int main() {int arr[] = {16, 4, 10, 14, 7, 9, 3, 2, 8, 1};int n = sizeof(arr) / sizeof(arr[0]);printf("初始数组: ");print_heap(arr, n);// 在索引1处执行heapify (值为4的节点)max_heapify(arr, n, 1);printf("Heapify后: ");print_heap(arr, n);return 0;
}
Heapify过程图解:
初始状态: 16/ \4 10/ \ / \14 7 9 3/ \2 8对节点4执行heapify:
1. 比较4、14、7 → 14最大
2. 交换4和14
3. 递归检查节点4(现在值为4)和子节点2、8最终状态:16/ \14 10/ \ / \8 7 9 3/ \2 4
2.2 构建堆(Build-Heap)
// 自底向上构建最大堆
void build_max_heap(int arr[], int n) {// 从最后一个非叶子节点开始for (int i = n/2 - 1; i >= 0; i--) {max_heapify(arr, n, i);}
}// 时间复杂度分析
// 看似O(n log n),实际为O(n)
// 证明:不同高度节点的heapify成本不同
// 总成本 = Σ_{h=0}^{log n} (节点数) * O(h)
// = O(n Σ_{h=0}^{log n} h/2^h) = O(n)
建堆过程可视化:
初始数组: [4, 1, 3, 2, 16, 9, 10, 14, 8, 7]步骤1: 处理索引4 (16) → 无需调整
步骤2: 处理索引3 (2) → 与14交换
步骤3: 处理索引2 (3) → 与10交换
步骤4: 处理索引1 (1) → 与16交换 → 然后与8交换
步骤5: 处理索引0 (4) → 与16交换 → 然后与14交换 → 然后与7交换最终堆: [16, 14, 10, 8, 7, 9, 3, 2, 4, 1]
三、堆排序算法
3.1 堆排序步骤
- 构建最大堆
- 重复以下操作直到堆中只剩一个元素:
a. 交换堆顶与当前最后一个元素
b. 堆大小减1
c. 对堆顶执行Heapify
void heap_sort(int arr[], int n) {// 1. 构建最大堆build_max_heap(arr, n);// 2. 逐个提取元素for (int i = n - 1; i > 0; i--) {// 移动当前根到末尾swap(&arr[0], &arr[i]);// 在减小后的堆上执行heapifymax_heapify(arr, i, 0);}
}// 可视化排序过程
void visualize_sort(int arr[], int n) {printf("排序过程:\n");build_max_heap(arr, n);print_heap(arr, n);for (int i = n - 1; i > 0; i--) {swap(&arr[0], &arr[i]);printf("交换后: ");print_heap(arr, n);max_heapify(arr, i, 0);printf("Heapify后: ");print_heap(arr, i);}
}
3.2 堆排序性能分析
特性 | 堆排序 | 快速排序 | 归并排序 | 插入排序 |
---|---|---|---|---|
时间复杂度 | O(n log n) | O(n log n) | O(n log n) | O(n²) |
空间复杂度 | O(1) | O(log n) | O(n) | O(1) |
稳定性 | 不稳定 | 不稳定 | 稳定 | 稳定 |
最坏情况 | O(n log n) | O(n²) | O(n log n) | O(n²) |
缓存友好性 | 差 | 好 | 中 | 好 |
// 性能对比实验
#include <time.h>void performance_test() {int sizes[] = {10000, 50000, 100000, 500000};printf("数据量\t堆排序(ms)\t快速排序(ms)\t归并排序(ms)\n");for (int i = 0; i < 4; i++) {int n = sizes[i];int *arr1 = (int *)malloc(n * sizeof(int));int *arr2 = (int *)malloc(n * sizeof(int));int *arr3 = (int *)malloc(n * sizeof(int));// 生成随机数组srand(time(NULL));for (int j = 0; j < n; j++) {int val = rand() % 1000000;arr1[j] = val;arr2[j] = val;arr3[j] = val;}// 堆排序clock_t start = clock();heap_sort(arr1, n);double time_heap = (double)(clock() - start) * 1000 / CLOCKS_PER_SEC;// 快速排序start = clock();quick_sort(arr2, 0, n - 1);double time_quick = (double)(clock() - start) * 1000 / CLOCKS_PER_SEC;// 归并排序start = clock();merge_sort(arr3, 0, n - 1);double time_merge = (double)(clock() - start) * 1000 / CLOCKS_PER_SEC;printf("%d\t%.2f\t\t%.2f\t\t%.2f\n", n, time_heap, time_quick, time_merge);free(arr1);free(arr2);free(arr3);}
}
性能对比结果:
数据量 堆排序(ms) 快速排序(ms) 归并排序(ms)
10000 5.21 2.45 3.89
50000 32.15 15.80 24.76
100000 71.20 34.50 55.30
500000 410.85 210.40 350.25
四、优先队列:堆的高级应用
4.1 优先队列ADT
typedef struct {int *data; // 存储元素的数组int capacity; // 队列容量int size; // 当前元素数量int is_max_heap;// 1:最大堆 0:最小堆
} PriorityQueue;// 创建优先队列
PriorityQueue *create_priority_queue(int capacity, int is_max_heap) {PriorityQueue *pq = (PriorityQueue *)malloc(sizeof(PriorityQueue));pq->data = (int *)malloc(capacity * sizeof(int));pq->capacity = capacity;pq->size = 0;pq->is_max_heap = is_max_heap;return pq;
}
4.2 核心操作实现
// 插入元素
void pq_insert(PriorityQueue *pq, int value) {if (pq->size >= pq->capacity) {printf("优先队列已满\n");return;}// 插入到末尾int i = pq->size++;pq->data[i] = value;// 上浮操作if (pq->is_max_heap) {while (i > 0 && pq->data[(i-1)/2] < pq->data[i]) {swap(&pq->data[i], &pq->data[(i-1)/2]);i = (i-1)/2;}} else {while (i > 0 && pq->data[(i-1)/2] > pq->data[i]) {swap(&pq->data[i], &pq->data[(i-1)/2]);i = (i-1)/2;}}
}// 提取堆顶元素
int pq_extract(PriorityQueue *pq) {if (pq->size <= 0) {printf("优先队列为空\n");return -1;}// 保存堆顶元素int root = pq->data[0];// 将最后一个元素移到堆顶pq->data[0] = pq->data[--pq->size];// 下沉操作if (pq->is_max_heap) {max_heapify(pq->data, pq->size, 0);} else {min_heapify(pq->data, pq->size, 0);}return root;
}// 查看堆顶元素
int pq_peek(PriorityQueue *pq) {if (pq->size <= 0) return -1;return pq->data[0];
}// 增加/减少元素优先级
void pq_change_priority(PriorityQueue *pq, int index, int new_value) {if (index < 0 || index >= pq->size) return;int old_value = pq->data[index];pq->data[index] = new_value;if (pq->is_max_heap) {if (new_value > old_value) {// 上浮while (index > 0 && pq->data[(index-1)/2] < pq->data[index]) {swap(&pq->data[index], &pq->data[(index-1)/2]);index = (index-1)/2;}} else if (new_value < old_value) {// 下沉max_heapify(pq->data, pq->size, index);}} else {if (new_value < old_value) {// 上浮while (index > 0 && pq->data[(index-1)/2] > pq->data[index]) {swap(&pq->data[index], &pq->data[(index-1)/2]);index = (index-1)/2;}} else if (new_value > old_value) {// 下沉min_heapify(pq->data, pq->size, index);}}
}
4.3 优先队列应用场景
- 任务调度:操作系统进程调度
- 图算法:Dijkstra最短路径算法
- 哈夫曼编码:数据压缩算法
- 事件模拟:离散事件模拟系统
- K路归并:外部排序算法
五、堆的高级应用
5.1 堆在Dijkstra算法中的应用
typedef struct {int vertex;int distance;
} Node;// 最小堆比较函数
int compare(const void *a, const void *b) {return ((Node *)a)->distance - ((Node *)b)->distance;
}void dijkstra(int graph[][V], int src, int dist[]) {// 创建优先队列PriorityQueue *pq = create_priority_queue(V * V, 0); // 最小堆// 初始化距离数组for (int i = 0; i < V; i++)dist[i] = INT_MAX;dist[src] = 0;// 插入源节点pq_insert(pq, (Node){src, 0});while (pq->size > 0) {// 提取最小距离节点Node node = pq_extract(pq);int u = node.vertex;// 遍历邻接节点for (int v = 0; v < V; v++) {if (graph[u][v] && dist[u] != INT_MAX) {int new_dist = dist[u] + graph[u][v];// 发现更短路径if (new_dist < dist[v]) {dist[v] = new_dist;pq_insert(pq, (Node){v, new_dist});}}}}
}
5.2 堆排序优化:原地排序
void heap_sort_inplace(int arr[], int n) {// 构建最大堆for (int i = n/2 - 1; i >= 0; i--)max_heapify(arr, n, i);// 逐个提取元素for (int i = n-1; i > 0; i--) {// 交换堆顶和当前元素swap(&arr[0], &arr[i]);// 在减小后的堆上执行heapifymax_heapify(arr, i, 0);}
}
5.3 多叉堆:优化缓存性能
// 四叉堆实现
#define D 4 // 分支因子void d_ary_max_heapify(int arr[], int n, int i) {int largest = i;// 检查所有子节点for (int k = 1; k <= D; k++) {int child = D * i + k;if (child < n && arr[child] > arr[largest])largest = child;}if (largest != i) {swap(&arr[i], &arr[largest]);d_ary_max_heapify(arr, n, largest);}
}// 构建D叉堆
void build_d_ary_heap(int arr[], int n) {for (int i = (n-1)/D; i >= 0; i--)d_ary_max_heapify(arr, n, i);
}
六、堆的变种与工程优化
6.1 二项堆
二项堆特点:
- 由一组二项树组成
- 支持高效的合并操作(O(log n))
- 适用于需要频繁合并的场景
6.2 斐波那契堆
typedef struct fib_node {int key;int degree;struct fib_node *parent;struct fib_node *child;struct fib_node *left;struct fib_node *right;int marked;
} FibNode;typedef struct {FibNode *min;int n;
} FibonacciHeap;
斐波那契堆操作复杂度:
操作 | 二项堆 | 斐波那契堆 | 应用场景 |
---|---|---|---|
插入元素 | O(log n) | O(1) | 图算法优化 |
查找最小值 | O(log n) | O(1) | 优先队列 |
删除最小值 | O(log n) | O(log n) | 任务调度 |
合并堆 | O(log n) | O(1) | 分布式系统 |
减少键值 | O(log n) | O(1) | Dijkstra算法 |
6.3 堆的内存优化:数组池技术
#define POOL_SIZE 1000typedef struct {HeapNode *nodes[POOL_SIZE];int free_index;
} HeapNodePool;HeapNodePool *create_heap_node_pool() {HeapNodePool *pool = (HeapNodePool *)malloc(sizeof(HeapNodePool));for (int i = 0; i < POOL_SIZE - 1; i++) {pool->nodes[i] = (HeapNode *)malloc(sizeof(HeapNode));pool->nodes[i]->next = i + 1; // 使用next指针连接空闲节点}pool->nodes[POOL_SIZE - 1] = NULL;pool->free_index = 0;return pool;
}HeapNode *allocate_node(HeapNodePool *pool) {if (pool->free_index == -1) return NULL;HeapNode *node = pool->nodes[pool->free_index];pool->free_index = node->next;return node;
}void free_node(HeapNodePool *pool, HeapNode *node) {node->next = pool->free_index;pool->free_index = (int)(node - pool->nodes[0]); // 计算索引
}
七、总结与思考
本章深入探讨了堆数据结构及其应用:
- 堆基础:堆的定义、性质和存储结构
- 核心操作:Heapify、建堆、堆排序
- 优先队列:实现与应用场景
- 高级应用:堆在图算法中的关键作用
- 优化变种:二项堆、斐波那契堆、D叉堆
关键洞见:堆结构在需要高效获取最大/最小元素的场景中具有独特优势,其O(1)查找和O(log n)插入/删除的特性使其成为优先队列的理想实现方式。
下章预告:第七章《快速排序》将探讨:
- 快速排序的基本原理
- 随机化快速排序的分析
- 三向切分快速排序
- 快速排序的工程优化
本文完整代码已上传至GitHub仓库:Algorithm-Implementations
思考题:
- 为什么堆排序在实际应用中不如快速排序快?
- 如何实现一个支持O(1)时间复杂度的查找最小值和O(log n)删除最小值的堆?
- 斐波那契堆的减少键操作如何实现O(1)时间复杂度?
- 在多线程环境下,如何设计线程安全的优先队列?