算法入门排序算法:堆排序
一、什么是堆排序?
堆排序(Heap Sort)是一种基于二叉堆数据结构的高效排序算法,由J. W. J. Williams于1964年发明。它结合了插入排序和归并排序的优点:像插入排序一样是原地排序,像归并排序一样具有O(n log n)的时间复杂度。
堆排序的核心数据结构是堆——一种特殊的完全二叉树,它满足堆属性:每个节点的值都大于或等于(最大堆)或小于或等于(最小堆)其子节点的值。
二、堆排序的工作原理
堆排序的过程可以分为两个主要阶段:
- 构建堆(Heapify):将无序数组构建成一个最大堆(或最小堆)
- 排序:反复将堆顶元素(最大或最小元素)与堆末尾元素交换,然后重新调整堆
这个过程就像是在筛选石子:先把所有石子堆成一个大堆(最大的石头在顶部),然后每次取走顶部的最大石头,重新调整堆,直到所有石头都按顺序取完。
三、堆排序的Java实现
下面是堆排序的完整Java实现,包含详细注释:
import java.util.Arrays;
public class HeapSort {
// 堆排序主方法
public static void heapSort(int[] array) {
int n = array.length;
System.out.println("原始数组: " + Arrays.toString(array));
// 1. 构建最大堆
System.out.println("开始构建最大堆...");
buildMaxHeap(array);
System.out.println("最大堆构建完成: " + Arrays.toString(array));
// 2. 逐个提取元素
System.out.println("开始排序过程...");
for (int i = n - 1; i > 0; i--) {
// 将当前堆顶(最大值)与末尾元素交换
swap(array, 0, i);
System.out.println("交换堆顶与位置" + i + "后: " + Arrays.toString(array));
// 调整剩余元素,使其保持堆属性
maxHeapify(array, i, 0);
System.out.println("调整堆后: " + Arrays.toString(array));
}
}
// 构建最大堆
private static void buildMaxHeap(int[] array) {
int n = array.length;
// 从最后一个非叶子节点开始,向上调整
// 最后一个非叶子节点的索引 = (n/2) - 1
for (int i = n / 2 - 1; i >= 0; i--) {
maxHeapify(array, n, i);
}
}
// 调整堆,使其满足最大堆属性
private static void maxHeapify(int[] array, int heapSize, int rootIndex) {
int largest = rootIndex; // 假设根节点是最大的
int leftChild = 2 * rootIndex + 1; // 左子节点索引
int rightChild = 2 * rootIndex + 2; // 右子节点索引
// 如果左子节点存在且大于根节点
if (leftChild < heapSize && array[leftChild] > array[largest]) {
largest = leftChild;
}
// 如果右子节点存在且大于当前最大值
if (rightChild < heapSize && array[rightChild] > array[largest]) {
largest = rightChild;
}
// 如果最大值不是根节点,需要交换并继续调整
if (largest != rootIndex) {
swap(array, rootIndex, largest);
// 递归调整受影响的子树
maxHeapify(array, heapSize, largest);
}
}
// 交换数组中的两个元素
private static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
// 可视化堆结构(辅助理解)
public static void printHeap(int[] array, int heapSize) {
System.out.println("堆结构可视化:");
int levels = (int) (Math.log(heapSize) / Math.log(2)) + 1;
int currentIndex = 0;
for (int level = 0; level < levels; level++) {
int elementsInLevel = (int) Math.pow(2, level);
int padding = (int) Math.pow(2, levels - level) - 1;
// 打印前导空格
for (int i = 0; i < padding; i++) {
System.out.print(" ");
}
// 打印当前层的元素
for (int i = 0; i < elementsInLevel && currentIndex < heapSize; i++) {
System.out.print(array[currentIndex++]);
for (int j = 0; j < padding * 2 + 1; j++) {
System.out.print(" ");
}
}
System.out.println();
}
}
public static void main(String[] args) {
int[] data = {4, 10, 3, 5, 1, 8, 7, 2, 9, 6};
System.out.println("=== 堆排序演示 ===");
heapSort(data);
System.out.println("最终排序结果: " + Arrays.toString(data));
// 额外演示:最小堆排序
System.out.println("\n=== 最小堆排序演示 ===");
int[] data2 = {4, 10, 3, 5, 1, 8, 7, 2, 9, 6};
heapSortMin(data2);
System.out.println("最小堆排序结果: " + Arrays.toString(data2));
}
// 最小堆排序版本
public static void heapSortMin(int[] array) {
int n = array.length;
// 构建最小堆
buildMinHeap(array);
// 逐个提取元素
for (int i = n - 1; i > 0; i--) {
swap(array, 0, i);
minHeapify(array, i, 0);
}
}
// 构建最小堆
private static void buildMinHeap(int[] array) {
int n = array.length;
for (int i = n / 2 - 1; i >= 0; i--) {
minHeapify(array, n, i);
}
}
// 调整最小堆
private static void minHeapify(int[] array, int heapSize, int rootIndex) {
int smallest = rootIndex;
int leftChild = 2 * rootIndex + 1;
int rightChild = 2 * rootIndex + 2;
if (leftChild < heapSize && array[leftChild] < array[smallest]) {
smallest = leftChild;
}
if (rightChild < heapSize && array[rightChild] < array[smallest]) {
smallest = rightChild;
}
if (smallest != rootIndex) {
swap(array, rootIndex, smallest);
minHeapify(array, heapSize, smallest);
}
}
}
代码解析:
-
堆构建:
buildMaxHeap():从最后一个非叶子节点开始,自底向上构建最大堆maxHeapify():调整指定节点及其子树,使其满足堆属性
-
排序过程:
- 将堆顶元素(最大值)与末尾元素交换
- 堆大小减1,重新调整堆
- 重复直到堆中只剩一个元素
-
辅助方法:
swap():交换数组元素printHeap():可视化堆结构,帮助理解- 最小堆版本:演示降序排序
四、堆排序的性能分析
时间复杂度:
- 构建堆:O(n) —— 看似是O(n log n),但通过数学分析可知是O(n)
- 每次调整堆:O(log n)
- 总时间复杂度:O(n log n)
空间复杂度:
堆排序是原地排序算法,只需要常数级别的额外空间(O(1)),用于临时存储交换的变量。
稳定性:
堆排序是不稳定的排序算法,因为堆调整过程中的交换可能改变相等元素的相对顺序。
五、堆排序的优缺点
优点:
- 时间复杂度稳定为O(n log n),没有最坏情况
- 原地排序,空间效率高
- 适用于大规模数据排序
- 可以高效实现优先级队列
缺点:
- 不稳定排序
- 缓存不友好(跳跃式访问模式)
- 常数因子较大,实际运行可能不如快速排序快
- 实现相对复杂
六、堆排序的实际应用
堆排序在以下场景中特别有用:
- 优先级队列:堆是优先级队列的理想数据结构
- 实时系统:需要保证最坏情况下性能的场景
- 外部排序:处理无法全部装入内存的大数据集
- 游戏开发:需要高效处理动态优先级的情况
- 操作系统:进程调度、内存管理等
七、堆排序的变体和优化
- 二叉堆:最基础的堆实现,使用数组存储
- 二项堆:支持高效合并操作的堆变体
- 斐波那契堆:理论上最优的堆结构,但实现复杂
- d-堆:每个节点有d个子节点,适合外部存储
- 左倾堆:支持高效合并的另一种堆结构
八、堆的数学性质
堆作为完全二叉树,具有以下重要性质:
- 高度:⌊log₂n⌋
- 非叶子节点范围:0 到 ⌊n/2⌋ - 1
- 节点i的子节点:2i+1 和 2i+2
- 节点i的父节点:⌊(i-1)/2⌋
这些性质使得我们可以用数组高效地表示堆,而不需要显式的指针结构。
九、总结
堆排序以其稳定的O(n log n)时间复杂度和原地排序的特性,在算法世界中占有重要地位。虽然在实际应用中可能不如快速排序快,但它的最坏情况保证使其在需要可靠性的场景中不可替代。
理解堆排序不仅有助于掌握一种高效的排序算法,还能深入理解堆这一重要数据结构。堆作为优先级队列的基础,在操作系统、数据库、网络调度等众多领域都有广泛应用。
正如计算机科学家Robert Sedgewick所说:"堆排序是唯一能够同时保证O(n log n)时间和O(1)空间的比较排序算法。"这种独特的性能特征使堆排序在理论研究和实际应用中都具有重要价值。
掌握堆排序,意味着你不仅学会了一种排序算法,更理解了一种重要的数据结构设计和分析方法。这是每个计算机科学学习者的必修课。

浙公网安备 33010602011771号