AIGC标识 堆排序(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

二、堆排序的核心思想

堆排序分两个阶段:

  1. 建堆:把无序数组调整成大根堆(根 = 最大值)
  2. 排序:反复把堆顶最大值与末尾交换,缩小堆的范围,再重新调整堆

每一轮:最大值被"摘走"放到数组末尾,剩余元素重新堆化。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轮,每轮 siftDown O(log n) → O(n log n)
  • 总计:O(n) + O(n log n) = O(n log n)

为什么堆排序实际速度慢于快排

虽然两者都是 O(n log n),堆排序的常数因子更大:

  1. 缓存不友好:堆化过程中父子节点索引跳跃大(2i+1, 2i+2),不像快排那样顺序扫描相邻元素
  2. 比较次数更多:每轮 siftDown 要比较2次(左子和右子),快排每轮只比较1次
  3. 数据局部性差:堆的父子关系导致数据访问模式分散,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)维护
posted @ 2026-07-28 11:15  FfHUCisI  阅读(22)  评论(0)    收藏  举报