3.1 堆排序:利用堆数据结构的排序

历史背景

堆排序由 J. W. J. Williams 于 1964 年发明。Williams 在同年还提出了堆(Heap)这种数据结构。堆排序是第一个能够在 O(n log n) 时间内完成排序且只需要常数额外空间的算法。它同时也是优先队列(Priority Queue)数据结构的经典应用。1978 年,Robert W. Floyd 对堆排序进行了改进,使其常数因子更小。

算法原理

堆排序利用最大堆(或最小堆)的性质:堆顶元素始终是当前堆中的最大(小)元素。算法分为两个阶段:

  1. 建堆阶段:将输入数组重新组织为一个最大堆。从最后一个非叶子节点开始,自底向上进行"下沉"(sift down)操作。
  2. 排序阶段:反复将堆顶元素(最大值)与堆的最后一个元素交换,然后将堆的大小减一,并对新的堆顶元素执行"下沉"操作以恢复堆的性质。

Java 实现

java
复制代码
public class HeapSort {
    
    /**
     * 堆排序
     * @param arr 待排序的整数数组
     */
    public static void sort(int[] arr) {
        int n = arr.length;
        
        // 第一阶段:建堆
        // 从最后一个非叶子节点开始,自底向上构建最大堆
        for (int i = n / 2 - 1; i >= 0; i--) {
            siftDown(arr, n, i);
        }
        
        // 第二阶段:排序
        // 反复将堆顶元素与堆末尾元素交换,然后缩小堆
        for (int i = n - 1; i > 0; i--) {
            // 将当前最大值(堆顶)交换到数组末尾
            int temp = arr[0];
            arr[0] = arr[i];
            arr[i] = temp;
            
            // 对剩余的堆进行调整
            siftDown(arr, i, 0);
        }
    }
    
    /**
     * 下沉操作:维护最大堆的性质
     * @param arr 数组
     * @param heapSize 当前堆的大小
     * @param root 需要调整的根节点索引
     */
    private static void siftDown(int[] arr, int heapSize, int root) {
        int largest = root;
        int leftChild = 2 * root + 1;
        int rightChild = 2 * root + 2;
        
        // 找出根节点、左子节点、右子节点中的最大值
        if (leftChild < heapSize && arr[leftChild] > arr[largest]) {
            largest = leftChild;
        }
        if (rightChild < heapSize && arr[rightChild] > arr[largest]) {
            largest = rightChild;
        }
        
        // 如果最大值不是根节点,交换并递归调整
        if (largest != root) {
            int temp = arr[root];
            arr[root] = arr[largest];
            arr[largest] = temp;
            siftDown(arr, heapSize, largest);
        }
    }
    
    /**
     * 迭代版本的下沉操作(避免递归开销)
     */
    private static void siftDownIterative(int[] arr, int heapSize, int root) {
        int current = root;
        while (true) {
            int largest = current;
            int leftChild = 2 * current + 1;
            int rightChild = 2 * current + 2;
            
            if (leftChild < heapSize && arr[leftChild] > arr[largest]) {
                largest = leftChild;
            }
            if (rightChild < heapSize && arr[rightChild] > arr[largest]) {
                largest = rightChild;
            }
            
            if (largest == current) {
                break;
            }
            
            int temp = arr[current];
            arr[current] = arr[largest];
            arr[largest] = temp;
            current = largest;
        }
    }
    
    /**
     * 堆排序(泛型版本)
     */
    public static <T extends Comparable<T>> void sortGeneric(T[] arr) {
        int n = arr.length;
        
        for (int i = n / 2 - 1; i >= 0; i--) {
            siftDownGeneric(arr, n, i);
        }
        
        for (int i = n - 1; i > 0; i--) {
            T temp = arr[0];
            arr[0] = arr[i];
            arr[i] = temp;
            siftDownGeneric(arr, i, 0);
        }
    }
    
    private static <T extends Comparable<T>> void siftDownGeneric(T[] arr, int heapSize, int root) {
        int current = root;
        while (true) {
            int largest = current;
            int leftChild = 2 * current + 1;
            int rightChild = 2 * current + 2;
            
            if (leftChild < heapSize && arr[leftChild].compareTo(arr[largest]) > 0) {
                largest = leftChild;
            }
            if (rightChild < heapSize && arr[rightChild].compareTo(arr[largest]) > 0) {
                largest = rightChild;
            }
            
            if (largest == current) {
                break;
            }
            
            T temp = arr[current];
            arr[current] = arr[largest];
            arr[largest] = temp;
            current = largest;
        }
    }
}

效率分析

指标 最好情况 最坏情况 平均情况
时间复杂度 O(n log n) O(n log n) O(n log n)
空间复杂度 O(1)(迭代版) O(1) O(1)
比较次数 ≈ n log n ≈ 2n log n ≈ 2n log n
交换次数 ≈ n log n ≈ n log n ≈ n log n

堆排序是不稳定的排序算法,但是原地排序。它的最大优势是时间复杂度始终为 O(n log n),且不需要额外空间。缺点是缓存不友好(堆的父子节点在内存中不相邻),实际运行速度通常慢于快速排序。

应用场景

  • 需要保证最坏情况 O(n log n) 的场景
  • 内存受限环境(如嵌入式系统)
  • 实现优先队列
  • 操作系统任务调度
  • 求 Top-K 问题(不需要完整排序,只需要最大的 K 个元素)