今天是最后一个比较排序。
堆排序使用了一种特殊的数据结构——堆,和C语言里的用于动态内存分配的堆不是一个概念。堆排序里的堆事实上是二叉树,因而又叫二叉堆。除了最后一层以外它是个满二叉树,最后一层剩余的数据从左边开始向右填。
可以很容易看出一些性质:
对于一个结点i,它的父结点索引为 floor(i/2),左子女为2i,右子女为2i + 1,在树图的右边还有它在数组中的存储方式。
另外堆可以分成两类:
最大堆和最小堆。最大堆的性质是对于除了根结点之外的每个结点i都有A[parent(i)] ≥ A[i],因此根结点具有最大值,左图中就是一个最大堆的例子。而最小堆的情况相反。
堆排序中使用的是最大堆。
另外还有一些性质需要知道:
含n个元素的堆的高度为floor(lgn)。
因为对于一个高度为h的堆来说,n必满足:2h ≤ n ≤ 2h+1 – 1 → lg(n+1) - 1 ≤ h ≤ lgn
又 lg(n+1) > lgn → lgn – 1 < h ≤ lgn → h = floor(lgn)
从而有结论:含n个元素的堆的高度为 floor(lgn)。
如果用数组存储规模为n的堆结构,叶子结点的下标从floor(n/2) + 1开始。(从图中可以看出叶子结点是从图中最后一个结点的父结点之后开始的)
#subroutines for heap sort
def left(i):
return 2*i
def right(i):
return 2*i + 1
def parent(i):
return i/2
在介绍堆排序的大致思路前先介绍一个堆排序算法中一个关键的子程序maxHeapify。它在建堆和排序过程中都有用到。
它的输入是一个部分被最大堆化(maxHeapify)的序列,意味着序列中有一部分已经是按最大堆形式存储的,其中对于没有子女的叶子结点来说也算是一个特殊的最大堆。
它假设一个结点i的左子树和右子树均为最大堆,但如果把结点i作为根结点加入进去则不一定能保持这个新的子树仍然为最大堆,因为结点i的值可能小于它的后代。因此maxHeapify的作用就是维持这个最大堆的性质,代码如下:
def maxHeapify(A, i, heapSize):
l = left(i)
r = right(i)
if l <= heapSize and A[i - 1] < A[l - 1]:
largest = l
else:
largest = i
if r <= heapSize and A[largest - 1] < A[r - 1]:
largest = r
if largest != i:
t = A[i - 1]
A[i - 1] = A[largest - 1]
A[largest - 1] = t
maxHeapify(A, largest, heapSize)
# return A
分析下它的过程,在结点i和它的左右子女中通过比较找到最大的一个值,然后和结点i进行交换,这样结点i和它的左右子女组成的二叉树就满足最大堆的性质,但此时变换位置后的结点i与它现在的左右子女依然有可能不满足最大堆的性质,于是将交换后结点i的索引作为根结点,递归调用maxHeapify,最终到达堆的底部(这里用heapSize作为终止条件,它代表序列中最大堆的大小,A[1至heapSize]中均为堆中的元素,不在这个范围内的元素不属于相应的堆)。此时结点i和它的左右子树形成了一个新的,规模+1的堆,因此如果从底部调用这个函数的话,可以构造最大堆。
另外要注意下它的数组实现,因为我们分析树的时候一般根结点的编号均为1,而数组的第一个元素是从0开始的,这样使得在实现的时候有些麻烦。一种考虑是将所有数组从下标1开始存放。我的处理方法是,假设使用者输入的参数均是按照平时的习惯,从1开始。在函数内部和输出也尽量保持这个惯例,但在执行访问数组元素时,对索引执行减一操作。
因为结点i的子树规模最大为2n/3,所以最坏情况的递归式为
T(n) = T(2n/3) + c
c为每次递归做的常数次操作的代价
T(n) = Θ(lgn)
下面是用maxHeapify构造最大堆的代码:
def buildMaxHeap(L):
n = len(L)
i = n/2
while i >= 1:
maxHeapify(L, i, len(L))
i = i - 1
# return L
注意到maxheapify要满足的前提条件是有最大堆的子树,而单元素的结点也满足这个性质,因此从从最后一个内结点开始循环执行maxHeapify子程序,向上构造最大堆,直到根结点。
之前分析过堆的性质,对于规模为n的堆,叶子结点是从floor(n/2) + 1开始的,所以在迭代开始的过程中有 i = n/2 这个步骤。
很容易看出建堆具有线性的渐进运行时间。
有了以上2个子程序,理解堆排序就很容易了,因为它的思路真的非常简单。利用最大堆的性质,当前处于根结点的值是序列中最大的,因此只要把它与A[n]交换就可以了。然后将A[n]从最大堆中删除,即对heapSize 做减一操作。此时根结点的值不一定满足最大堆的性质,因此调用maxHeapify(A, 1, heapSize)来重新维持最大堆的性质,之后重复这样的迭代,每次将根结点中的元素与相应位置的元素做交换,并减少堆的大小,直到堆中只剩一个元素即完成排序过程。
def heapSort(L):
buildMaxHeap(L)
heapSize = len(L)
while heapSize >= 2:
t = L[0]
L[0] = L[heapSize - 1]
L[heapSize - 1] = t
heapSize = heapSize - 1
maxHeapify(L, 1, heapSize)
return L
分析下运行时间:
buildMaxHeap的代价为Θ(n)
循环过程的代价为Θ(lgn)的从2到n的级数求和,因此代价为Θ(nlgn)
两个部分相加,低阶被舍去,因此堆排序的运行时间为Θ(nlgn),在比较排序算法中和归并排序一样是渐进最优的算法。但是它是原地排序,空间复杂度更好。
比较排序终于弄完了。。剩下还有线性时间排序
。。睡了先。。

浙公网安备 33010602011771号