堆排序(Heap Sort)

堆排序的核心不是“排序”本身,而是反复利用堆顶一定是当前最大值这个性质,把最大元素一个个放到数组尾部。

定义

堆排序是一种基于堆数据结构的排序算法。它利用堆这种数据结构的特性:堆顶元素总是最大(最大堆)或最小(最小堆)。

核心思路

  1. 建堆:将无序数组构建成最大堆(或最小堆)
  2. 排序:
    • 将堆顶元素(最大值)与末尾元素交换
    • 堆的大小减1
    • 对新堆顶进行下沉操作,恢复堆性质
    • 重复以上步骤,直到堆的大小为1
原数组: [4, 10, 3, 5, 1]

建堆:   [10, 5, 3, 4, 1]  (最大堆)
           10
          /  \
         5    3
        / \
       4   1

排序过程:
交换10和1: [1, 5, 3, 4] + [10]
调整堆:    [5, 4, 3, 1] + [10]
交换5和1:  [1, 4, 3] + [5, 10]
调整堆:    [4, 1, 3] + [5, 10]
...
最终: [1, 3, 4, 5, 10]

为什么它能稳定在 O(n log n)

可以把过程拆成两段:

  1. 建堆:O(n)
  2. 每次把堆顶换到末尾,再下沉恢复堆:共做 n-1 次,每次 O(log n)

所以总复杂度稳定落在 O(n log n),不会像快排那样退化到 O(n^2)。

复杂度分析

指标复杂度说明
最好时间O(n log n)建堆O(n) + n次调整O(log n)
平均时间O(n log n)同上
最坏时间O(n log n)性能稳定
空间复杂度O(1)原地排序
稳定性❌ 不稳定交换操作破坏相对顺序

Go 代码

Go 实现

package main
 
import "fmt"
 
func heapify(arr []int, n, i int) {
    largest := i
    left := 2*i + 1
    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 {
        arr[i], arr[largest] = arr[largest], arr[i]
        heapify(arr, n, largest)
    }
}
 
func heapSort(arr []int) {
    n := len(arr)
 
    // 建堆
    for i := n/2 - 1; i >= 0; i-- {
        heapify(arr, n, i)
    }
 
    // 排序
    for i := n - 1; i > 0; i-- {
        arr[0], arr[i] = arr[i], arr[0]
        heapify(arr, i, 0)
    }
}
 
func main() {
    arr := []int{12, 11, 13, 5, 6, 7}
 
    fmt.Println("原始数组:", arr)
    heapSort(arr)
    fmt.Println("排序后:", arr)
}

思路展开

2. 堆的数组表示

# 对于索引i的节点(从0开始):
parent(i) = (i - 1) // 2      # 父节点
left_child(i) = 2 * i + 1     # 左子节点
right_child(i) = 2 * i + 2    # 右子节点

易错点

堆排序最容易错的是下标关系和“当前堆大小”。

  • 建堆从最后一个非叶子节点开始,不是从根开始。
  • 排序阶段每次交换后,堆的有效长度会减少 1。
  • heapify 处理的是“以某个节点为根的子树恢复堆性质”,不是整堆重建。
  • 堆排序不稳定,别把它和优先队列的功能混为一谈。

经典题目

基础应用

堆的应用

Top K 问题

优缺点

优点

  • ✅ 时间复杂度稳定:保证 O(n log n)
  • ✅ 原地排序:只需 O(1) 额外空间
  • ✅ 不需要递归(可避免栈溢出)
  • ✅ 适合找Top K元素

缺点

  • ❌ 不稳定:交换操作破坏相对顺序
  • ❌ 实际性能不如快速排序(缓存不友好)
  • ❌ 建堆过程复杂

应用场景

  1. Top K问题:找最大或最小的K个元素
  2. 优先队列:任务调度、事件驱动
  3. 时间保证:需要稳定的O(n log n)且空间受限
  4. 不需要稳定性:对稳定性无要求

堆排序 vs 其他排序

特性堆排序快速排序归并排序
时间复杂度O(n log n)O(n log n) ~ O(n²)O(n log n)
空间复杂度O(1)O(log n)O(n)
稳定性不稳定不稳定稳定
实际性能中等最快较慢
最坏保证有无有

为什么堆排序实际性能不如快排

  1. 缓存不友好:堆的操作跳跃访问内存
  2. 常数因子大:建堆和调整的操作较多
  3. 分支预测差:比较操作不规律

堆排序的优化

  • 使用迭代版下沉:把递归 heapify 改成循环,减少函数调用开销,也避免极端输入下的递归深度。
  • 从后往前建堆:只需要处理索引 n/2-1 到 0 的非叶子节点,叶子节点天然满足堆性质。
  • 按场景选择堆大小:求前 k 大时不必完整堆排序,维护大小为 k 的最小堆即可,复杂度为 O(n log k)。
  • 不要误追求稳定性:如果需要稳定排序,应优先考虑归并排序或在元素中附带原始下标。

相关主题


返回:排序算法 | 算法学习导航