堆排序(Heap Sort)
堆排序(Heap Sort)
一、前置知识:什么是堆
堆是一种特殊的完全二叉树,用数组来存储(不需要指针),利用索引关系表达父子关系:
- 父节点索引:
parent(i) = (i-1) / 2 - 左子节点索引:
left(i) = 2*i + 1 - 右子节点索引:
right(i) = 2*i + 2
大根堆(Max Heap):每个节点的值 ≥ 其子节点的值 → 根节点是全局最大值
小根堆(Min Heap):每个节点的值 ≤ 其子节点的值 → 根节点是全局最小值
大根堆示例(数组: [9, 8, 7, 3, 5, 6, 2])
9 ← 根(最大值)
/ \
8 7
/ \ / \
3 5 6 2
索引映射:
0→9, 1→8, 2→7, 3→3, 4→5, 5→6, 6→2
parent(6)=2→7, left(1)=3→3, right(1)=4→5
二、堆排序的核心思想
堆排序分两个阶段:
- 建堆:把无序数组调整成大根堆(根 = 最大值)
- 排序:反复把堆顶最大值与末尾交换,缩小堆的范围,再重新调整堆
每一轮:最大值被"摘走"放到数组末尾,剩余元素重新堆化。n轮之后,数组从后往前依次存放最大、次大、……最小值,排序完成。
初始: [4, 10, 3, 5, 1]
建大根堆: [10, 5, 3, 4, 1]
10在堆顶(最大)
第1轮: 10 ↔ 1 → [1, 5, 3, 4 | 10]
重新堆化 → [5, 4, 3, 1 | 10]
第2轮: 5 ↔ 1 → [1, 4, 3 | 5, 10]
重新堆化 → [4, 1, 3 | 5, 10]
第3轮: 4 ↔ 3 → [3, 1 | 4, 5, 10]
重新堆化 → [3, 1 | 4, 5, 10]
第4轮: 3 ↔ 1 → [1 | 3, 4, 5, 10]
结果: [1, 3, 4, 5, 10] ← 升序完成
三、关键操作详解
3.1 堆化(Heapify / Sift Down)
堆化是维护堆性质的核心操作。从某个节点出发,让它与子节点比较,如果违反堆性质就与较大的子节点交换,然后继续向下检查,直到满足堆性质或到达叶子。
// heapify 对以i为根的子树执行下沉操作,维护大根堆性质
// n 是堆的有效大小(排序时堆会逐渐缩小)
func heapify(arr []int, i, n 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, largest, n) // 递归继续下沉
}
}
时间复杂度:O(log n),因为最多下沉到叶子,路径长度 ≤ 树高 = log n
3.2 建堆(Build Heap)
从最后一个非叶子节点开始,自底向上逐个堆化。最后一个非叶子节点索引为 (n/2 - 1)。
func buildHeap(arr []int) {
n := len(arr)
// 从最后一个非叶子节点向根遍历
for i := n/2 - 1; i >= 0; i-- {
heapify(arr, i, n)
}
}
时间复杂度:看似 O(n log n)(n个节点 × 每次 O(log n)),但精确分析是 O(n)。原因是:底层节点很少需要下沉(叶子不用下沉),高层节点虽然路径长但数量少。数学证明:∑(节点数 × 下沉深度) = O(n)。
3.3 排序过程
建好堆后,每次把堆顶(arr[0],最大值)与当前堆末尾交换,堆大小减1,再堆化:
func heapSort(arr []int) {
n := len(arr)
buildHeap(arr) // 先建大根堆
for i := n - 1; i > 0; i-- {
arr[0], arr[i] = arr[i], arr[0] // 堆顶最大值放到末尾
heapify(arr, 0, i) // 缩小堆范围,重新堆化
}
}
四、Go 语言完整实现
package main
import "fmt"
// HeapSort 堆排序(升序)
func HeapSort(arr []int) {
n := len(arr)
// 1. 建大根堆
buildMaxHeap(arr, n)
// 2. 反复取出堆顶最大值放到末尾
for i := n - 1; i > 0; i-- {
arr[0], arr[i] = arr[i], arr[0] // 交换堆顶和堆尾
siftDown(arr, 0, i) // 堆化剩余部分
}
}
func buildMaxHeap(arr []int, n int) {
for i := n/2 - 1; i >= 0; i-- {
siftDown(arr, i, n)
}
}
// siftDown 下沉操作(迭代版,避免递归栈开销)
func siftDown(arr []int, i, n int) {
for {
left := 2*i + 1
right := 2*i + 2
largest := i
if left < n && arr[left] > arr[largest] {
largest = left
}
if right < n && arr[right] > arr[largest] {
largest = right
}
if largest == i {
break // 已满足堆性质
}
arr[i], arr[largest] = arr[largest], arr[i]
i = largest // 继续从子节点位置下沉
}
}
func main() {
data := []int{12, 11, 13, 5, 6, 7, 3, 1, 9}
fmt.Println("排序前:", data)
HeapSort(data)
fmt.Println("排序后:", data)
}
Go 标准库的 heap 包
Go 提供了 container/heap 掯口,可以自定义堆类型。需要实现以下5个方法:
package main
import (
"container/heap"
"fmt"
)
// IntHeap 定义一个最小堆
type IntHeap []int
func (h IntHeap) Len() int { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] } // 小根堆
func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *IntHeap) Push(x any) {
*h = append(*h, x.(int))
}
func (h *IntHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[:n-1]
return x
}
func main() {
h := &IntHeap{2, 1, 5, 6, 4, 3}
heap.Init(h) // 建堆
fmt.Println("堆初始化后:", *h)
heap.Push(h, 0) // 插入元素
fmt.Println("插入0后:", *h)
// 依次弹出最小元素(即排序结果)
fmt.Println("依次弹出:")
for h.Len() > 0 {
fmt.Printf(" 弹出: %d, 剩余: %v\n", heap.Pop(h), *h)
}
}
运行结果:
堆初始化后: [1 2 3 6 4 5]
插入0后: [0 1 3 6 2 5 4]
依次弹出:
弹出: 0, 剩余: [1 2 3 6 5 4]
弹出: 1, 剩余: [2 4 3 6 5]
弹出: 2, 剩余: [4 5 3 6]
弹出: 3, 剩余: [4 5 6]
弹出: 4, 剩余: [5 6]
弹出: 5, 剩余: [6]
弹出: 6, 剩余: []
五、复杂度分析
| 情况 | 时间复杂度 | 说明 |
|---|---|---|
| 最好 | O(n log n) | 堆排序不受数据分布影响 |
| 平均 | O(n log n) | 同上 |
| 最坏 | O(n log n) | 不退化!这是堆排序和快排的核心区别 |
- 空间复杂度:O(1),原地排序,用数组本身存储堆结构
- 稳定性:❌ 不稳定——堆化过程中父子节点交换可能跨越相等元素
时间复杂度推导
- 建堆:O(n)
- 排序阶段:n-1轮,每轮
siftDownO(log n) → O(n log n) - 总计:O(n) + O(n log n) = O(n log n)
为什么堆排序实际速度慢于快排
虽然两者都是 O(n log n),堆排序的常数因子更大:
- 缓存不友好:堆化过程中父子节点索引跳跃大(2i+1, 2i+2),不像快排那样顺序扫描相邻元素
- 比较次数更多:每轮 siftDown 要比较2次(左子和右子),快排每轮只比较1次
- 数据局部性差:堆的父子关系导致数据访问模式分散,CPU缓存命中率低
六、堆的应用场景
堆排序本身在工程中用得不多,但堆这种数据结构应用极广:
| 应用 | 说明 |
|---|---|
| 优先队列 | 任务调度、Dijkstra最短路径、Huffman编码 |
| Top-K 问题 | 找最大/最小的K个元素,O(n + k log n) |
| 中位数维护 | 两个堆(大根+小根)动态维护中位数 |
| 合并K个有序链表 | 最小堆每次取出最小头节点 |
| 定时器/延时任务 | 最小堆管理到期时间最近的任务 |
Top-K 实战
package main
import "fmt"
// TopKMax 找出数组中最大的K个元素(使用小根堆)
// 思路:维护一个大小为K的小根堆,堆顶是K个元素中最小的
// 新元素 > 堆顶时,弹出堆顶,插入新元素
func TopKMax(arr []int, k int) []int {
if k >= len(arr) {
HeapSort(arr)
return arr[len(arr)-k:]
}
// 先用前K个元素建小根堆
heap := make([]int, k)
for i := 0; i < k; i++ { heap[i] = arr[i] }
buildMinHeap(heap, k)
// 遍历剩余元素
for i := k; i < len(arr); i++ {
if arr[i] > heap[0] { // 比堆顶小值大才替换
heap[0] = arr[i]
siftDownMin(heap, 0, k)
}
}
return heap
}
func buildMinHeap(arr []int, n int) {
for i := n/2 - 1; i >= 0; i-- {
siftDownMin(arr, i, n)
}
}
func siftDownMin(arr []int, i, n int) {
for {
left := 2*i + 1
right := 2*i + 2
smallest := i
if left < n && arr[left] < arr[smallest] { smallest = left }
if right < n && arr[right] < arr[smallest] { smallest = right }
if smallest == i { break }
arr[i], arr[smallest] = arr[smallest], arr[i]
i = smallest
}
}
func HeapSort(arr []int) {
n := len(arr)
buildMaxHeapSort(arr, n)
for i := n - 1; i > 0; i-- {
arr[0], arr[i] = arr[i], arr[0]
siftDownMax(arr, 0, i)
}
}
func buildMaxHeapSort(arr []int, n int) {
for i := n/2 - 1; i >= 0; i-- { siftDownMax(arr, i, n) }
}
func siftDownMax(arr []int, i, n int) {
for {
l, r, largest := 2*i+1, 2*i+2, i
if l < n && arr[l] > arr[largest] { largest = l }
if r < n && arr[r] > arr[largest] { largest = r }
if largest == i { break }
arr[i], arr[largest] = arr[largest], arr[i]
i = largest
}
}
func main() {
data := []int{3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9}
fmt.Println("原始数组:", data)
fmt.Println("最大的5个元素:", TopKMax(data, 5))
}
七、四种 O(n log n) 排序对比总览
| 特性 | 快速排序 | 归并排序 | 堆排序 | 希尔排序* |
|---|---|---|---|---|
| 时间(最好) | O(n log n) | O(n log n) | O(n log n) | O(n log n) |
| 时间(最坏) | O(n²) ❌ | O(n log n) ✅ | O(n log n) ✅ | O(n^1.3) |
| 空间 | O(log n) | O(n) | O(1) ✅ | O(1) |
| 稳定性 | 不稳定 | 稳定 ✅ | 不稳定 | 不稳定 |
| 实际速度 | 最快 ✅ | 中等 | 较慢(缓存不友好) | 较快 |
| 适用场景 | 通用内存排序 | 外部排序、稳定性 | 最坏保障、Top-K | 中等规模数据 |
*希尔排序时间复杂度取决于步长序列,此处取常见 Shell 步长的典型值。
八、今日练习
package main
import "fmt"
// 练习1: 实现堆排序并验证
func HeapSort(arr []int) {
n := len(arr)
for i := n/2 - 1; i >= 0; i-- {
siftDown(arr, i, n)
}
for i := n - 1; i > 0; i-- {
arr[0], arr[i] = arr[i], arr[0]
siftDown(arr, 0, i)
}
}
func siftDown(arr []int, i, n int) {
for {
l, r, max := 2*i+1, 2*i+2, i
if l < n && arr[l] > arr[max] { max = l }
if r < n && arr[r] > arr[max] { max = r }
if max == i { break }
arr[i], arr[max] = arr[max], arr[i]
i = max
}
}
// 练习2: 实现小根堆排序(降序输出)
func HeapSortDesc(arr []int) {
n := len(arr)
// 建小根堆
for i := n/2 - 1; i >= 0; i-- {
siftUpMin(arr, i, n)
}
// 反复取出最小值放末尾
for i := n - 1; i > 0; i-- {
arr[0], arr[i] = arr[i], arr[0]
siftUpMin(arr, 0, i)
}
}
func siftUpMin(arr []int, i, n int) {
for {
l, r, min := 2*i+1, 2*i+2, i
if l < n && arr[l] < arr[min] { min = l }
if r < n && arr[r] < arr[min] { min = r }
if min == i { break }
arr[i], arr[min] = arr[min], arr[i]
i = min
}
}
// 练习3: 验证堆性质
func isMaxHeap(arr []int) bool {
n := len(arr)
for i := 0; i <= n/2-1; i++ {
left := 2*i + 1
right := 2*i + 2
if left < n && arr[i] < arr[left] { return false }
if right < n && arr[i] < arr[right] { return false }
}
return true
}
func main() {
// 验证堆排序(升序)
data := []int{12, 11, 13, 5, 6, 7, 3, 1, 9}
fmt.Println("升序排序前:", data)
HeapSort(data)
fmt.Println("升序排序后:", data)
// 验证堆排序(降序)
data2 := []int{12, 11, 13, 5, 6, 7, 3, 1, 9}
fmt.Println("\n降序排序前:", data2)
HeapSortDesc(data2)
fmt.Println("降序排序后:", data2)
// 验证建堆后是否满足大根堆性质
data3 := []int{4, 10, 3, 5, 1}
fmt.Println("\n建堆前:", data3, "isMaxHeap:", isMaxHeap(data3))
// 建堆
n := len(data3)
for i := n/2 - 1; i >= 0; i-- { siftDown(data3, i, n) }
fmt.Println("建堆后:", data3, "isMaxHeap:", isMaxHeap(data3))
}
运行结果:
升序排序前: [12 11 13 5 6 7 3 1 9]
升序排序后: [1 3 5 6 7 9 11 12 13]
降序排序前: [12 11 13 5 6 7 3 1 9]
降序排序后: [13 12 11 9 7 6 5 3 1]
建堆前: [4 10 3 5 1] isMaxHeap: false
建堆后: [10 5 3 4 1] isMaxHeap: true
九、知识要点总结
| 要点 | 说明 |
|---|---|
| 堆结构 | 完全二叉树用数组存储,索引 2i+1/2i+2 表达父子关系 |
| 大根堆 | 每个节点 ≥ 子节点,根 = 全局最大 |
| 堆化(siftDown) | 节点与子节点比较,违反则交换后继续下沉,O(log n) |
| 建堆 | 自底向上逐个堆化,精确复杂度 O(n) |
| 排序过程 | 堆顶最大值 ↔ 末尾 → 缩堆 → 堆化 → 重复 |
| 时间复杂度 | 始终 O(n log n),不退化 |
| 空间复杂度 | O(1),原地排序 |
| 不稳定 | 父子交换可能跨越相等元素 |
| 实际速度 | 缓存不友好,慢于快排和归并 |
| Top-K | 维护大小为K的堆,O(n)遍历+O(k log n)维护 |

浙公网安备 33010602011771号