堆排序、大顶堆和小顶堆原理介绍
引言
堆(Heap)是一种特殊的完全二叉树结构,广泛应用于计算机科学中,特别是在排序算法和优先队列的实现中。堆可以分为大顶堆(Max Heap)和小顶堆(Min Heap)。本文将详细介绍堆排序的原理,以及大顶堆和小顶堆的概念、特性,并通过具体例子更好地理解这些概念。

1. 堆的基本概念
1.1 定义
堆是一种特殊的完全二叉树,满足以下性质:
- 完全二叉树:除了最后一层外,其他每一层都被完全填充,并且所有节点都尽可能靠左排列。
- 堆序性:每个节点与其子节点之间满足特定的关系,具体取决于堆的类型(大顶堆或小顶堆)。
1.2 大顶堆(Max Heap)
在大顶堆中,每个节点的值都不小于其子节点的值。因此,堆顶(根节点)始终是堆中的最大元素。
性质
对于任意节点 iii,如果它有左子节点 2i+12i+12i+1 和右子节点 2i+22i+22i+2,则满足:
- A[i]≥A[2i+1]A[i] \geq A[2i+1]A[i]≥A[2i+1]
- A[i]≥A[2i+2]A[i] \geq A[2i+2]A[i]≥A[2i+2]
示例
考虑一个数组 [4, 10, 3, 5, 1],我们将其构建成一个大顶堆:
- 初始数组:
[4, 10, 3, 5, 1] - 构建大顶堆:
- 从最后一个非叶子节点开始调整(即索引为 ⌊n2⌋−1\lfloor \frac{n}{2} \rfloor - 1⌊2n⌋−1 的节点),这里是索引 1。
- 调整索引 1 的节点
10,它已经满足大顶堆的条件,不需要调整。 - 调整索引 0 的节点
4,它不满足大顶堆的条件,需要与较大的子节点10交换。
最终构建的大顶堆如下:
10
/ \
5 3
/ \
4 1
对应的数组表示为 [10, 5, 3, 4, 1]。
1.3 小顶堆(Min Heap)
在小顶堆中,每个节点的值都不大于其子节点的值。因此,堆顶(根节点)始终是堆中的最小元素。
性质
对于任意节点 iii,如果它有左子节点 2i+12i+12i+1 和右子节点 2i+22i+22i+2,则满足:
- A[i]≤A[2i+1]A[i] \leq A[2i+1]A[i]≤A[2i+1]
- A[i]≤A[2i+2]A[i] \leq A[2i+2]A[i]≤A[2i+2]
示例
考虑相同的数组 [4, 10, 3, 5, 1],我们将其构建成一个小顶堆:
- 初始数组:
[4, 10, 3, 5, 1] - 构建小顶堆:
- 从最后一个非叶子节点开始调整(即索引为 ⌊n2⌋−1\lfloor \frac{n}{2} \rfloor - 1⌊2n⌋−1 的节点),这里是索引 1。
- 调整索引 1 的节点
10,它不满足小顶堆的条件,需要与较小的子节点1交换。 - 调整索引 0 的节点
4,它不满足小顶堆的条件,需要与较小的子节点1交换。
最终构建的小顶堆如下:
1
/ \
4 3
/ \
10 5
对应的数组表示为 [1, 4, 3, 10, 5]。
2. 堆排序原理
2.1 步骤
- 建堆:将输入数组构建成一个大顶堆或小顶堆。
- 排序:
- 大顶堆排序:每次取出堆顶的最大元素,将其与堆的最后一个元素交换,然后重新调整剩余元素为大顶堆,重复此过程直到所有元素都被排序。
- 小顶堆排序:每次取出堆顶的最小元素,将其与堆的最后一个元素交换,然后重新调整剩余元素为小顶堆,重复此过程直到所有元素都被排序。
2.2 图示示例
大顶堆排序示例
考虑一个无序数组 [4, 10, 3, 5, 1],我们通过大顶堆进行排序:
-
建堆:首先将数组构建成一个大顶堆。
Initial array: [4, 10, 3, 5, 1] After building Max Heap: 10 / \ 5 3 / \ 4 1 -
排序:
- 第一次交换:将堆顶
10与最后一个元素1交换,得到[1, 5, 3, 4, 10],然后调整剩余部分[1, 5, 3, 4]为大顶堆。 - 第二次交换:将新的堆顶
5与最后一个元素4交换,得到[1, 4, 3, 5, 10],然后调整剩余部分[1, 4, 3]为大顶堆。 - 第三次交换:将新的堆顶
4与最后一个元素3交换,得到[1, 3, 4, 5, 10],然后调整剩余部分[1, 3]为大顶堆。 - 第四次交换:将新的堆顶
3与最后一个元素1交换,得到[1, 3, 4, 5, 10],此时堆已为空,排序完成。
- 第一次交换:将堆顶
最终排序结果:
Sorted array: [1, 3, 4, 5, 10]
小顶堆排序示例
类似地,我们可以使用小顶堆对同一个数组进行排序。步骤相同,只是每次取出堆顶的最小元素进行交换。
3. 堆操作实例图示
为了更直观地理解大顶堆和小顶堆的操作过程,我们将通过图示展示插入和删除操作的具体步骤。
3.1 大顶堆操作实例
插入操作
假设我们有一个大顶堆 [10, 8, 3, 4, 1, 5],现在要插入元素 8:
-
插入新元素:将
8添加到堆的末尾。10 / \ 8 3 / \ / 4 1 5 \ 8 -
上浮调整:比较
8和它的父节点3,发现8 > 3,交换它们的位置。10 / \ 8 8 / \ / 4 1 5 \ 3 -
继续上浮调整:比较
8和它的新父节点8,发现8 == 8,停止调整。
最终结果:
10
/ \
8 8
/ \ /
4 1 5
\
3
删除操作
假设我们有一个大顶堆 [10, 8, 3, 4, 1, 5],现在要删除堆顶元素 10:
-
删除堆顶元素:将堆顶
10与堆的最后一个元素5交换,然后移除最后一个元素10。5 / \ 8 3 / \ 4 1 -
下沉调整:比较
5和它的子节点8和3,发现8 > 5,交换它们的位置。8 / \ 5 3 / \ 4 1 -
继续下沉调整:比较
5和它的新子节点4和1,发现5已经满足大顶堆的条件,调整结束。
最终结果:
8
/ \
5 3
/ \
4 1
3.2 小顶堆操作实例
插入操作
假设我们有一个小顶堆 [1, 2, 3, 10, 5, 4],现在要插入元素 2:
-
插入新元素:将
2添加到堆的末尾。1 / \ 2 3 / \ 10 5 \ 4 \ 2 -
上浮调整:比较
2和它的父节点3,发现2 < 3,交换它们的位置。1 / \ 2 2 / \ 10 5 \ 4 \ 3 -
继续上浮调整:比较
2和它的新父节点2,发现2 == 2,停止调整。
最终结果:
1
/ \
2 2
/ \
10 5
\
4
\
3
删除操作
假设我们有一个小顶堆 [1, 2, 3, 10, 5, 4],现在要删除堆顶元素 1:
-
删除堆顶元素:将堆顶
1与堆的最后一个元素4交换,然后移除最后一个元素1。4 / \ 2 3 / \ 10 5 -
下沉调整:比较
4和它的子节点2和3,发现2 < 4,交换它们的位置。2 / \ 4 3 / \ 10 5 -
继续下沉调整:比较
4和它的新子节点10和5,发现4已经满足小顶堆的条件,调整结束。
最终结果:
2
/ \
4 3
/ \
10 5
4. 时间复杂度分析
- 建堆时间复杂度:O(n)O(n)O(n),其中 nnn 是数组长度。
- 插入/删除操作时间复杂度:单次操作的时间复杂度为 O(logn)O(\log n)O(logn),因为每次调整最多需要遍历树的高度。
- 堆排序时间复杂度:总体时间复杂度为 O(nlogn)O(n \log n)O(nlogn),因为每次删除堆顶元素后需要调整堆。
5. 应用场景
5.1 堆排序
堆排序适用于需要稳定排序性能的场景,尤其是在内存有限的情况下,因为它只需要常数级别的额外空间。
5.2 优先队列
堆常用于实现优先队列,特别是大顶堆用于最大优先队列,小顶堆用于最小优先队列。
5.3 数据流处理
堆可用于实时数据流处理中的Top K问题,例如找到前K个最大的元素或最小的元素。
6. 总结
堆是一种高效的完全二叉树结构,广泛应用于排序和优先队列的实现中。大顶堆和小顶堆分别用于维护最大值和最小值的优先级。堆排序通过建堆和反复调整堆顶元素,实现了稳定的 O(nlogn)O(n \log n)O(nlogn) 排序性能。

被折叠的 条评论
为什么被折叠?



