堆排序

95°C 23-03-2025 notbyai
最近更新于:2025-03-23 17:23:36

一、 堆的基本概念

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

  • 大顶堆(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. 构建大顶堆
  • 从最后一个非叶节点开始,依次对节点进行堆化,构建一个大顶堆。
  1. 排序过程
  • 将堆顶(最大值)与数组末尾交换,把最大值固定到正确位置;
  • 缩小堆的有效范围,对新的堆顶进行堆化,确保剩余部分仍然是大顶堆;
  • 重复以上过程直到排序完成。

评论留言

欢迎您,!您可以在这里畅言您的的观点与见解!

0 条评论