当前位置: 首页 > news >正文

详解堆排序(超详细)

堆排序

  • 一、 堆的基本概念
  • 二、 堆排序的主要步骤:
    • 1. 构建大顶堆(Max-Heap)
    • 2. 排序过程
    • 3. 堆化的详细过程
    • 4. 堆排序的完整步骤总结
    • 5. 堆排序的时间复杂度
    • 6. 堆排序的空间复杂度
  • 三、 举例讲解
    • 示例数组
    • 第一步:构建大顶堆
      • 1.1 找到最后一个非叶子节点
      • 1.2 从索引 1 开始堆化
      • 1.3 接下来堆化索引 0
      • 1.4 检验并判断是否需要重新堆化
      • 1.5 构建大顶堆结果
    • 第二步:排序过程
      • 2.1 第一次交换
        • 堆化过程:
      • 2.2 第二次交换
        • 堆化过程:
      • 2.3 第三次交换
        • 堆化过程:
      • 2.4 第四次交换
    • 最终排序结果
  • 四、 Python 实现示例
  • 总结

一、 堆的基本概念

堆是一种完全二叉树,可以分为两种类型:

  • 大顶堆(Max Heap):每个结点的值都大于或等于其左右子结点的值。堆顶(根节点)元素是整个堆中最大的。
  • 小顶堆(Min Heap):每个结点的值都小于或等于其左右子结点的值。堆顶(根节点)元素是整个堆中最小的。

堆排序通常使用大顶堆来实现升序排序。排序时,首先构建大顶堆,然后将堆顶元素与数组最后一个元素交换,再对剩余的部分重新构建大顶堆,如此循环,直到整个序列有序。


二、 堆排序的主要步骤:

1. 构建大顶堆(Max-Heap)

堆排序首先需要将输入的无序数组调整为一个大顶堆(Max-Heap)。大顶堆的特点是每个父节点的值都大于或等于其左右子节点的值。通过这个过程,我们可以确保堆顶(根节点)的元素是当前堆中最大的。

  • 具体步骤
    • 从最后一个非叶子节点开始,对每个节点进行“堆化”(heapify)操作,直到根节点。堆化的作用是确保每个子树的结构满足堆的性质。
    • 为什么从最后一个非叶子节点开始?
      因为叶子节点本身就是堆,只有非叶子节点可能不满足堆的性质,因此从最后一个非叶子节点开始堆化,可以确保堆化时每个子树都已经是有效的堆。

假设数组的长度是 n,那么最后一个非叶子节点的索引为 (n // 2) - 1。我们从该节点开始,依次进行堆化,直到堆化到根节点。

  • 堆化(Heapify)
    • 堆化的目标是,确保一个父节点大于其子节点。
    • 对于某个父节点,比较其左右子节点,选出较大的子节点,将父节点与较大的子节点交换位置,递归继续向下堆化,直到堆的性质满足。

2. 排序过程

构建好大顶堆后,我们进入排序阶段。此时,堆顶的元素是当前数组中的最大元素。我们将堆顶元素与数组的最后一个元素交换位置,将最大元素“移出”堆外。然后缩小堆的范围,并对新的堆顶元素执行堆化操作,保证堆顶元素仍然是最大值。重复这个过程,直到堆的大小缩小到 1。

  • 具体步骤
    1. 将堆顶元素(最大元素)与数组的最后一个元素交换。
    2. 将堆的大小减 1(即排好序的部分不再参与堆化)。
    3. 对堆顶元素执行堆化操作,保证剩下的部分仍然是大顶堆。
    4. 重复步骤 1 至步骤 3,直到堆的大小为 1。

3. 堆化的详细过程

堆化是堆排序中非常重要的一步。它的目标是从某个节点开始,调整树的结构使其满足堆的性质(即父节点的值大于或等于子节点的值)。堆化的过程可以分为以下几个步骤:

  1. 比较当前节点和其左右子节点的值,找到三者中最大的一个。
  2. 如果当前节点的值已经大于或等于其左右子节点的值,则堆化过程结束。
  3. 如果当前节点的值小于其子节点,则将当前节点与最大子节点交换。
  4. 交换后,需要继续对交换后的子树进行堆化操作,确保堆的性质。

4. 堆排序的完整步骤总结

  1. 构建大顶堆
    • 从最后一个非叶子节点开始,依次对每个节点进行堆化,构建大顶堆。
  2. 排序过程
    • 将堆顶元素与数组的最后一个元素交换,减小堆的大小。
    • 对新的堆顶元素执行堆化,恢复堆的性质。
    • 重复上述过程直到堆的大小为 1。

5. 堆排序的时间复杂度

  • 构建堆:需要进行 n / 2 次堆化,每次堆化的时间复杂度为 O(log n),所以构建大顶堆的时间复杂度为 O(n)。
  • 排序过程:每次将堆顶元素与最后一个元素交换后,都需要对剩余的部分执行堆化。每次堆化的时间复杂度为 O(log n),一共需要执行 n - 1 次交换,因此排序过程的时间复杂度为 O(n log n)。

因此,堆排序的总时间复杂度为 O(n log n),这是一个较为稳定的时间复杂度。

6. 堆排序的空间复杂度

堆排序是原地排序算法,除了原始数组外,不需要额外的存储空间。所以堆排序的空间复杂度为 O(1)


三、 举例讲解

示例数组

假设初始数组为:

[4, 10, 3, 5, 1]

我们目标是使用大顶堆来排序,使得排序后得到升序排列的数组。

第一步:构建大顶堆

1.1 找到最后一个非叶子节点

对于长度 n=5 的数组,最后一个非叶子节点索引为

floor(n/2) - 1 = floor(5/2) - 1 = 2 - 1 = 1

所以从索引 1 开始往前(包括索引 0)依次堆化。

1.2 从索引 1 开始堆化

索引 1 的元素是 10

  • 左子节点索引:2 * 1 + 1 = 3,对应元素 5
  • 右子节点索引:2 * 1 + 2 = 4,对应元素 1

比较:
10 与 5、1 比较,10 已经大于其两个子节点,无需交换。
堆化后子树保持:[10, 5, 1](索引 1 及其子节点)不变。

1.3 接下来堆化索引 0

索引 0 的元素是 4

  • 左子节点索引:2 * 0 + 1 = 1,对应元素 10
  • 右子节点索引:2 * 0 + 2 = 2,对应元素 3

比较:

  • 当前节点 4,与左子节点 10 和右子节点 3 比较,最大的值为 10(索引 1)。
  • 交换:将 4 与 10 交换,数组变为:
    [10, 4, 3, 5, 1]
    

1.4 检验并判断是否需要重新堆化

交换后,以原索引 1 的位置(现在值为 4)的子树需要重新堆化:

对于索引 1(当前值 4):

  • 左子节点索引:2 * 1 + 1 = 3,对应元素 5
  • 右子节点索引:2 * 1 + 2 = 4,对应元素 1
    比较:
  • 在 4、5、1 中,最大值是 5(索引 3),所以交换 4 与 5。
  • 交换后数组变为:
    [10, 5, 3, 4, 1]
    

索引 3(值 4)是叶子节点,无需继续堆化。

1.5 构建大顶堆结果

经过上述堆化过程,整个数组已经构建成大顶堆,数组状态为:

[10, 5, 3, 4, 1]
  • 根节点 10 为最大值
  • 对应的二叉树结构为:
            10
           /  \
          5    3
         / \
        4   1
    

第二步:排序过程

排序过程的目标是不断把堆顶(最大值)交换到数组末尾,然后对剩余部分重新堆化。

2.1 第一次交换

  • 交换堆顶与最后一个元素
    当前数组:[10, 5, 3, 4, 1]
    交换索引 0 与索引 4(最后一个元素):
    交换后数组变为:

    [1, 5, 3, 4, 10]
    
  • 说明:此时最大值 10 固定在最后位置,不参与后续堆化。

  • 对剩余堆(索引 0 到 3)重新堆化
    子数组为:[1, 5, 3, 4],堆的大小为 4。

堆化过程:
  • 从索引 0 开始堆化。
    索引 0 的元素:1

    • 左子节点索引:1,值为 5
    • 右子节点索引:2,值为 3
      比较:最大值为 5(索引 1)。
      交换:将 1 与 5 交换,数组变为:
    [5, 1, 3, 4, 10]
    

    继续堆化索引 1(值为 1):

    • 左子节点索引:3,值为 4
    • 右子节点索引:4,不在堆的范围内(堆大小为 4)
      比较:最大值为 4(索引 3)。
      交换:将 1 与 4 交换,数组变为:
    [5, 4, 3, 1, 10]
    

    索引 3 为叶子节点,堆化结束。

排序后,堆调整完成,堆为:[5, 4, 3, 1],根节点为最大值 5。

2.2 第二次交换

  • 交换堆顶与堆的最后一个元素
    当前数组:[5, 4, 3, 1, 10]
    交换索引 0 与索引 3:
    交换后数组变为:

    [1, 4, 3, 5, 10]
    
  • 说明:当前最大值 5 固定在正确位置。

  • 对剩余堆(索引 0 到 2)重新堆化
    子数组为:[1, 4, 3],堆大小为 3。

堆化过程:
  • 对索引 0(值 1)堆化:

    • 左子节点索引:1,值为 4
    • 右子节点索引:2,值为 3
      最大值为 4(索引 1)。
      交换:将 1 与 4 交换,数组变为:
    [4, 1, 3, 5, 10]
    

    对索引 1(值 1):

    • 左子节点索引:3,不在堆范围内(堆大小为 3)
      堆化结束。

排序后,堆调整完成,堆为:[4, 1, 3]。

2.3 第三次交换

  • 交换堆顶与堆的最后一个元素
    当前数组:[4, 1, 3, 5, 10]
    交换索引 0 与索引 2:
    交换后数组变为:

    [3, 1, 4, 5, 10]
    
  • 固定最大值 4 在位置索引 2。

  • 对剩余堆(索引 0 到 1)重新堆化
    子数组为:[3, 1],堆大小为 2。

堆化过程:
  • 对索引 0(值 3)堆化:
    • 左子节点索引:1,值为 1
      3 大于 1,无需交换。
      堆化结束。

排序后,堆调整完成,堆为:[3, 1]。

2.4 第四次交换

  • 交换堆顶与堆的最后一个元素
    当前数组:[3, 1, 4, 5, 10]
    交换索引 0 与索引 1:
    交换后数组变为:

    [1, 3, 4, 5, 10]
    
  • 此时只剩下一个元素,不需要再堆化。

最终排序结果

经过上述所有交换和堆化,数组最终变为:

[1, 3, 4, 5, 10]

这就是堆排序完成后的结果,数组按升序排列。


四、 Python 实现示例

以下是一个简单的 Python 实现示例,帮助更好地理解具体实现步骤:

# 堆调整函数
def heapify(arr, heap_size, i):
    # 初始化最大值为当前节点索引
    largest = i
    # 计算左子节点的索引
    left = 2 * i + 1
    # 计算右子节点的索引
    right = 2 * i + 2

    # 如果左子节点存在(索引小于堆大小),且左子节点的值大于当前最大值
    if left < heap_size and arr[left] > arr[largest]:
        # 更新最大值索引为左子节点
        largest = left

    # 如果右子节点存在(索引小于堆大小),且右子节点的值大于当前最大值
    if right < heap_size and arr[right] > arr[largest]:
        # 更新最大值索引为右子节点
        largest = right

    # 如果最大值不是当前节点(说明需要调整)
    if largest != i:
        # 交换当前节点和最大值节点
        arr[i], arr[largest] = arr[largest], arr[i]
        # 递归调整交换后的子树,以保持堆的性质
        heapify(arr, heap_size, largest)

# 堆排序函数
def heap_sort(arr):
    n = len(arr)  # 获取数组长度
    # 构建大顶堆
    for i in range(n // 2 - 1, -1, -1):
        # 从最后一个非叶子节点开始向上调整堆
        heapify(arr, n, i)
    
    # 排序过程
    for i in range(n - 1, 0, -1):
        # 将堆顶(最大值)与当前未排序部分的最后一个元素交换
        arr[0], arr[i] = arr[i], arr[0]
        # 对剩余未排序部分重新堆化,保持大顶堆性质
        heapify(arr, i, 0)

# 示例用法
if __name__ == "__main__":
    array = [12, 11, 13, 5, 6, 7]  # 定义一个待排序数组
    print("原始数组:", array)  # 输出原始数组
    heap_sort(array)  # 调用堆排序函数对数组进行排序
    print("排序后的数组:", array)  # 输出排序后的数组

总结

我们可以总结堆排序的关键步骤:

  1. 构建大顶堆

    • 从最后一个非叶节点开始,依次对节点进行堆化,构建一个大顶堆。
  2. 排序过程

    • 将堆顶(最大值)与数组末尾交换,把最大值固定到正确位置;
    • 缩小堆的有效范围,对新的堆顶进行堆化,确保剩余部分仍然是大顶堆;
    • 重复以上过程直到排序完成。

相关文章:

  • AI Tokenization
  • Docker 镜像构建与优化
  • 修形还是需要再研究一下
  • Maven中为什么有些依赖不用引入版本号
  • 尝试在软考61天前开始成为软件设计师-数据结构算法
  • 内核编程十:进程的虚拟地址空间
  • Docker Hub Mirror 终极解决方案——0成本,超高速!
  • RAG优化:python从零实现时间管理大师Self-RAG
  • 红宝书第十二讲:详解JavaScript中的工厂模式与原型模式等各种设计模式
  • 第八章:防火墙
  • [实操]Mysql8 InnoDB引擎主从配置三节点操作流程
  • 论文阅读笔记:Denoising Diffusion Implicit Models
  • 消息队列ActiveMQ、RabbitMQ、RocketMQ、Kafka对比分析和选型
  • 【杂谈】Godot4.4 对象池(附脚本)
  • 盖泽 寻边器 帮助类
  • 开发中后端返回下划线数据,要不要统一转驼峰?
  • 【HTML5游戏开发教程】零基础入门合成大西瓜游戏实战 | JS物理引擎+Canvas动画+完整源码详解
  • C# BULK INSERT导入大数据文件数据到SqlServer
  • centos7.9镜像源及Python引入ssl问题处理
  • OLED中英文混合显示
  • Meta一季度净利增长三成:上调全年资本支出,受关税影响亚洲出口电商广告支出减少
  • 美国务院宣布新一轮与伊朗相关的制裁
  • 十二届上海市委第六轮巡视全面进驻,巡视组联系方式公布
  • A股三大股指涨跌互现:3343股收涨,两市成交超1.1万亿元
  • 国有六大行一季度合计净赚超3444亿,不良贷款余额均上升
  • 金科股份:去年营收约275亿元,今年确保所有项目“零烂尾”