在堆排序中核心是理解堆排序的调整过程,就是对局部的三个结点进行比较,一个根节点与两个孩子节点进行比较,如果某个孩子更大,把孩子换上去,双亲结点换下来。从最后一个非叶子的双亲结点进行调整(就是有孩子的双亲中编号最大的)从它开始进行递减调整到根节点,每次把局部最大的孩子结点调到双亲位置,如果孩子没有比双亲大就不管,一直调整到根节点位置,此时根节点就是最大的,然后我们把它与最后一个叶子结点进行交换,最后一个位置就是最大的了,交换完树就不满足大根堆了,我们继续调整前面的结点重新构造大根堆,同样从底部最后一个非叶子的双亲结点开始调整,刚才换下来的这个节点就不需要再进行调整了,包括已经换好的节点也不需要参与调整。
注意1:我们一旦从大根堆找到最大值,那么位置也就最终确定了,这个最大值就退出大根堆的调整,每次调整的结点不包含已经调整好的结点
注意2:每一轮都会从大根堆中找到一个最大值
注意3:我们的数组存放排序数据的下标从1-length,第一个位置0用来临时存储局部双亲节点的值用于与比自己大的孩子交换。在前面介绍的希尔排序中也用0这个位置存储临时的值,其他正常参与排序的数据存在1-length位置。当然你可以自己申请一个用临时变量存储来进行交换操作,数组中 0 位置正常存放排序的数据
为了实现的方便我们用数组实现,也就是二叉树的顺序存储,同时父亲下标为i时,左孩子下标为2*i,右孩子下标2*i+1,如果用二叉链表会比较麻烦,因为要找父亲结点用于交换,单向二叉链表完成不了,需要再加入一个父亲指针域。
下面为具体实现代码:
#include <stdio.h>
// 调整堆的函数,使以 k 为根的子树满足最大堆的性质
void HeadAdjust(int a[], int k, int len) {
// 临时保存当前节点的值
a[0] = a[k];
// 从 k 节点的左子节点开始向下调整堆
for (int i = 2 * k; i <= len; i *= 2) {
// 如果右子节点存在且比左子节点大,则选择右子节点
if (i < len && a[i] < a[i + 1]) {
i++;
}
// 如果当前节点已经比子节点大,不需要继续调整
if (a[0] >= a[i]) {
break;
}
else {
// 将子节点的值上移
a[k] = a[i];
k = i; // 继续向下调整
}
}
// 将保存的节点值放到最终位置
a[k] = a[0];
}
// 构建最大堆的函数
void BuildMaxHeap(int a[], int len) {
// 从最后一个非叶子节点开始调整堆
for (int i = len / 2; i > 0; i--) {
HeadAdjust(a, i, len);
}
}
// 交换两个整数的函数
void Swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
// 堆排序的函数
void HeapSort(int a[], int len) {
// 首先构建最大堆
BuildMaxHeap(a, len);
// 从最后一个元素开始,将最大元素交换到末尾,并重新调整堆
for (int i = len; i > 1; i--) {
Swap(&a[i], &a[1]); // 交换根节点与当前堆的最后一个节点
// 重新调整堆,注意调整的长度是 i-1,因为已排序的部分在末尾
HeadAdjust(a, 1, i - 1);
}
}
int main() {
// 初始化数组,注意第一个元素是0,用于保持1-based索引
int a[8] = { 0, 5, 3, 7, 8, 9, 1, 6 };
// 对数组进行堆排序
HeapSort(a, 7);
// 打印排序后的数组,注意每个元素之间用箭头分隔
for (int i = 1; i <= 7; i++) {
printf("%d", a[i]);
if (i < 7) {
printf("->");
}
}
printf("\n"); // 打印换行符
return 0;
}
运行结果: