【算法系列】经典的堆排序算法

ops/2025/3/3 10:09:30/

文章目录

  • 堆排序算法
    • 什么是堆排序?
      • 二叉堆的概念
    • 堆排序的基本步骤
    • 堆排序的详细流程
      • 构建最大堆
      • 维护最大堆
      • 排序过程
      • Java代码实现
    • 堆排序的图示步骤
      • 1. 初始的数组与堆
      • 2. 构建最大堆
        • 2.1. 检查节点9(序号为3)
        • 2.2. 检查节点6(序号为2)
        • 2.3. 检查节点8(序号为1)
        • 2.4. 检查节点3(序号为0)
        • 2.5. 完成堆构造
      • 3. 归位
        • 3.1 归位索引8
        • 3.2 归位索引7
        • 3.3 归位索引6
        • 3.4 归位索引5
        • 3.5 归位索引4
        • 3.6 归位索引3
        • 3.7 归位索引2
        • 3.8 归位索引1
        • 3.9 归位索引0
    • 时间复杂度和空间复杂度
    • 特点和适用场景
      • 优点
      • 缺点
      • 适用场景
    • 结语

堆排序算法

什么是堆排序?

堆排序(Heap Sort)是一种基于二叉堆数据结构的比较排序算法。它利用了完全二叉树的特性,将待排序的数据构造成一个最大堆或最小堆,然后通过不断移除堆顶元素并重新调整堆来实现排序。堆排序具有稳定的时间复杂度O(n log n),并且不需要额外的空间,因此在处理大规模数据时表现良好。

二叉堆的概念

  • 最大堆:每个节点的值都大于或等于其子节点的值。
  • 最小堆:每个节点的值都小于或等于其子节点的值。

在堆排序中,通常使用最大堆来进行升序排序,使用最小堆进行降序排序。本文主要介绍基于最大堆的升序排序。

堆排序的基本步骤

  1. 构建最大堆

    • 将数组视为一个完全二叉树,并将其转换为最大堆。
    • 从最后一个非叶子节点开始(即 n/2 - 1),向上遍历每个节点,调用 heapify 函数来维护最大堆的性质。
  2. 堆排序过程

    • 将堆顶元素(最大值)与当前未排序部分的最后一个元素交换。
    • 缩小堆的大小(减少一个元素),并将新的堆顶元素向下调整以保持最大堆的性质。
    • 重复上述两步直到整个数组有序。

堆排序的详细流程

构建最大堆

构建最大堆的过程是从最后一个非叶子节点开始,逐层向上调整每个节点,确保它们满足最大堆的性质。

维护最大堆

heapify 函数用于维护最大堆的性质。如果发现某个节点不满足最大堆的条件,则交换该节点与其子节点中的较大者,并递归地继续调整子树。

排序过程

排序过程是通过不断将堆顶元素与当前未排序部分的最后一个元素交换,并缩小堆的大小,直到整个数组有序。

Java代码实现

java">public static void heapSort(int[] arr) {int n = arr.length;// 构建最大堆for (int i = n / 2 - 1; i >= 0; i--) {heapify(arr, n, i);}// 将堆顶元素逐个取出,并从后向前放置到数组中for (int i = n - 1; i > 0; i--) {// 交换堆顶元素至最后一个无序位置swap(arr, 0, i);// 重新调整为最大堆heapify(arr, i, 0);}
}private static void heapify(int[] arr, int n, int i) {int largest = i;  // 使用根节点索引初始化int left = 2 * i + 1; // 左叶子节点索引int right = (i + 1) * 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, largest);// 对子树递归堆化heapify(arr, n, largest);}
}private static void swap(int[] arr, int i, int j) {int temp = arr[i];arr[i] = arr[j];arr[j] = temp;
}

堆排序的图示步骤

这里以数组[3, 8, 6, 9, 2, 1, 5, 4, 7]升序排序,使用最大堆演示排序过程,具体步骤如下:

1. 初始的数组与堆

在这里插入图片描述
堆排序是利用完全二叉树进行排序的,内存中实际保存的是数组,但程序中的逻辑结构是一棵完全二叉树,如上图。

2. 构建最大堆

2.1. 检查节点9(序号为3)

从最后一个非叶子节点9(序号为3)开始检查,依次构造最大堆。
在这里插入图片描述
数组长度为n = arr.length = 9,第一个非叶子节点序号为i = n / 2 - 1 = 3
节点9的左右节点序号分别为2 * i + 1(i + 1) * 2,即7和8,它们的值分别为4和7。由于9>4,9>7,所以节点9符合最大堆的特性,无需调整。
在这里插入图片描述

2.2. 检查节点6(序号为2)

i–,值变为2,检查序号为2的节点。
在这里插入图片描述
其左右节点的序号分别为2 * i + 1(i + 1) * 2,即5和6,它们的值分别为1和5。由于6>1,6>5,所以节点6符合最大堆的特性,也无需调整。
在这里插入图片描述

2.3. 检查节点8(序号为1)

i–,值变为1,检查序号为1的节点。
在这里插入图片描述
其左右节点的序号分别为3和4,它们的值分别为9和2。由于8<9,所以交换节点8和9。
在这里插入图片描述
交换完成后,由于节点序号为3的元素值减小,以此为根的子树有可能违反最大堆的特性,所以递归检查该节点。
在这里插入图片描述
由于8>4且8>7,所以该子树符合最大堆特点。

2.4. 检查节点3(序号为0)

i–,值变为0,检查序号为0的节点。
在这里插入图片描述
其左右节点的序号分别为1和2,它们的值分别为9和6。由于3<9,所以交换节点3和9。
在这里插入图片描述
交换完成后,由于节点序号为1的元素值减小,以此为根的子树有可能违反最大堆的特性,所以递归检查该节点。
在这里插入图片描述
交换完成后,由于节点序号为3的元素值减小,以此为根的子树有可能违反最大堆的特性,继续递归检查该节点。
在这里插入图片描述

2.5. 完成堆构造

i–,值变为-1,循环结束。至此最大堆构造完成。

3. 归位

3.1 归位索引8

将堆顶元素逐个取出,并从后向前放置到数组进行归位。
在这里插入图片描述
将堆顶元素9与索引为8的元素交换位置,元素9完成排序,从堆中移除(虚线表示)。
交换完成后,由于节点序号为0的元素值减小,以此为根的子树有可能违反最大堆的特性,继续递归检查该节点。
在这里插入图片描述
交换序号分别为0和1的节点元素,并继续递归检查序号为1的节点。
在这里插入图片描述
交换序号分别为1和3的节点元素,并继续递归检查序号为3的节点。
在这里插入图片描述
至此,剩余8个元素恢复为最大堆。

3.2 归位索引7

在这里插入图片描述
将堆顶元素8与索引为7的元素交换位置,元素8完成排序,从堆中移除(虚线表示)。
交换完成后,由于节点序号为0的元素值减小,以此为根的子树有可能违反最大堆的特性,继续递归检查该节点。
在这里插入图片描述
交换序号分别为0和1的节点元素,并继续递归检查序号为3的节点。
在这里插入图片描述
交换序号分别为1和3的节点元素,并继续递归检查序号为3的节点。3号节点为叶子节点,无须调整。
至此,剩余7个元素恢复为最大堆。

3.3 归位索引6

在这里插入图片描述
将堆顶元素7与索引为6的元素交换位置,元素7完成排序,从堆中移除(虚线表示)。
交换完成后,由于节点序号为0的元素值减小,以此为根的子树有可能违反最大堆的特性,继续递归检查该节点。
在这里插入图片描述
交换序号分别为0和2的节点元素,并继续递归检查序号为2的节点。
在这里插入图片描述
2号节点符合最大堆特点,无须调整。
至此,剩余6个元素恢复为最大堆。

3.4 归位索引5

在这里插入图片描述
将堆顶元素6与索引为5的元素交换位置,元素6完成排序,从堆中移除(虚线表示)。
交换完成后,由于节点序号为0的元素值减小,以此为根的子树有可能违反最大堆的特性,继续递归检查该节点。
在这里插入图片描述
交换序号分别为0和2的节点元素,并继续递归检查序号为2的节点。
在这里插入图片描述
2号节点为叶子节点,无须调整。
至此,剩余5个元素恢复为最大堆。

3.5 归位索引4

在这里插入图片描述
将堆顶元素5与索引为4的元素交换位置,元素5完成排序,从堆中移除(虚线表示)。
交换完成后,由于节点序号为0的元素值减小,以此为根的子树有可能违反最大堆的特性,继续递归检查该节点。
在这里插入图片描述
交换序号分别为0和1的节点元素,并继续递归检查序号为1的节点。
在这里插入图片描述
交换序号分别为1和3的节点元素,并继续递归检查序号为3的节点。
在这里插入图片描述
3号节点为叶子节点,无须调整。
至此,剩余4个元素恢复为最大堆。

3.6 归位索引3

在这里插入图片描述
将堆顶元素4与索引为3的元素交换位置,元素4完成排序,从堆中移除(虚线表示)。
交换完成后,由于节点序号为0的元素值减小,以此为根的子树有可能违反最大堆的特性,继续递归检查该节点。
在这里插入图片描述
交换序号分别为0和1的节点元素,并继续递归检查序号为1的节点。
在这里插入图片描述
1号节点为叶子节点,无须调整。
至此,剩余3个元素恢复为最大堆。

3.7 归位索引2

在这里插入图片描述
将堆顶元素3与索引为2的元素交换位置,元素3完成排序,从堆中移除(虚线表示)。
交换完成后,由于节点序号为0的元素值减小,以此为根的子树有可能违反最大堆的特性,继续递归检查该节点。
在这里插入图片描述
交换序号分别为0和1的节点元素,并继续递归检查序号为1的节点。
在这里插入图片描述
1号节点为叶子节点,无须调整。
至此,剩余2个元素恢复为最大堆。

3.8 归位索引1

在这里插入图片描述
将堆顶元素2与索引为1的元素交换位置,元素2完成排序,从堆中移除(虚线表示)。
交换完成后,由于节点序号为0的元素值减小,以此为根的子树有可能违反最大堆的特性,继续递归检查该节点。
在这里插入图片描述
0号节点为叶子节点,无须调整。
至此,剩余1个元素恢复为最大堆。

3.9 归位索引0

将堆顶元素1与索引为0的元素交换位置,元素1完成排序,从堆中移除(虚线表示)。
在这里插入图片描述
至此,循环结束,递归也结束,排序完成。

时间复杂度和空间复杂度

  • 时间复杂度:
    • 构建堆的时间复杂度是 O(n)。
    • 每次提取堆顶元素并重新调整堆的时间复杂度是 O(log n),总共需要进行 n 次这样的操作。
    • 因此,总的时间复杂度为 O(n log n)。
  • 空间复杂度:
    • 堆排序是一个原地排序算法,只需要常数级别的额外空间 O(1)。

特点和适用场景

优点

  • 稳定性:虽然堆排序不是稳定的排序算法,但它的时间复杂度在最坏情况下也是 O(n log n),这使得它在处理大规模数据时非常高效。
  • 空间效率高:由于它是原地排序算法,不需要额外的存储空间。

缺点

性能略逊于快速排序:尽管堆排序的时间复杂度是 O(n log n),但在实际应用中,它的常数因子较大,导致其运行速度可能比快速排序慢。

适用场景

当你需要一种稳定的时间复杂度 O(n log n) 的排序算法,并且内存占用要求较高时,堆排序是一个不错的选择。

结语

堆排序作为一种经典的排序算法,不仅在理论上有重要的意义,在实际应用中也经常被使用。理解堆排序的工作原理及其背后的二叉堆数据结构,有助于更好地掌握其他高级算法和数据结构。


http://www.ppmy.cn/ops/162737.html

相关文章

CF 118A.String Task(Java实现)

题目分析 输入一个字符串&#xff0c;遍历每一个字符&#xff0c;如果是元音字母就删除&#xff0c;辅音字母就在其前面增加一个.&#xff0c;且所有字母输出都是小写。 思路分析 将输入的字符串改为字符数组&#xff0c;考虑到任意位置插入的情况&#xff0c;所以主要选择Lin…

C++的类和对象入门

目录 目录 目录 一、类 1.1类的定义 1.2访问限定符 1.3类域 1.4类的命名规范 1.5class和struct的默认访问权限 二、类的实例化 2.2对象的大小和存储 2.3空类的大小 三、this指针 3.1this指针的定义 3.2this指针的作用 3.2.1区分同名变量和局部变量 3.2.2返回对象…

Lesson 2 Breakfast or lunch?

Lesson 2 Breakfast or lunch? 词汇 until prep. 直到…… 用法&#xff1a;not until 时间点 直到……才 区别&#xff1a;untiltill 意思相同 例句&#xff1a;直到午夜&#xff0c;我才睡着。    I did not fall in asleep until midnight.    她直到50多岁才结婚…

PHP的学习

PHP的基础前提【HTML、CSS】 第一步先进行VS cood的下载&#xff1a;Visual Studio Code - Code Editing. Redefined 【选择适合自己的电脑的版本eg:我就是64位的win】

Qt 中signals和slots、Q_SIGNAL和Q_LOT、Q_SIGNALS和Q_SLOTS的区别和使用

Qt 中signals和slots、Q_SIGNAL和Q_SLOT、Q_SIGNALS和Q_SLOTS的区别和使用 1.signals和slots 信号和槽函数需要在类的声明中明确声明。信号需要使用signals关键字&#xff0c;而槽函数可以使用slots关键字&#xff08;虽然在现代Qt中&#xff0c;槽函数也可以直接作为普通成员…

人工智能之数学基础:线性代数中的特殊矩阵

本文重点 矩阵是数学中一个重要的工具,在各个领域都有广泛的应用。其中,一些特殊矩阵由于具有独特的性质,在特定的问题中发挥着关键作用。 单位矩阵 单位矩阵是一种特殊的方阵,在矩阵乘法中起到类似于数字 “1” 的作用。对于一个的单位矩阵,其主对角线元素全为 1,其余…

【愚公系列】《Python网络爬虫从入门到精通》037-文件的存取

标题详情作者简介愚公搬代码头衔华为云特约编辑,华为云云享专家,华为开发者专家,华为产品云测专家,CSDN博客专家,CSDN商业化专家,阿里云专家博主,阿里云签约作者,腾讯云优秀博主,腾讯云内容共创官,掘金优秀博主,亚马逊技领云博主,51CTO博客专家等。近期荣誉2022年度…

js基础二

JavaScript基础下 1 事件处理 JS 事件&#xff08;event&#xff09;是当用户与网页进行交互时发生的事情&#xff0c;例如单机某个链接或按钮、在文本框中输入文本、按下键盘上的某个按键、移动鼠标等等。当事件发生时&#xff0c;您可以使用 JavaScript 中的事件处理程序&a…