在
【数据结构Python描述】二叉堆(heap)简介和Python手工实现及使用二叉堆实现优先级队列
中,我们学习了什么是
二叉堆
,以及具体什么是最小堆和最大堆。
通过
【数据结构Python描述】自底向上构建二叉堆实现及其
O
(
n
)
O(n)
O
(
n
)
时间复杂度分析
,我们又学习了如何在给定一个序列(如:列表)的情况下,高效地将其转换为一个二叉堆。
本文将基于上述两篇文章的内容,来介绍如何通过最大堆来进行排序,即
堆排序
。
一、堆排序简介
1. 理论
如果给定输入列表
A[1..n]
,其中
n=len(A)
,则
堆排序
算法需要先构建一个最大堆,即最大元素总是位于根节点处,且所有子节点不大于其父节点。由于最大元素位于根节点
A[1]
处,因此我们可以通过如下步骤对A进行排序:
首先,将根节点和
A[n]
进行交换,则最大值的位置确定;
其次,由于新的根节点可能使得当前完全二叉树违背
堆序性质
,因此可以采用
元素交换的方式使其重新满足堆序性质
,需要注意的是,此时元素交换时无需再考虑元素
A[n]
;
然后,将新的根节点和
A[n-1]
进行交换,则次大值的位置确定;
再次,由于新的根节点可能使得当前完全二叉树违背
堆序性质
,再次采用元素交换的方式使其重新满足堆序性质,需要注意的是,此时元素交换时无需再考虑元素
A[n]
和
A[n-1]
;
最后,重复上述过程,直到
A[1..n]
按照非递减顺序排列。
2. 图解
2.1 建最大堆
实际上,给定一个列表,建一个最大堆的流程和
建一个最小堆
的流程基本一致,具体地,针对给定的10个元素列表,建堆步骤如下:
首先,同
建最小堆
一样,如图
(
a
)
(a)
(
a
)
所示,自底向上,先确保以位置5为根节点的完全二叉树是一个二叉堆,此时满足最大堆的堆序性质,不涉及元素交换;
其次,确保以位置4为根节点的完全二叉树是一个二叉堆,此时违背了最大堆的堆序性质,需对位置4和位置8的元素进行交换,如结果如图
(
c
)
(c)
(
c
)
所示;
紧接着,依次确保以位置3,2,1处为根节点的完全二叉树是一个二叉堆,最终建堆结果如图
(
f
)
(f)
(
f
)
所示。
2.2 用最大堆排序
下面是利用上述创建的最大堆进行排序的过程,具体地:
首先,将根节点元素16和最底层最右侧元素1交换,然后通过元素交换使得不包括16在内的其他节点均满足堆序性质,结果如图
(
b
)
(b)
(
b
)
所示;
然后,将根节点元素14和最底层最右侧元素1交换,然后通过元素交换使得不包括16和14在内的其他节点均满足堆序性质,结果如图
(
c
)
(c)
(
c
)
所示;
重复上述过程,直到所有元素排好序,如图
(
j
)
(j)
(
j
)
所示。
二、堆排序实现
由于堆一定是一个二叉树,而二叉树的实现可以是基于链式结构,也可以基于数组结构,为降低实现难度,且避免使用链式结构导致额外的内存开销(如一个节点保存其父、子节点引用所需的变量),这里使用
基于数组结构来描述二叉树
。
def __down_heap ( arr, num, j) :
"""
通过元素交换,确保列表arr[0:num]中,元素arr[j]及其子孙节点满足堆序性质
:param arr: 列表形式给出的待排序列
:param num: 列表索引上限
:param j: 列表元素的索引
:return: None
"""
left = 2 * j + 1
right = 2 * j + 2
if left < num:
large_child = left
if right < num:
if arr[ right] > arr[ left] :
large_child = right
if arr[ large_child] > arr[ j] :
arr[ large_child] , arr[ j] = arr[ j] , arr[ large_child]
__down_heap( arr, num, large_child)
def heapify ( arr) :
"""将列表形式给出的元素视为完全二叉树,对其进行建堆"""
for j in range ( ( len ( arr) - 1 ) // 2 , - 1 , - 1 ) :
__down_heap( arr, len ( arr) , j)
def heap_sort ( arr) :
"""堆排序"""
heapify( arr)
for i in range ( len ( arr) - 1 , 0 , - 1 ) :
arr[ i] , arr[ 0 ] = arr[ 0 ] , arr[ i]
__down_heap( arr, i, 0 )
if __name__ == '__main__' :
arr = [ 4 , 1 , 3 , 2 , 16 , 9 , 10 , 14 , 8 , 7 ]
heap_sort( arr)
print ( arr)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
三、堆排序复杂度
1. 时间复杂度
堆排序的时间复杂度分为两个部分:
一部分是调用
heapify
函数的建最大堆所需的时间开销,由
之前的理论分析
可知为
O
(
n
)
O(n)
O
(
n
)
;
另一部分是循环调用
__down_heap
方法确保未排序的完全二叉树为最大堆的时间开销,可按照如下计算:
l
o
g
(
n
−
1
)
+
l
o
g
(
n
−
2
)
+
⋅
⋅
⋅
+
l
o
g
(
1
)
=
l
o
g
(
(
n
−
1
)
!
)
log(n-1)+log(n-2)+\cdot\cdot\cdot+log(1)=log((n-1)!)
l
o
g
(
n
−
1
)
+
l
o
g
(
n
−
2
)
+
⋅
⋅
⋅
+
l
o
g
(
1
)
=
l
o
g
(
(
n
−
1
)
!
)
而:
l
o
g
(
(
n
−
1
)
!
)
<
l
o
g
(
n
!
)
<
n
l
o
g
(
n
)
log((n-1)!)<log(n!)<nlog(n)
l
o
g
(
(
n
−
1
)
!
)
<
l
o
g
(
n
!
)
<
n
l
o
g
(
n
)
最后忽略低阶项
O
(
n
)
O(n)
O
(
n
)
,故最坏时间复杂度为
n
l
o
g
(
n
)
nlog(n)
n
l
o
g
(
n
)
。
2. 空间复杂度
由上述堆排序的实现代码可以看出,在堆排序过程中,除了输入的待排序列和排序过程中使用的辅助变量外,没有额外的内存开销,因此这是典型的原地排序,故其最坏空间复杂度为
Θ
(
n
)
\Theta{(n)}
Θ
(
n
)
。