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