一、 堆的基本概念
堆是一种完全二叉树,可以分为两种类型:
- 大顶堆(Max Heap):每个结点的值都大于或等于其左右子结点的值。堆顶(根节点)元素是整个堆中最大的。
- 小顶堆(Min Heap):每个结点的值都小于或等于其左右子结点的值。堆顶(根节点)元素是整个堆中最小的。
堆排序通常使用大顶堆来实现升序排序。排序时,首先构建大顶堆,然后将堆顶元素与数组最后一个元素交换,再对剩余的部分重新构建大顶堆,如此循环,直到整个序列有序。
二、 堆排序的主要步骤:
1. 构建大顶堆(Max-Heap)
堆排序首先需要将输入的无序数组调整为一个大顶堆(Max-Heap)。大顶堆的特点是每个父节点的值都大于或等于其左右子节点的值。通过这个过程,我们可以确保堆顶(根节点)的元素是当前堆中最大的。
- 具体步骤:
- 从最后一个非叶子节点开始,对每个节点进行“堆化”(heapify)操作,直到根节点。堆化的作用是确保每个子树的结构满足堆的性质。
- 为什么从最后一个非叶子节点开始?
因为叶子节点本身就是堆,只有非叶子节点可能不满足堆的性质,因此从最后一个非叶子节点开始堆化,可以确保堆化时每个子树都已经是有效的堆。
n
,那么最后一个非叶子节点的索引为(n // 2) - 1
。我们从该节点开始,依次进行堆化,直到堆化到根节点。 - 堆化(Heapify):
- 堆化的目标是,确保一个父节点大于其子节点。
- 对于某个父节点,比较其左右子节点,选出较大的子节点,将父节点与较大的子节点交换位置,递归继续向下堆化,直到堆的性质满足。
2. 排序过程
构建好大顶堆后,我们进入排序阶段。此时,堆顶的元素是当前数组中的最大元素。我们将堆顶元素与数组的最后一个元素交换位置,将最大元素“移出”堆外。然后缩小堆的范围,并对新的堆顶元素执行堆化操作,保证堆顶元素仍然是最大值。重复这个过程,直到堆的大小缩小到 1。
- 具体步骤:
- 将堆顶元素(最大元素)与数组的最后一个元素交换。
- 将堆的大小减 1(即排好序的部分不再参与堆化)。
- 对堆顶元素执行堆化操作,保证剩下的部分仍然是大顶堆。
- 重复步骤 1 至步骤 3,直到堆的大小为 1。
3. 堆化的详细过程
堆化是堆排序中非常重要的一步。它的目标是从某个节点开始,调整树的结构使其满足堆的性质(即父节点的值大于或等于子节点的值)。堆化的过程可以分为以下几个步骤:
- 比较当前节点和其左右子节点的值,找到三者中最大的一个。
- 如果当前节点的值已经大于或等于其左右子节点的值,则堆化过程结束。
- 如果当前节点的值小于其子节点,则将当前节点与最大子节点交换。
- 交换后,需要继续对交换后的子树进行堆化操作,确保堆的性质。
4. 堆排序的完整步骤总结
- 构建大顶堆:
- 从最后一个非叶子节点开始,依次对每个节点进行堆化,构建大顶堆。
- 排序过程:
- 将堆顶元素与数组的最后一个元素交换,减小堆的大小。
- 对新的堆顶元素执行堆化,恢复堆的性质。
- 重复上述过程直到堆的大小为 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) # 输出排序后的数组
总结
我们可以总结堆排序的关键步骤:
- 构建大顶堆:
- 从最后一个非叶节点开始,依次对节点进行堆化,构建一个大顶堆。
- 排序过程:
- 将堆顶(最大值)与数组末尾交换,把最大值固定到正确位置;
- 缩小堆的有效范围,对新的堆顶进行堆化,确保剩余部分仍然是大顶堆;
- 重复以上过程直到排序完成。
评论留言
欢迎您,!您可以在这里畅言您的的观点与见解!