堆排序原理与C++实现详解

堆排序原理与C++实现详解

堆排序(Heap Sort)是一种基于堆数据结构的高效排序算法,它利用堆的特性(大顶堆或小顶堆)来实现排序,时间复杂度稳定为 O(n log n),空间复杂度为 O(1),属于原地排序算法。本文将从堆的基础概念出发,详细讲解堆排序的核心原理,再一步步实现C++版本的堆排序代码,并对算法性能进行分析。

一、堆的基础概念

在讲解堆排序之前,我们需要先明确"堆"的定义和特性:

  1. 堆的结构:堆是一棵完全二叉树,完全二叉树的特点是除了最后一层外,每一层的节点数都是满的,最后一层的节点从左到右依次排列,没有空缺。

  2. 堆的类型

    • 大顶堆:每个父节点的值都大于等于其两个子节点的值,堆顶(根节点)是整个堆中的最大值。

    • 小顶堆:每个父节点的值都小于等于其两个子节点的值,堆顶是整个堆中的最小值。

  3. 堆的存储:由于堆是完全二叉树,我们可以用数组(顺序表)来高效存储,无需额外的指针开销。假设数组下标从 0 开始,对于索引为 i 的节点:

    • 左子节点的索引:2 * i + 1

    • 右子节点的索引:2 * i + 2

    • 父节点的索引:(i - 1) / 2(整数除法,自动向下取整)

二、堆排序的核心原理

堆排序的核心思路是利用大顶堆(或小顶堆)的堆顶元素是最大值(或最小值)的特性,通过不断提取堆顶元素并调整堆结构,最终得到有序数组。具体步骤分为 3 步:

1. 构建初始堆(Heapify)

将待排序的无序数组转换为大顶堆(升序排序用大顶堆,降序排序用小顶堆)。构建堆的关键是"堆化"操作------从最后一个非叶子节点开始,依次向前对每个节点进行调整,确保每个节点都满足大顶堆的特性。

为什么从最后一个非叶子节点开始?因为叶子节点没有子节点,本身已经是合法的堆结构,无需调整。最后一个非叶子节点的索引就是 (n - 2) / 2(n 为数组长度,最后一个节点索引为 n-1,其父节点即为 (n-1-1)/2 = (n-2)/2)。

2. 提取堆顶元素并调整堆

堆构建完成后,堆顶元素是最大值。将堆顶元素与数组末尾的元素交换,此时数组末尾就存放了当前的最大值;然后将剩余的 n-1 个元素重新调整为大顶堆(因为交换后堆顶元素可能破坏堆结构,需要从堆顶开始重新堆化)。

3. 重复步骤2

不断重复"交换堆顶与末尾元素→调整堆"的过程,直到剩余元素个数为 1,此时数组已经完全有序。

三、C++实现堆排序的步骤拆解

我们以升序排序为例,用大顶堆实现堆排序。C++代码的核心是两个函数:

  • heapify 函数:调整指定节点为根的子树为大顶堆

  • heapSort 函数:主排序函数,负责构建初始堆和循环提取堆顶元素

1. 实现 heapify 函数

函数功能:给定数组、数组长度、当前需要调整的节点索引,将该节点为根的子树调整为大顶堆。

实现思路:

  1. 假设当前节点 i 是最大值节点,记录其索引为 largest;

  2. 计算当前节点的左子节点 l = 2i + 1,右子节点 r = 2i + 2;

  3. 比较左子节点与 largest 的值,若左子节点更大,则更新 largest 为 l;

  4. 比较右子节点与 largest 的值,若右子节点更大,则更新 largest 为 r;

  5. 若 largest 不等于 i(说明当前节点不是最大值,需要交换):

    • 交换节点 i 和节点 largest 的值;

    • 递归调整 largest 所在的子树(因为交换后,largest 节点可能破坏了子树的堆结构)。

2. 实现 heapSort 函数

函数功能:对数组进行堆排序(升序)。

实现思路:

  1. 构建初始大顶堆:从最后一个非叶子节点 (n-2)/2 开始,向前遍历每个节点,调用 heapify 函数调整;

  2. 循环提取堆顶元素:

    • 从数组末尾开始(索引 n-1),依次与堆顶(索引 0)交换;

    • 交换后,剩余未排序元素的个数减 1,对新的堆顶(索引 0)调用 heapify 函数,调整剩余元素为大顶堆;

    • 重复上述过程,直到未排序元素个数为 1。

四、完整C++代码实现

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

// 堆化函数:调整以i为根的子树为大顶堆
void heapify(vector<int>& arr, int n, int i) {
    int largest = i;         // 初始化最大值为根节点
    int l = 2 * i + 1;       // 左子节点索引
    int r = 2 * i + 2;       // 右子节点索引

    // 若左子节点大于根节点,更新最大值索引
    if (l < n && arr[l] > arr[largest]) {
        largest = l;
    }

    // 若右子节点大于当前最大值,更新最大值索引
    if (r < n && arr[r] > arr[largest]) {
        largest = r;
    }

    // 若最大值不是根节点,需要交换并递归调整子树
    if (largest != i) {
        swap(arr[i], arr[largest]);  // 交换根节点和最大值节点
        heapify(arr, n, largest);    // 递归调整交换后的子树
    }
}

// 堆排序主函数(升序)
void heapSort(vector<int>& arr) {
    int n = arr.size();

    // 第一步:构建初始大顶堆
    // 从最后一个非叶子节点开始向前遍历
    for (int i = n / 2 - 1; i >= 0; i--) {
        heapify(arr, n, i);
    }

    // 第二步:循环提取堆顶元素并调整堆
    for (int i = n - 1; i > 0; i--) {
        swap(arr[0], arr[i]);  // 交换堆顶(最大值)和当前未排序部分的末尾
        heapify(arr, i, 0);    // 调整剩余i个元素为大顶堆(i是当前未排序元素个数)
    }
}

// 辅助函数:打印数组
void printArray(const vector<int>& arr) {
    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;
}

// 测试代码
int main() {
    vector<int> arr = {12, 11, 13, 5, 6, 7};
    cout << "排序前的数组:";
    printArray(arr);

    heapSort(arr);

    cout << "排序后的数组:";
    printArray(arr);

    return 0;
}

代码说明

  • 使用 vector 存储数组,便于动态调整长度,适配不同规模的排序需求;

  • heapify 函数采用递归实现,逻辑清晰,也可以用迭代实现(避免递归栈开销,适合大规模数据);

  • printArray 函数用于辅助打印排序前后的数组,方便验证结果;

  • 测试用例为 {12, 11, 13, 5, 6, 7},排序后输出为 {5, 6, 7, 11, 12, 13},符合升序要求。

五、堆排序的性能分析

1. 时间复杂度

  • 构建初始堆的时间复杂度:O(n)。虽然构建堆时需要遍历 O(n/2) 个节点,每个节点的堆化操作时间复杂度为 O(log n),但整体复杂度经过数学推导为 O(n);

  • 循环提取堆顶元素的时间复杂度:O(n log n)。共需要循环 n-1 次,每次堆化操作的时间复杂度为 O(log n),因此总复杂度为 O(n log n);

  • 堆排序的整体时间复杂度:O(n log n),且最好、最坏、平均时间复杂度均为 O(n log n),稳定性优于快速排序(快速排序最坏时间复杂度为 O(n²))。

2. 空间复杂度

堆排序是原地排序算法,除了递归调用栈(递归实现的 heapify 函数)外,不需要额外的存储空间。若采用迭代实现 heapify 函数,空间复杂度为 O(1);递归实现的空间复杂度为 O(log n)(递归深度为堆的高度,即 log n)。

3. 稳定性

堆排序是不稳定排序。因为在交换堆顶元素和末尾元素时,可能会导致相同值的元素相对位置发生变化。例如,数组 [2, 2, 1],构建大顶堆后堆顶为 2(第一个 2),与末尾的 1 交换后得到 [1, 2, 2],此时两个 2 的相对位置未变;但对于数组 [3, 2, 2, 1],交换过程中可能会导致两个 2 的位置颠倒。

六、堆排序的应用场景

堆排序适合处理大规模数据,尤其是对排序稳定性要求不高的场景,例如:

  • 操作系统中的进程调度(利用堆快速获取优先级最高的进程);

  • Top K 问题(例如获取数组中前 K 个最大元素,无需完整排序,用堆实现效率更高);

  • 大数据排序(堆排序的原地排序特性使其在内存有限的场景下更具优势)。

七、常见问题与优化

1. 递归堆化的栈溢出问题

对于大规模数据(例如数组长度超过 1e5),递归实现的 heapify 函数可能会导致栈溢出。解决方案:将递归实现改为迭代实现。

迭代版 heapify 函数实现:

cpp 复制代码
void heapifyIterative(vector<int>& arr, int n, int i) {
    while (true) {
        int largest = i;
        int l = 2 * i + 1;
        int r = 2 * i + 2;

        if (l < n && arr[l] > arr[largest]) {
            largest = l;
        }
        if (r < n && arr[r] > arr[largest]) {
            largest = r;
        }

        if (largest == i) {
            break;  // 无需调整,退出循环
        }

        swap(arr[i], arr[largest]);
        i = largest;  // 更新当前节点索引,继续调整子树
    }
}

2. 降序排序的实现

若需要实现降序排序,只需将大顶堆改为小顶堆即可。修改 heapify 函数中的比较逻辑:将"大于"改为"小于",确保每个父节点的值小于等于子节点的值。

八、总结

堆排序是一种高效、稳定的排序算法,核心是利用堆的特性通过"构建堆→提取堆顶→调整堆"的流程实现排序。其时间复杂度为 O(n log n),空间复杂度低,适合大规模数据排序。本文通过C++实现了基于大顶堆的升序堆排序,并讲解了核心函数的实现逻辑、性能分析和常见优化方案,希望能帮助大家深入理解堆排序的原理与应用。

相关推荐
2501_941803621 天前
在柏林智能城市照明场景中构建实时调控与高并发能耗数据分析平台的工程设计实践经验分享
算法
七七powerful1 天前
docker28.1.1和docker-compose v.2.35.1安装
java·docker·eureka
CoderIsArt1 天前
常用SCSI数据结构的详细注释和用法
数据结构
福楠1 天前
C++ STL | list
c语言·开发语言·数据结构·c++·算法·list
努力学算法的蒟蒻1 天前
day55(1.6)——leetcode面试经典150
算法·leetcode·面试
s砚山s1 天前
代码随想录刷题——二叉树篇(十)
算法
2301_764441331 天前
基于HVNS算法和分类装载策略的仓储系统仿真平台
人工智能·算法·分类
小白学大数据1 天前
百科词条结构化抓取:Java 正则表达式与 XPath 解析对比
java·开发语言·爬虫·正则表达式
AI科技星1 天前
统一场论变化的引力场产生电磁场推导与物理诠释
服务器·人工智能·科技·线性代数·算法·重构·生活