堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。
堆是一种类似于完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或者索引总是大于(小于)它的父节点。
二叉树的遍历
树的遍历:对树中所有元素不重复的访问一遍,也称作扫描。
遍历树的方式:
- 广度优先遍历
- 层序遍历
- 深度优先遍历
- 前序遍历
- 中序遍历
- 后序遍历
堆排序
- 堆是一个完全二叉树
- 每个非叶子结点都要大于或者等于其左右孩子结点的值的堆称为大顶堆
- 每个非叶子结点都要小于或者等于其左右孩子结点的值的堆称为小顶堆
- 根节点一定是大顶堆的最大值或者小顶堆的最小值
堆排序的实现:
堆排序可以按以下步骤来完成:
1.先构建大顶堆
为了更直观的看到大顶堆的实现过程,我们可以实现将一个数组先打印成一个二叉树的格式
def print_tree(src):
length = len(src)
k = math.ceil(math.log2(length+1))
index = 1
max_width = 2 * length
for i in range(k): # 0,1,2
for j in range(2**i):
print("{:^{}}".format(src[index],max_width),end = " ")
index += 1
if index >= length:
break
max_width = max_width // 2
print( )
src = [0,9,8,7,6,5,4,3,2,1]
print_tree(src)
9
8 7
6 5 4 3
2 1
有个这个二叉树打印函数,我们在下面的构建二叉树的过程中能更直观的看到大(小)顶堆的构建过程
我们可以先调整任意一个结点,使之满足大(小)顶堆对于任一结点的要求:对于大顶堆父节点大于或者等于他的儿子结点,对于小顶堆则相反。
def heap_adujst(n, i, src:list): # 传入需要调整的结点的编号和需要调整的数组
while n >= 2*i: # 说明一定有左节点
# 下面进行三个数值的比较
max_child_index = 2*i #假设这三个结点中最大的一个值为左节点,记住它的索引
right_child_index = 2*i+1 # 设定一个存在或否的右节点的索引位左节点的索引+1
if n > 2*i: # 说明一定存在右节点..即n >= 2*i
if src[right_child_index] > src[max_child_index]: # 如果右节点的值大于左节点
max_child_index = right_child_index #那么这两个结点的值交换
# 下面来比较父节点 i 和他的字节的最大值,并交换
if src[max_child_index] > src[i]: # 如果子结点的值大于父节点的值,就交换位置
src[max_child_index], src[i] = src[i], src[max_child_index]
# 下面这一步至关重要,交换i的位置,一直调整到跳出while循环,也就是待调整的i结点没有子结点位置
i = max_child_index # 这是本题的高效点
else: # 否则就说明当前跟结点是最大的,不需要调整,直接跳出循环
break
src = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
n = len(src) - 1
heap_adujst(n,1,src)
3
2 7
4 5 6 1
8 9
调整正确,现在我们可以愉快的构建大顶堆了
def adjust(n,src:list):
total_i = n // 2 # 得到这个数组的待调整的所有结点数的最后一个数
for i in range(total_i,0,-1): # 遍历所有结点,并调整
heap_adujst(n,i,src) # 这是本题的高效地方
return src # 最后的到一个大顶堆
src = list(range(10))
n = len(src) - 1
adjust(n,src)
9
8 7
4 5 6 1
2 3
大顶堆构建完成
2.取出当前大顶堆的索引1处的最大值和末尾的数进行交换,现在这个最大值就是位与有序区,然后将这个被破坏的大顶堆重新构建成一个新的大顶堆,重复以上步骤,注意边界条件。
# 排序
def sort(n,src):
adjust(n,src)
while n > 1:
src[1],src[n] = src[n], src[1]
n -= 1 # 每次交换完,有序区在不断增大,无序区在不断减小
if n == 2: # 边界条件
if src[2] >= src[1]:
break
heap_adujst(n,1,src) # 将大顶堆的最大值拿走,在调整最上面的一层三个数,每次调整都可以获得由一个最大值
return src
sort(n,src)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print_tree(src)
1
2 3
4 5 6 7
8 9
完成,与我们预想的结果一致…
用小顶堆实现的方法几乎一致,为了和上面的大顶堆构建过程区分下,下面使用小顶堆的方式来实现排序吧…
def heap_sort(src):
n = len(src) - 1 #n 表示总结点数, 包括叶子结点
def heap_adjust(n,i,src): # 中序遍历,i 表示结点的数字
while n >= 2 * i: # 如果这个结点有左子结点
min_child_index = 2*i
right_child_index = 2*i + 1
if n > 2*i: # 言外之意就是 n >= 2*i + 1
if src[right_child_index] < src[min_child_index]: # 如果右节点值小于左节点值
min_child_index = right_child_index
if src[min_child_index] < src[i]: # 子结点最大值小于根节点的值
src[min_child_index], src[i] =src[i], src[min_child_index]
i = min_child_index
else: # 说明当前结点满足条件,根结点为最小值,
break
return src # 都是在原src就地操作,占用空间很小
def adjust(src):
n = len(src)-1 # 结点总数,包括叶子结点
total_i = n // 2 # 需要处理的根节点数
for i in range(total_i,0,-1): # 使用中序遍历,倒着来,一直到1这个结点
heap_adjust(n,i,src)
return src
def sorte(src):
adjust(src)
n = len(src)-1
while n > 1:
src[1], src[n] = src[n], src[1] # 无序区的收尾交换数据
n -= 1
heap_adjust(n,1,src)
print_tree(src)
return src
return sorte(src)
heap_sort([0,1,2,3,4,5])
最后附上一张wikipedia的的堆排序的gif
堆排序总结
- 堆排序是利用堆性质的一种选择排序,在堆定选出最大值或者最小值
- 时间复杂度
- 堆排序的时间复杂度是O(nlogn)
- 由于堆排序堆原始记录的排序状态并不敏感,因此它无论是最好、最坏和时间平均复杂度均为O(nlogn)
- 空间复杂度:只使用了一个交换用的空间,所以空间复杂度O(1)
- 稳定性:不稳定