复习之前学过的堆排序,发现掌握的不是特别牢固,又仔细阅读了几篇博文,整理出来这篇记录。
1 堆排序介绍
1.1 与堆有关的概念
二叉树:每个节点最多有两个子节点的树。
完全二叉树:除了最后一层之外,每一层都被完全填充并且最后一层左对齐的二叉树。
最大堆:每个父节点都大于孩子节点的完全二叉树。
最小堆:每个父节点都小于孩子节点的完全二叉树。
这里我们讨论最大堆,在最大堆中,如果节点标号从 1
开始,满足如下条件:
- 父节点标号为
i
,左孩子节点标号为i*2
, 右孩子节点标号为i*2 + 1
。 - 最后一个节点标号为
j
,则它的父节点的标号为j // 2
,也是最后一个有子节点。
1.2 堆排序
给定一个列表[90,50,80,16,30,60,70,10,2]
,在上面讲到,节点标号从1
开始满足某些性质方便表示,为了和下标索引对应起来,在列表最前端添加一个占位元素0
,此时列表变为[0,90,50,80,16,30,60,70,10,2]
。
堆排序主要步骤如下:
- 首先我们将列表调整成一个最大堆。
- 此时堆首的元素最大,将它与堆最有一个元素交换。
- 将剩下的元素调整成一个最大堆。
- 循环执行,直到需要调整的堆中只有一个元素。
2堆排序步骤详解
-
将列表调整为一个最大堆,得到结果如下:
-
将最大的堆顶元素与最后一个元素交换,即
90
与2
交换,得到如下结果:
-
此时除了90剩下的元素构成的堆已经不是最大堆,调整剩下的元素,使它成为最大堆
-
调整结果如下,然后继续重复上述步骤,直到堆中只有一个元素。
3 代码
整体的排序分为 2 块:
- 初始化最大堆
- 交换堆顶与堆尾元素
注:
1.在初始化最大堆的过程中,从最后一个有子节点开始,也就是从最后一个极小堆开始
2.在队首加入占位元素,如果直接用python内置的list
,用.insert(0)
方法时间复杂度为O(n)
,而用单链表时间复杂度仅为O(1)
。文件头需要加入from collections import deque
def heap_sort(l):
# 加入占位元素
L = deque(l)
L.appendleft(0)
Length = len(L) - 1 #最大索引下标
first_sort_count = Length // 2 # 最后一个有子节点索引下标
# 初始化最大堆, 从最后一个有子节点开始
for i in range(first_sort_count):
heap_adjust(L, first_sort_count - i, Length)
# 开始排序
for i in range(Length-1):
swap_param(L, 1, Length-i)
heap_adjust(L, 1, Length-i-1)
return [L[i] for i in range(1, len(L))]
def heap_adjust(L, start, end):
tmp = L[start]
i = start
j = i*2
while j <= end:
if (j<end) and (L[j]<L[j+1]):
j += 1
if tmp < L[j]:
L[i] = L[j]
i = j
j = i*2
else:
break
L[i] = tmp
def swap_param(L, i, j):
L[i], L[j] = L[j], L[i]
4 时间和空间复杂度
空间复杂度:O(1)
。就地排序,全程并没有使用额外的空间。
时间复杂度:O(nlogn)
。
分为初始化堆过程和每次选取最大数后重新建堆的过程。
1.初始化建堆过程时间:O(n)
首先要理解怎么计算这个堆化
假设高度为k,则从倒数第二层右边的节点开始,这一层的节点都要执行子节点比较然后交换(如果顺序是对的就不用交换);倒数第三层呢,则会选择其子节点进行比较和交换,如果没交换就可以不用再执行下去了。如果交换了,那么又要选择一支子树进行比较和交换;
那么第 i 层的时间计算为:s = 2^( i - 1 ) * ( k - i );其中 i 表示第几层,2^( i - 1) 表示该层上有多少个元素,( k - i) 表示子树上要比较的次数,如果在最差的条件下,就是比较次数后还要交换;因为这个是常数,所以提出来后可以忽略;
S = 2^(k-2) * 1 + 2^(k-3)2+.....+2(k-2) + 2^(0)*(k-1) ===> 因为叶子层不用交换,所以i从 k-1 开始到 1;
这个等式求解:等式左右乘上2,然后和原来的等式相减,就变成了:
S = 2^(k - 1) + 2^(k - 2) + 2^(k - 3) ..... + 2 - (k-1)。
除最后一项外,就是一个等比数列了,直接用求和公式:S = ;
S = 2^k -k -1;又因为k为完全二叉树的深度,所以 (2^k) <= n < (2^k -1 ),总之可以认为:k = logn
综上所述得到:S = n - longn -1,所以时间复杂度为:O(n)。
2.重构最大堆过程
在取出堆顶点放到对应位置并把原堆的最后一个节点填充到堆顶点之后,需要对堆进行重建,只需要对堆的顶点调用heap_adjust()函数。
每次重建意味着有一个节点出堆,所以需要将堆的容量减一。heap_adjust()函数的时间复杂度k=log(n),k为堆的层数。所以在每次重建时,随着堆的容量的减小,层数会下降,函数时间复杂度会变化。重建堆一共需要n-1次循环,每次循环的比较次数为log(i),则相加为:log2+log3+…+log(n-1)+log(n)≈log(n!)。可以证明log(n!)和nlog(n)是同阶函数,所以时间复杂度为O(nlogn)。
最终的时间复杂度为O(nlogn)
。