笔记
堆排序算法一开始调用BUILD-MAX-HEAP将输入数组
构建成最大堆。此时,数组中的最大元素在根结点
,因此通过将
与
交换,可以将
放置在排序后的正确位置。由于根结点换了一个新的元素,现在的根结点有可能不满足堆的性质,因此需要调用MAX-HEAPIFY来维护堆的性质,只不过MAX-HEAPIFY是针对
来调用的。然后堆排序算法重复这一过程,直到堆的大小一直降到
为止。
堆排序的时间复杂度为
。因为一共调用了
次MAX-HEAPIFY,每次调用的时间为
;而只调用了一次BUILD-MAX-HEAP,调用时间为
。
下图给出了一个堆排序的例子。
练习
6.4-1 参照图6-4的方法,说明HEAPSORT在数组A = <5, 13, 2, 25, 7, 17, 20, 8, 4>上的操作过程。
解
6.4-2 试分析在使用下列循环不变式时,HEAPSORT的正确性:
在算法的第2~5行for循环每次迭代开始时,子数组
是一个包含了数组
中最小的
个元素的最大堆,而子数组
包含了数组
中已排好序的最大的
个元素。
解
(1) 初始状态
此时
,子数组
是一个包含了所有元素并且已经建好了的最大堆,而子数组
为空。因此,在进入迭代之前,循环不变式为真。
(2) 保持
假设在第
次迭代之前,循环不变式为真。那么此时子数组
包含了整个数组
中最小的
个元素,并且
是一个最大堆,因此
保存了子数组
中的最大元素,也就是整个数组
中第
大的元素(有
个元素比它大,所以按从大到小顺序,它是第
个)。
迭代过程的第一步是将这个第
大的元素从
中取出,并与最大堆
的末尾元素
交换。此时
变成了第
大的元素,它与
组成了一个子数组
,并且
包含了已排好序的最大的
个元素。因此进入下一次迭代
之前,循环不变式的后半部分为真。
上一步将最大堆
中最大元素取出了,并且将堆的末尾元素交换到了
的位置,因此堆的大小减小了
,并且
位置有可能不满足最大堆的性质了。因此,迭代过程的第二步是将
减
,并调用MAX-HEAPIFY(A, 1)来维持最大堆的性质。注意,此时最大堆已经变成了
,并且它包含了整个数组
中最小的
个元素。因此进入下一次迭代
之前,循环不变式的前半部分也为真。
(3) 终止
前面说明了在每次迭代之前,循环不变式都为真。循环终止时,有
,此时循环不变式也应当为真。将
代入循环不变式,得到“
实际上是整个数组
中的最小元素,子数组
包含了整个数组
中已排好序的最大的
个元素”。显然,
已经是最小的元素,并且
之后的元素已经排好序,所以整个数组
都已经排好序。
6.4-3 对于一个按升序排列的包含
个元素的有序数组
来说,HEAPSORT的时间复杂度是多少?如果
是降序呢?
解
我们要假设数组中的元素各不相同。无论数组按升序排列还是按降序排序,HEAPSORT的时间复杂度都为
。
如果数组中的元素全部相同,那么HEAPSORT的时间复杂度降为
。因为此时for循环中的MAX-HEAPIFY的时间复杂度都为O(1)。
6.4-4 证明:在最坏情况下,HEAPSORT的时间复杂度是
。
解
由于建堆的时间属于低阶项,我们这里不考虑建堆的时间,只考察建堆后的排序时间。每次迭代交换
与
之后,此时堆中一共有
个元素,并且此时堆的高度为
。
被交换到
的位置后,再调用MAX-HEAPIFY来维护堆的性质。最坏情况下
会被逐层下降到最底层为止,下降的次数等于此时堆的高度
。因此,最坏情况下,HEAPSORT调用MAX-HEAPIFY的总的开销为
这里用到了第3章的结论
。由此可见,最坏情况下,HEAPSORT的时间复杂度是
。
6.4-5 证明:在所有元素都不同的情况下,HEAPSORT的时间复杂度是
。
上题证明了堆排序的最坏情况时间复杂度为
。本题显然还需要证明最好情况下的时间复杂度也为
。不过这一证明过程较为复杂,我们记住结论即可。
以下是堆排序的代码链接。
https://github.com/yangtzhou2012/Introduction_to_Algorithms_3rd/tree/master/Chapter06/HeapSort