Skip to content

堆排序

为什么需要堆排序

简单选择排序每一趟都从未排序部分选出最小(或最大)元素,但每趟比较后只留下了最终结果,中间的比较信息全部丢失,导致时间复杂度固定为 O(n²)。

堆排序的核心改进:利用这种数据结构保存比较过程中的"胜负关系",使得每次选出最值只需 O(log n) 的调整,从而将总时间降至 O(n log n)。它是一种原地、不稳定的排序算法,在 408 考研中属于高频考点。

核心思想

堆排序分为两个阶段:

  1. 建堆:将无序数组自底向上调整为一个大顶堆(升序排序)
  2. 排序:反复将堆顶最大元素与末尾元素交换,然后对堆顶做一次向下调整(sift-down),堆的规模减 1,直到堆中只剩一个元素

整体思路可以概括为:建堆 → 取顶 → 调整 → 取顶 → 调整 → ... → 有序

交互可视化

通过下方的交互动画,你可以逐步观察堆排序的执行过程:

加载可视化中...

操作详解

堆的概念

堆是一棵完全二叉树,满足以下性质之一:

  • 大顶堆(最大堆):每个结点的值 ≥ 其左右孩子的值,即 A[i] ≥ A[2i+1]A[i] ≥ A[2i+2]
  • 小顶堆(最小堆):每个结点的值 ≤ 其左右孩子的值,即 A[i] ≤ A[2i+1]A[i] ≤ A[2i+2]

下标关系(数组从 0 开始)

关系公式
结点 i 的父结点(i - 1) / 2
结点 i 的左孩子2i + 1
结点 i 的右孩子2i + 2

若数组从 1 开始存储,则父结点为 i/2,左孩子为 2i,右孩子为 2i+1。408 真题中两种编号均有出现,务必注意题目约定。

建堆过程

建堆采用**自底向上的向下调整(sift-down)**策略:从最后一个非叶子结点开始,依次向前对每个结点执行 sift-down。

最后一个非叶子结点的下标为 n/2 - 1(数组从 0 开始)。

cpp
// 向下调整:将以 i 为根的子树调整为大顶堆
// A[] 为数组,n 为堆的元素个数
void SiftDown(int A[], int n, int i) {
    int largest = i;
    int left = 2 * i + 1;
    int right = 2 * i + 2;
    if (left < n && A[left] > A[largest])
        largest = left;
    if (right < n && A[right] > A[largest])
        largest = right;
    if (largest != i) {
        swap(A[i], A[largest]);
        SiftDown(A, n, largest);  // 继续向下调整
    }
}

// 建堆:从最后一个非叶子结点到根,依次 sift-down
void BuildMaxHeap(int A[], int n) {
    for (int i = n / 2 - 1; i >= 0; i--)
        SiftDown(A, n, i);
}

建堆的时间复杂度为 O(n),不是 O(n log n)。直觉上看似每个结点都做了 O(log n) 的调整,但大部分结点位于树的底层、调整距离很短。通过数学证明(对每层结点数 × 调整高度求和),总操作次数为 O(n)。这是 408 选择题常考的知识点。

排序过程

在大顶堆上进行升序排序:

  1. 将堆顶元素 A[0](当前最大值)与堆的最后一个元素 A[n-1] 交换
  2. 堆的规模减 1(最后一个位置已归位)
  3. 对新的堆顶 A[0] 执行 sift-down,恢复堆性质
  4. 重复上述步骤,直到堆中只剩一个元素
cpp
void HeapSort(int A[], int n) {
    BuildMaxHeap(A, n);           // 第一步:建堆
    for (int i = n - 1; i > 0; i--) {
        swap(A[0], A[i]);         // 堆顶与当前末尾交换
        SiftDown(A, i, 0);       // 对堆顶做向下调整,堆规模为 i
    }
}

堆的插入与删除

插入:将新元素追加到堆的末尾,然后执行向上调整(sift-up),依次与父结点比较并交换,直到满足堆性质。时间复杂度 O(log n)。

cpp
// 向上调整(大顶堆)
void SiftUp(int A[], int i) {
    while (i > 0) {
        int parent = (i - 1) / 2;
        if (A[i] > A[parent]) {
            swap(A[i], A[parent]);
            i = parent;
        } else {
            break;
        }
    }
}

删除堆顶:将堆顶与最后一个元素交换,堆规模减 1,然后对新堆顶执行 sift-down。时间复杂度 O(log n)。

⭐ 注意:用sift-up 逐个插入的方式建堆,时间复杂度为 O(n log n),不如自底向上 sift-down 建堆的 O(n) 高效。408 真题曾考过两种建堆方式的区别。

复杂度分析

指标复杂度说明
最好时间O(n log n)任何情况下都需要完整的建堆 + 调整过程
最坏时间O(n log n)与输入序列无关
平均时间O(n log n)三种情况均为同一量级
空间复杂度O(1)原地排序,仅需常数级辅助空间
稳定性不稳定堆顶与末尾交换会打乱相同元素的相对顺序

⭐ 堆排序是所有比较类排序中少数在最坏情况下仍能保持 O(n log n) 的算法(快排最坏为 O(n²)),但实际运行中由于缓存不友好,常数因子较大。

考研高频考点

  • ⭐ 建堆的时间复杂度为 O(n) 而非 O(n log n)(选择题/判断题高频陷阱)
  • ⭐ 堆排序是不稳定排序(简答题对比各排序算法必考)
  • ⭐ 堆排序空间复杂度 O(1),属于原地排序(与归并排序 O(n) 对比)
  • ⭐ 父子结点下标关系(从 0 或从 1 开始两种情况,选择题/填空题常考)
  • ⭐ Top-K 问题:从 n 个元素中选出前 K 大/小,建一个大小为 K 的小顶/大顶堆,时间 O(n log K)(算法设计题常见应用)
  • 在大顶堆中插入/删除一个元素后,画出调整后的堆(手工模拟题)
  • 给定序列判断是否为堆,或画出建堆的中间过程(选择题/填空题)
  • 堆排序 vs 快速排序 vs 归并排序的综合比较(简答题)