基础排序算法(四)堆排序

一 堆排序

堆排序是一种非常高效且独特的排序算法,它巧妙地将数据结构中的“堆”应用于排序过程。

1.1 特性介绍

堆排序特性总结

特性 说明
核心思想 利用这种数据结构进行选择排序。将待排序列构造成一个大顶堆(或小顶堆),从而不断将堆顶元素(最大值或最小值)与序列末尾元素交换,并调整堆结构,最终得到有序序列
时间复杂度 建堆:O(n);排序过程:O(n log n)。总体时间复杂度为 O(n log n),且最好、最坏、平均情况均如此
空间复杂度 O(1),是原地排序算法,只需要常数级别的额外空间用于元素交换
稳定性 不稳定。因交换堆顶和末尾元素时,可能改变相等元素的原始相对顺序
主要优势 时间复杂度稳定,不会出现像快速排序那样的最坏O(n²)情况;原地排序,空间效率高
主要劣势 常数因子较大,通常比平均情况下的快速排序慢;对小规模数据排序效率相对不高;不稳定

1.2 算法工作原理

堆排序的运作过程清晰地分为“建堆”和“排序”两大阶段。下图以数组 [12, 11, 13, 5, 6, 7]为例,展示了构建大顶堆以及后续排序的关键步骤:
图片

1.2.1 构建堆(Heapify)​​

这是堆排序的第一步,目标是將一个无序的完全二叉树调整成一个​​大顶堆​​(对于升序排序)。大顶堆的性质是:每个节点的值都​​大于或等于​​其左右子节点的值。

  • 通常从​​最后一个非叶子节点​​开始(索引为 n/2 - 1,其中 n是数组长度),依次向前遍历每个非叶子节点。
  • 对每个非叶子节点,执行“向下调整”操作:比较该节点与其左右子节点的值,如果子节点中有值更大的,则交换它们的位置,并递归地对被交换的子节点进行同样的调整,确保以该节点为根的子树满足堆的性质。

1.2.2 排序​​

一旦大顶堆构建完成,堆顶元素(根节点)就是整个序列中的最大值。

  • 交换与调整​​:将堆顶元素与堆中最后一个元素交换。此时,最大值就位于数组的末尾,可以视为已排序部分。
  • 缩小堆范围​​:将堆的大小减一(即忽略最后一个已排序的元素)。
  • ​​重新调整堆​​:由于新的堆顶元素可能破坏堆的性质,因此需要对剩余的未排序序列(从新的根节点开始)再次进行向下调整,使其重新成为大顶堆。
  • 重复上述“交换-调整-缩小”步骤,直到堆的大小变为1,此时整个数组就有序了。

1.3 复杂度分析

1.3.1 时间复杂度 O(n log n) 的由来​​:

  • ​​建堆阶段​​:虽然需要调整n/2个节点,但由于二叉树的结构特性,大部分调整操作在较低的层进行。建堆操作的时间复杂度经分析为 ​​O(n)​​。
  • ​​排序阶段​​:需要进行n-1次交换和调整。每次调整堆(向下调整)的时间复杂度与当前堆的高度相关,即 ​​O(log n)​​。因此,排序阶段的总时间复杂度为 ​​O(n log n)​​。
    堆排序的整体时间复杂度为 ​​O(n log n)​​,并且这个性能在最好、最坏和平均情况下都保持一致,非常稳定。

1.3.2 ​​空间复杂度 O(1) 的由来​​:

堆排序的所有操作(交换、调整)都是在原数组上进行的,只需要固定数量的额外变量(如用于交换的临时变量 temp),因此是​​原地排序​​,空间复杂度为 ​​O(1)​​。

1.4 使用场景

堆排序在以下场景中表现出色:

​​需要稳定的最坏情况性能​​:当无法承受快速排序最坏情况下O(n²)的时间复杂度时,堆排序的O(n log n)最坏情况性能是可靠的选择。
​​内存空间受限​​:由于是原地排序,空间复杂度为O(1),非常适合嵌入式系统等内存紧张的环境。
​​寻找Top K元素​​:不需要完全排序整个序列,只需找出最大或最小的K个元素。可以构建一个大小为K的小顶堆(找最大K个)或大顶堆(找最小K个),然后遍历剩余元素进行调整,效率很高。
​​实时数据流处理​​:堆结构适合处理不断产生新数据的场景,可以动态维护当前数据的极值或特定顺序。

然而,对于​​小规模数据​​,简单排序如​​插入排序​​可能因常数因子更小而实际更快。若​​需要稳定性​​(保持相等元素的原始顺序),则应选择​​归并排序​​等稳定算法。

1.5 代码实现

1.5.1 c 语言实现

以下是堆排序的一个典型C语言实现,包含了关键的堆调整(heapify)函数和排序主函数。

#include <stdio.h>

// 交换数组中两个元素的值
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

// 堆调整函数(大顶堆)
// arr: 待调整的数组
// n: 当前堆的大小(即需要调整的数组长度)
// i: 待调整的根节点索引
void heapify(int arr[], int n, int i) {
    int largest = i;       // 初始化最大元素为根节点
    int left = 2 * i + 1;  // 左子节点索引
    int 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) {
        swap(&arr[i], &arr[largest]);
        heapify(arr, n, largest); // 递归调整受影响的子树
    }
}

// 堆排序主函数
void heapSort(int arr[], int n) {
    // 1. 构建初始大顶堆
    // 从最后一个非叶子节点开始向上调整
    for (int i = n / 2 - 1; i >= 0; i--)
        heapify(arr, n, i);

    // 2. 逐个从堆顶提取元素
    for (int i = n - 1; i > 0; i--) {
        // 将当前堆顶元素(最大值)与末尾元素交换
        swap(&arr[0], &arr[i]);
        // 交换后,调整剩余的元素(从0到i-1),使其重新成为大顶堆
        heapify(arr, i, 0);
    }
}

// 打印数组函数
void printArray(int arr[], int n) {
    for (int i = 0; i < n; i++)
        printf("%d ", arr[i]);
    printf("\n");
}

// 主函数测试
int main() {
    int arr[] = {12, 11, 13, 5, 6, 7};
    int n = sizeof(arr) / sizeof(arr[0]);

    printf("排序前的数组: \n");
    printArray(arr, n);

    heapSort(arr, n);

    printf("排序后的数组: \n");
    printArray(arr, n);
    return 0;
}

1.5.2 关键点解释

  • heapify函数:这是堆排序的核心。它确保以节点 i为根的子树满足大顶堆的性质。通过比较根节点与其左右子节点,找到最大值,并在需要时进行交换和递归调整。
  • heapSort函数:
    • ​​建堆循环​​:for (int i = n / 2 - 1; i >= 0; i--)从最后一个非叶子节点开始,自底向上地调用 heapify,最终构建出整个大顶堆。
    • ​排序循环​​:for (int i = n - 1; i > 0; i--)每次循环将堆顶(最大值)与当前未排序序列的最后一个元素交换,然后对新的堆顶调用 heapify来调整剩余元素,使其恢复成大顶堆。

1.6 与常见排序算法比较

排序算法比较

算法 平均时间复杂度 最坏时间复杂度 空间复杂度 稳定性 主要特点与适用场景
堆排序 O(n log n) O(n log n) O(1) 不稳定 最坏性能稳定,原地排序,适合内存受限和需要稳定最坏情况性能的场景
快速排序 O(n log n) O(n²) O(log n) 不稳定 平均性能极佳,是许多标准库的实现选择,但对初始数据敏感,最坏情况性能差
归并排序 O(n log n) O(n log n) O(n) 稳定 性能稳定,是稳定的O(n log n)排序,但需要O(n)额外空间,适合外部排序和对稳定性有要求的场景
插入排序 O(n²) O(n²) O(1) 稳定 对小规模或基本有序数据效率很高,常作为快速排序或归并排序的辅助排序

1.7 总结

总的来说,堆排序是一种基于​​堆数据结构​​的高效排序算法,其​​时间复杂度稳定在O(n log n)​​,并且是​​原地排序​​,空间复杂度为O(1)。虽然在实际应用中可能不如快速排序快速,且是​​不稳定​​排序,但在​​需要保证最坏情况性能、内存空间有限或需要解决Top K问题​​等特定场景下,它依然是一个非常有价值的选择。理解堆排序对于深入掌握数据结构和算法设计思想具有重要意义。

posted on 2025-10-31 11:33  weiwei2021  阅读(1)  评论(0)    收藏  举报