堆的数据结构近似一个完全二叉树结构,同时满足堆积的性质,即子节点的键值或索引总是大于或小于它的父节点。所以堆必须满足两个条件:
(1)必须是一个完全二叉树结构
(2)子节点的键值或索引总是大于或小于它的父节点
首先来简单介绍一下什么是完全二叉树结构;完全二叉树结构的每个节点都有只是两条连接线,从上到下,从左到右依次生成如图1;如果我们想添加一个节点,只能添加到F节点之下,如果添加到G节点之下将不满足全二叉树结构的特点(即从上到下,从左到右依次生成)
图1
再来看看子节点的键值或索引总是大于或小于它的父节点这一个条件;图2满足条件(1)所以它是一个完全二叉树;9做为父节点大于6,5;6做为父节点大于5,2;5做为父节点大于1;因此图2同时满足条件(1)条件(2),这种结构就是堆
具体代码如下:
def heapify(arr, n, i):
# 初始化最大值为根节点
largest = i
left = 2 * i + 1 # 计算左节点位置
right = 2 * i + 2 # 计算右节点位置
# 如果左子节点大于根节点,则更新最大值
if left < n and arr[i] < arr[left]:
largest = left
# 如果右子节点大于根节点,则更新最大值
if right < n and arr[largest] < arr[right]:
largest = right
# 如果最大值不是根节点,则交换根节点和最大值,并递归调整堆
if largest != i:
arr[i], arr[largest] = arr[largest], arr[i]
heapify(arr, n, 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[i], arr[0] = arr[0], arr[i]
heapify(arr, i, 0)
return arr
print(heap_sort([5, 2, 9, 1, 5, 6]))
# 输出 [1, 2, 5, 5, 6, 9]
代码解释:
(1)有这样一组数列,现在我们对其进行递增排序
(2) 根据原始数据生成一个完全二叉树
(3)根据父节点一定大于子节点这一特点,所以父节点9就是原始数据的最大值,这时将完全二叉树的最后一个数也就是1与父节点9进行交换
(4)将9这个数据分支砍掉,然后放入数列
(5) 将父节点1,先与左节点比较,因为1<6,所以两者交换位置
(6) 交换后再看1为父节点的分支,因为1<5,所以两者交换位置
(7) 这时父节点6做为完全二叉树的最大数6,将其与完全二叉树的最后一个数也就是2与父节点6进行交换
(8)将这个6数据分支砍掉,然后放入数列
(9)将父节点2,先与左节点比较,因为2<5,所以两者交换位置;交换后再看2为父节点的分支,因为2>1,所以两者不需要交换位置
(10) 这时父节点5做为完全二叉树的最大数,将其与完全二叉树的最后一个数也就是1与父节点5进行交换
(11)将5这个数据分支砍掉,然后放入数列
(12) 将父节点1,先与左节点比较,因为1<2,所以两者交换位置
(13) 将父节点2,先与左节点比较,因为2>1,所以两者不需要交换位置;再与右节点比较,因为2<5,所以两者交换位置
(14) 这时父节点5做为完全二叉树的最大数,将其与完全二叉树的最后一个数也就是12与父节点5进行交换
(15)将5这个数据分支砍掉,然后放入数列
(16) 此时堆里只剩下2和1,先放入父节点,再放入子节点,得到最后的排序结果
(17) 至此排序结束,返回结果
小结:
堆排序是一种高效的排序算法,时间复杂度为O(n log n),适用于大规模数据的排序。它特别适合于数据流排序,即数据是逐个到达的,不需要一次性加载所有数据。
注意事项
- 堆排序是不稳定的排序算法,即相等的元素可能会改变它们的相对顺序。
- 堆排序的空间复杂度为O(1),因为它是在原地进行排序,不需要额外的存储空间。
- 堆排序适用于数据量较大的情况,对于小规模数据,其他排序算法(如快速排序、插入排序等)可能更高效。
平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | |
堆排序算法 | O(n log n) | O(n log n) | O(1) |