堆排序原理与实现详解

我们来详细、系统地解释一下**堆排序**。

堆排序是一种基于**二叉堆**数据结构的比较类排序算法。它以其原址排序(只需要常数级别的额外空间)和O(n log n)的时间复杂度而闻名。

为了理解堆排序,我们需要分步拆解几个核心概念。

  1. 什么是"堆"?

这里的"堆"不是指内存管理中的堆,而**是一种特殊的**完全二叉树**。**它必须满足以下性质之一:

* **大顶堆 **:每个节点的值都**大于或等于**其左右子节点的值。

* 推论:堆顶(根节点)是整个堆中的**最大元素**。

* **小顶堆 **:每个节点的值都**小于或等于**其左右子节点的值。

* 推论:堆顶(根节点)是整个堆中的**最小元素**。

在堆排序中,我们通常使用**大顶堆**。

**重要特性**:堆通常用**数组**来存储。对于一个给定下标 `i` 的节点:

* 其父节点下标:`(i-1) / 2`

* 其左子节点下标:`2*i + 1`

* 其右子节点下标:`2*i + 2`

例如,下面是一个大顶堆及其数组表示:

```

90

/ \

80\] \[70

/ \ /

40\] \[30\]\[20

```

数组:`[90, 80, 70, 40, 30, 20]`


  1. 堆排序的核心思想

堆排序的巧妙之处在于利用了大顶堆(或小顶堆)的堆顶是最大(或最小)值这一特性。其基本思想可以概括为两个步骤:

  1. **建堆**:将一个无序的数组构建成一个大顶堆。

  2. **排序**:不断地将堆顶元素(当前最大值)与堆的末尾元素交换,然后缩小堆的范围,并对新的堆顶元素进行"下沉"操作,以重新维持堆的性质。重复此过程,直到堆中只剩下一个元素。


3. 堆排序的详细步骤

我们以数组 `[4, 10, 3, 5, 1]` 的升序排序为例(使用大顶堆)。

步骤一:构建大顶堆

目标是重新排列数组元素,使其满足大顶堆的性质。

  1. **从最后一个非叶子节点开始**。最后一个非叶子节点的下标是 `n/2 - 1` (n是数组长度)。这里 `n=5`,所以从下标 `5/2 - 1 = 1` 开始(即元素 `10`)。

  2. **进行"下沉"操作**:

* **"下沉"**:对于一个节点,如果它比它的子节点小,就将它与较大的那个子节点交换,并继续向下比较,直到它大于等于它的所有子节点,或者成为叶子节点。

  1. **从下到上,从右到左地对所有非叶子节点执行"下沉"**

* 处理下标1(元素10):它的子节点是下标3(5)和4(1)。10比它们都大,无需下沉。堆状态:`[4, 10, 3, 5, 1]`

* 处理下标0(元素4):它的子节点是下标1(10)和2(3)。4 < 10,所以与10交换。交换后,下标1变成了4。现在4的子节点是下标3(5)和4(1)。4 < 5,所以与5交换。交换后,下标3变成了4,它已经是叶子节点,停止。

* 现在数组变成了:`[10, 5, 3, 4, 1]`。检查一下,这已经是一个大顶堆了。

```

10

/ \

5\] \[3

/ \

4\] \[1

```

步骤二:排序

现在,堆顶元素(下标0)是最大值。

  1. **第一次交换与下沉**:

* 将堆顶元素(10)与当前堆的最后一个元素(1)交换。交换后,数组为 `[1, 5, 3, 4, 10]`。此时,`10` 已经位于其最终的正确位置。我们将堆的大小减1(现在有效的堆范围是前4个元素 `[1, 5, 3, 4]`)。

* 对新的堆顶元素 `1` 进行**下沉**操作,以重新构建大顶堆。

* 1的子节点是5和3。5更大,所以1和5交换。数组变为 `[5, 1, 3, 4, 10]`。

* 现在1(在下标1)的子节点是4(在下标3)。1 < 4,所以交换。数组变为 `[5, 4, 3, 1, 10]`。现在又形成了一个有效的大顶堆(范围在前4个元素)。

  1. **重复此过程**:

* **第二次**:将堆顶(5)与当前堆的最后一个元素(1)交换。数组变为 `[1, 4, 3, 5, 10]`。`5` 和 `10` 都在最终位置。堆大小减1(有效堆是 `[1, 4, 3]`)。

* 对 `1` 进行下沉:1的子节点是4和3,与4交换。数组变为 `[4, 1, 3, 5, 10]`。

* **第三次**:将堆顶(4)与当前堆的最后一个元素(3)交换。数组变为 `[3, 1, 4, 5, 10]`。`4`, `5`, `10` 在最终位置。堆大小减1(有效堆是 `[3, 1]`)。

* 对 `3` 进行下沉:3的子节点是1,无需交换。

* **第四次**:将堆顶(3)与当前堆的最后一个元素(1)交换(也就是自己和自己交换)。数组变为 `[1, 3, 4, 5, 10]`。排序完成。

最终,我们得到了升序排列的数组:`[1, 3, 4, 5, 10]`。


  1. 算法复杂度分析

* **时间复杂度**:**O(n log n)**

* **建堆过程**:看似是O(n log n),但通过精细分析,其平均时间复杂度可以达到**O(n)**。

* **排序过程**:需要进行n-1次循环,每次循环中主要操作是堆顶的"下沉",下沉操作的时间复杂度与树高有关,即O(log n)。所以排序过程的时间复杂度是**O(n log n)**。

* 综合起来,堆排序的**最好、最坏、平均**时间复杂度都是 **O(n log n)**。这是一个非常稳定的性能。

* **空间复杂度**:**O(1)**

* 因为堆排序是**原址排序**,所有操作都在原数组上进行,只使用了常数级别的临时变量。


  1. 优缺点

**优点**:

* 时间复杂度稳定在O(n log n),效率高。

* 空间复杂度低,是原址排序。

**缺点**:

* 算法不稳定(相等的元素在排序后相对位置可能会改变)。

* 在数据量较小的情况下,其常数因子可能使得性能不如快速排序或归并排序。

* 由于内存访问模式比较跳跃(不像快速排序那样局部顺序访问),对CPU缓存不友好。


总结

堆排序是一种高效且节省内存的排序算法。它的核心在于:

  1. **将待排序序列构造成一个大顶堆**。

  2. **不断取出堆顶的最大值,并将其放到序列末尾,然后调整剩余部分使其保持堆结构**。

理解堆排序的关键在于理解**堆的数据结构**和**下沉操作**。

例子构建数字序列 **825479** 的大顶堆。

1. 将数组视为完全二叉树

首先,我们将数组 `[8, 2, 5, 4, 7, 9]` 看作一个完全二叉树:

```

8

/ \

2 5

/ \ /

4 7 9

```

数组索引对应关系:

  • 索引0: 8

  • 索引1: 2

  • 索引2: 5

  • 索引3: 4

  • 索引4: 7

  • 索引5: 9

2. 构建大顶堆的过程

**大顶堆性质**:每个节点的值都大于或等于其子节点的值。

我们从最后一个非叶子节点开始向前调整(最后一个非叶子节点下标 = n/2 - 1 = 6/2 - 1 = 2)。

步骤1:调整索引2(值为5)

节点5的子节点是9(索引5),5 < 9,需要交换:

```

交换前:

5

/

9

交换后:

9

/

5

```

数组变为:`[8, 2, 9, 4, 7, 5]`

树结构:

```

8

/ \

2 9

/ \ /

4 7 5

```

步骤2:调整索引1(值为2)

节点2的子节点是4和7,最大值是7,2 < 7,需要交换:

```

交换前:

2

/ \

4 7

交换后:

7

/ \

4 2

```

数组变为:`[8, 7, 9, 4, 2, 5]`

树结构:

```

8

/ \

7 9

/ \ /

4 2 5

```

步骤3:调整索引0(值为8)

节点8的子节点是7和9,最大值是9,8 < 9,需要交换:

```

交换前:

8

/ \

7 9

交换后:

9

/ \

7 8

```

数组变为:`[9, 7, 8, 4, 2, 5]`

树结构:

```

9

/ \

7 8

/ \ /

4 2 5

```

步骤4:检查交换后的影响

交换后,索引2的值变为8,需要检查是否需要继续调整:

节点8的子节点是5,8 > 5,不需要调整。

3. 最终的大顶堆

**最终数组**:`[9, 7, 8, 4, 2, 5]`

**树形结构**:

```

9

/ \

7 8

/ \ /

4 2 5

```

**验证大顶堆性质**:

  • 节点9 ≥ 子节点7和8 ✓

  • 节点7 ≥ 子节点4和2 ✓

  • 节点8 ≥ 子节点5 ✓

  • 叶子节点4、2、5没有子节点 ✓

原始数组 `[8, 2, 5, 4, 7, 9]` 经过堆化后,得到的大顶堆为 `[9, 7, 8, 4, 2, 5]`。

这个堆满足大顶堆的所有条件,堆顶元素9是整个堆中的最大值。如果要进行堆排序,接下来就可以开始不断取出堆顶元素进行排序了。

相关推荐
青岛少儿编程-王老师3 小时前
CCF编程能力等级认证GESP—C++6级—20250927
java·c++·算法
一人の梅雨3 小时前
1688 拍立淘接口深度开发:从图像识别到供应链匹配的技术实现
人工智能·算法·计算机视觉
递归不收敛3 小时前
吴恩达机器学习课程(PyTorch适配)学习笔记:1.4 模型评估与问题解决
pytorch·学习·机器学习
SundayBear3 小时前
Qt 开发修炼指南:从入门到通透的实战心法
开发语言·qt·嵌入式
又是忙碌的一天3 小时前
前端学习 JavaScript
前端·javascript·学习
_给我学起来3 小时前
数据结构:树
数据结构
ooo-p3 小时前
FPGA学习篇——Verilog学习之计数器的实现
学习·fpga开发
A9better4 小时前
嵌入式开发学习日志37——stm32之USART
stm32·嵌入式硬件·学习
Miraitowa_cheems4 小时前
LeetCode算法日记 - Day 64: 岛屿的最大面积、被围绕的区域
java·算法·leetcode·决策树·职场和发展·深度优先·推荐算法