1、堆
堆在形式上是一棵完全二叉树(假设树的高度是h,所有的叶子结点都是在第h层或h-1层)
堆分为两类,大根堆和小根堆:
- 大根堆:每个结点的值都大于等于其孩子结点的值;
- 小根堆:每个结点的值都小于等于其孩子结点的值;
堆在形式上是一棵完全二叉树,用数组存储它,不会浪费空间;
2、堆排序的过程(大根堆)
堆排序中最关键的操作是将序列调整为堆。
(1)从无序序列所确定的完全二叉树的第一个非叶子结点开始,从右到左,从下到上,对每个结点进行调整,最终得到一个大根堆。
对结点的调整方法:
1)将当前结点(假设为a)的值与它的孩子结点进行比较,如果存在大于a的孩子结点,则从中选出最大的一个与a交换。
2)当a来到下一层的时候重复 1)过程,直到a的孩子结点值都小于a为止。
(2)将根结点(假设为c)与无序序列中最后一个元素(假设为d)交换,c进入有序序列,到达最终位置。此时只有结点d可能不满足堆,对其进行调整。
(3)重复(2),直到无序序列只剩下1个时,排序结束。
3、算法实现
def sift(arr, i, n):
j = 2*i + 1
temp = arr[i]
while j < n:
if j + 1 < n and arr[j] < arr[j+1]:
j += 1
if temp >= arr[j]:
break
arr[i] = arr[j]
i = j
j = 2*i+1
arr[i] = temp
def heap_sort(arr):
if not arr or len(arr) <= 1:
return
n = len(arr)
for i in range(n//2-1, -1, -1): # 建堆
sift(arr, i, n)
for i in range(n, 1, -1):
arr[0], arr[i-1] = arr[i-1], arr[0] # 堆顶和最后一个无序元素交换位置
sift(arr, 0, i-1) # 调整堆
if __name__ == '__main__':
a = [10, 1, 5, 2, 4, 3, 2, 1]
# a = [5, 8, 7, 6, 3, 2, 1]
heap_sort(a)
print(a)
4、复杂度分析
(1)时间复杂度分析
时间复杂度为。
对于sift() 函数,走了当前结点到叶子结点的路径,完全二叉树的高度为,即对每个结点调整的时间复杂度为
。对于heap_sort()函数,第1个循环的基本操作次数约为
,第2个循环的基本操作次数约为
,所以整个算法的基本操作次数为
,化简后得其时间复杂度为
。
堆排序在最坏情况下,时间复杂度也为,这是相对于快速排序的最大优点。
(2)空间复杂度分析
空间复杂度为O(1)。
额外空间有一个temp,所以空间复杂度为O(1)。
五、适用场景
堆排序适用于序列数比较多的场景,比如从1万个序列中选出前10个。
不适合用于序列小的场景。原因主要在于构建初始堆时,对于每个元素都需要进行一定数量的比较操作,以确保满足堆的性质。在数据量较小的情况下,这种比较与交换的操作成本相对较高,导致堆排序在处理少量数据时相比其他排序算法可能不是最优选择。
PS:堆排序是一种不稳定的排序算法。