Introduction to Algorithms - Third Edition
Part II. Sorting and Order Statistics
Chapter 6. Heapsort
堆排序是原地排序算法,其运行时间是 O ( n lg n ) O(n \lg n) O(nlgn)。
堆排序引入了一种算法设计技术:使用一种数据结构管理信息,这种情况下的数据结构是“堆”。
堆数据结构还可以建构一个有效的优先级队列。
“堆”一词最初是在堆排序中创造的,但后来指“垃圾收集存储区”,如编程语言 Java 和 Lisp 所提供的。
在本书中提到的堆,都指的是堆数据结构,不是垃圾收集存储区。
6.1 堆
(二叉)堆数据结构是一个数组对象,可以将其视为近似完全二叉树。树的每个节点对应于数组的一个元素。该树完全填充了所有层次,除了最后一层(从左填充到某点)。
表示堆的数组
A
A
A 具有两个属性:
A
.
l
e
n
g
t
h
A.length
A.length 通常提供数组中的元素个数,
A
.
h
e
a
p
-
s
i
z
e
A.heap\text{-}size
A.heap-size 表示堆中多少个元素存储在数组
A
A
A 中。
尽管
A
[
1..
A
.
l
e
n
g
t
h
]
A[1..A.length]
A[1..A.length] 可以包含数字,但是只有
A
[
1..
A
.
h
e
a
p
-
s
i
z
e
]
A[1..A.heap\text{-}size]
A[1..A.heap-size] 中的元素才是堆中的有效元素,其中
0
≤
A
.
h
e
a
p
-
s
i
z
e
≤
A
.
l
e
n
g
t
h
0 \le A.heap\text{-}size \le A.length
0≤A.heap-size≤A.length。
树的根是
A
[
1
]
A[1]
A[1],给定一个节点的索引
i
i
i,可以轻松地计算其父节点,左子节点和右子节点的索引:
PARENT ( i ) \texttt{PARENT} (i) PARENT(i)
return ⌊ i / 2 ⌋ \lfloor i / 2 \rfloor ⌊i/2⌋
LEFT ( i ) \texttt{LEFT} (i) LEFT(i)
return 2 i 2i 2i
RIGHT ( i ) \texttt{RIGHT} (i) RIGHT(i)
return 2 i + 1 2i+1 2i+1
在大多数计算机中,通过简单地将
i
i
i 的二进制表示左移一位,
L
E
F
T
\tt LEFT
LEFT 过程可在一条指令内计算
2
i
2i
2i。
R
I
G
H
T
\tt RIGHT
RIGHT 过程可通过将
i
i
i 的二进制表示左移一位,然后再添加 1 作为低阶位来快速计算
2
i
+
1
2i+1
2i+1。
P
A
R
E
N
T
\tt PARENT
PARENT 过程可以通过将
i
i
i 右移一位来计算
⌊
i
/
2
⌋
\lfloor i / 2 \rfloor
⌊i/2⌋。
好的堆排序的实现,通常将这些过程作为“宏”过程或“内联”过程来实现。
二叉堆有两种:最大堆(max-heap)和最小堆(min-heap)。
- 在最大堆中,最大堆性质是,除了根以外的每个结点
i
i
i,有
A
[
PARENT
(
i
)
]
≥
A
[
i
]
A[\texttt{PARENT}(i)] \ge A[i]
A[PARENT(i)]≥A[i]。
最大堆中的最大元素存储在根结点中。 - 在最小堆中,最小堆性质是,除了根以外的每个结点
i
i
i,有
A
[
PARENT
(
i
)
]
≤
A
[
i
]
A[\texttt{PARENT}(i)] \le A[i]
A[PARENT(i)]≤A[i]。
最小堆中的最小元素存储在根结点中。
堆可以视为一棵树,堆中一个结点的高度定义为,从该结点到叶子的最长简单下降路径上边的数目。定义堆的高度为其根结点的高度。
练习
6.1-1 在一个高度为
h
h
h 的堆中,元素的最大和最小个数是多少?
解: 最大个数:
2
0
+
2
1
+
⋯
+
2
h
=
∑
i
=
0
h
2
i
=
2
(
h
+
1
)
−
1
2^0 + 2^1 + \dots + 2^h = \sum_{i=0}^h 2^i = 2^{(h+1)} - 1
20+21+⋯+2h=∑i=0h2i=2(h+1)−1
最小个数:
2
0
+
2
1
+
⋯
+
2
h
−
1
+
1
=
∑
i
=
0
h
−
1
2
i
+
1
=
2
h
2^0 + 2^1 + \dots + 2^{h-1} + 1 = \sum_{i=0}^{h-1}2^i + 1= 2^{h}
20+21+⋯+2h−1+1=∑i=0h−12i+1=2h
6.1-2 证明:含有
n
n
n 个元素的堆的高度是
⌊
lg
n
⌋
\lfloor \lg n \rfloor
⌊lgn⌋。
证: 假设含有
n
n
n 个元素的堆的高度是
h
h
h。由 练习 6.1-1,得
2
h
≤
n
≤
2
h
+
1
−
1
<
2
h
+
1
2^h \le n \le 2^{h+1}-1 < 2^{h+1}
2h≤n≤2h+1−1<2h+1,
所以,
h
≤
lg
n
<
h
+
1
h \le \lg n < h+1
h≤lgn<h+1。
因为
h
h
h 是一个整数,根据
⌊
⌋
\lfloor\rfloor
⌊⌋ 的定义,可得
h
=
⌊
lg
n
⌋
h = \lfloor \lg n \rfloor
h=⌊lgn⌋。
6.1-3 证明:在最大堆中的任何子树中,子树的根结点包含该子树中的最大值。
证: 最大堆
A
A
A 中,对于每个结点
i
i
i,都有
A
[
i
]
>
A
[
LEFT
(
i
)
]
A[i] > A[\texttt{LEFT}(i)]
A[i]>A[LEFT(i)] 且
A
[
i
]
>
A
[
RIGHT
(
i
)
]
A[i] > A[\texttt{RIGHT}(i)]
A[i]>A[RIGHT(i)]。
6.1-4 在一个最大堆中,假设所有元素不同,则最小元素可能位于堆的什么位置?
解: 最大堆中所有叶子结点都有可能是最小元素所处的位置。
6.1-5 一个已排序好的数组是最小堆吗?
解: 若从小到大排序,是。
6.1-6 包含
<
23
,
17
,
14
,
6
,
13
,
10
,
1
,
5
,
7
,
12
>
<23, 17, 14, 6, 13, 10, 1, 5, 7, 12>
<23,17,14,6,13,10,1,5,7,12> 的数组是一个最大堆吗?
解: 不是。
A
[
4
]
=
6
A[4] = 6
A[4]=6,
A
[
9
]
=
7
A[9] = 7
A[9]=7,
A
[
4
]
A[4]
A[4] 是
A
[
9
]
A[9]
A[9] 的父节点,但
A
[
4
]
<
A
[
9
]
A[4] < A[9]
A[4]<A[9],不符合最大堆性质。
6.1-7 证明:当用数组表示存储
n
n
n 个元素的堆时,叶子结点的下标是
⌊
n
/
2
⌋
+
1
,
⌊
n
/
2
⌋
+
2
,
…
,
n
\lfloor n/2 \rfloor+1 , \lfloor n/2 \rfloor+2, \dots, n
⌊n/2⌋+1,⌊n/2⌋+2,…,n。
证: 索引为
n
n
n 的元素的父节点的索引为
⌊
n
/
2
⌋
\lfloor n/2 \rfloor
⌊n/2⌋,又因为索引为
n
n
n 的元素的父节点是最后一个非叶子结点,所以第一个叶子结点索引为
⌊
n
/
2
⌋
+
1
\lfloor n/2 \rfloor+1
⌊n/2⌋+1,第二个叶子结点索引为
⌊
n
/
2
⌋
+
2
\lfloor n/2 \rfloor+2
⌊n/2⌋+2,直至最后一个结点索引为
n
n
n。
6.2 保持堆的性质
假定以 LEFT ( i ) \texttt{LEFT}(i) LEFT(i) 和 RIGHT ( i ) \texttt{RIGHT}(i) RIGHT(i) 为根的二叉树都是最大堆,但 A [ i ] A[i] A[i] 可能小于它的子节点的值,这违反了最大堆的性质。
MAX-HEAPIFY ( A , i ) \texttt{MAX-HEAPIFY} (A, i) MAX-HEAPIFY(A,i)
l = LEFT ( i ) l = \texttt{LEFT}(i) l=LEFT(i)
r = RIGHT ( i ) r = \texttt{RIGHT}(i) r=RIGHT(i)
if l ≤ A . h e a p - s i z e l \le A.heap\text-size l≤A.heap-size and A [ l ] > A [ i ] A[l] > A[i] A[l]>A[i]
l a r g e s t = l largest = l largest=l
else l a r g e s t = i largest = i largest=i
if r ≤ A . h e a p - s i z e r \le A.heap\text-size r≤A.heap-size and A [ r ] > A [ l a r g e s t ] A[r] > A[largest] A[r]>A[largest]
l a r g e s t = r largest = r largest=r
if l a r g e s t ≠ i largest \ne i largest=i
exchange A [ i ] A[i] A[i] with A [ l a r g e s t ] A[largest] A[largest]
MAX-HEAPIFY ( A , l a r g e s t ) \texttt{MAX-HEAPIFY} (A, largest) MAX-HEAPIFY(A,largest)
MAX-HEAPIFY
\texttt{MAX-HEAPIFY}
MAX-HEAPIFY 的运行时间:
T
(
n
)
≤
T
(
2
n
/
3
)
+
Θ
(
1
)
T(n) \le T(2n/3) + \Theta(1)
T(n)≤T(2n/3)+Θ(1),
因为
i
i
i 结点的子树大小至多为
2
n
/
3
2n/3
2n/3 —— 最坏情况发生在最底层半满的时候。
根据主定理的情况2(定理4.1),得
T
(
n
)
=
O
(
lg
n
)
T(n) = O(\lg n)
T(n)=O(lgn)。
练习
6.2-1 以图 6.2 为例,图示出
MAX-HEAPIFY
(
A
,
3
)
\texttt{MAX-HEAPIFY} (A, 3)
MAX-HEAPIFY(A,3) 作用于数组
A
=
<
27
,
17
,
3
,
16
,
13
,
10
,
1
,
5
,
7
,
12
,
4
,
8
,
9
,
0
>
A = < 27, 17, 3, 16, 13, 10, 1, 5, 7, 12, 4, 8, 9, 0 >
A=<27,17,3,16,13,10,1,5,7,12,4,8,9,0> 的过程。
解:
6.2-2 从过程
MAX-HEAPIFY
\texttt{MAX-HEAPIFY}
MAX-HEAPIFY 开始,为过程
MIN-HEAPIFY
(
A
,
i
)
\texttt{MIN-HEAPIFY}(A,i)
MIN-HEAPIFY(A,i) 编写伪代码,对最小堆执行相应的操作。比较
MIN-HEAPIFY
\texttt{MIN-HEAPIFY}
MIN-HEAPIFY 与
MAX-HEAPIFY
\texttt{MAX-HEAPIFY}
MAX-HEAPIFY 的运行时间。
解:
MIN-HEAPIFY
(
A
,
i
)
\texttt{MIN-HEAPIFY} (A, i)
MIN-HEAPIFY(A,i)
l
=
LEFT
(
i
)
l = \texttt{LEFT}(i)
l=LEFT(i)
r
=
RIGHT
(
i
)
r = \texttt{RIGHT}(i)
r=RIGHT(i)
if
l
≤
A
.
h
e
a
p
-
s
i
z
e
l \le A.heap\text-size
l≤A.heap-size and
A
[
l
]
<
A
[
i
]
A[l] < A[i]
A[l]<A[i]
s
m
a
l
l
e
s
t
=
l
smallest = l
smallest=l
else
s
m
a
l
l
e
s
t
=
i
smallest = i
smallest=i
if
r
≤
A
.
h
e
a
p
-
s
i
z
e
r \le A.heap\text-size
r≤A.heap-size and
A
[
r
]
<
A
[
s
m
a
l
l
e
s
t
]
A[r] < A[smallest]
A[r]<A[smallest]
s
m
a
l
l
e
s
t
=
r
smallest = r
smallest=r
if
s
m
a
l
l
e
s
t
≠
i
smallest \ne i
smallest=i
exchange
A
[
i
]
A[i]
A[i] with
A
[
s
m
a
l
l
e
s
t
]
A[smallest]
A[smallest]
MIN-HEAPIFY
(
A
,
s
m
a
l
l
e
s
t
)
\texttt{MIN-HEAPIFY} (A, smallest)
MIN-HEAPIFY(A,smallest)
MIN-HEAPIFY \texttt{MIN-HEAPIFY} MIN-HEAPIFY 过程运行时间 T ( n ) = O ( lg n ) T(n) = O(\lg n) T(n)=O(lgn)。
6.2-3 当
A
[
i
]
A[i]
A[i] 大于其子节点的值时,调用过程
MAX-HEAPIFY
(
A
,
i
)
\texttt{MAX-HEAPIFY} (A,i)
MAX-HEAPIFY(A,i) 有什么影响?
解: 以
i
i
i 为根结点的树不作任何变动。
6.2-4 对于
i
>
A
.
h
e
a
p
-
s
i
z
e
/
2
i > A.heap\text-size/2
i>A.heap-size/2,调用过程
MAX-HEAPIFY
(
A
,
i
)
\texttt{MAX-HEAPIFY} (A,i)
MAX-HEAPIFY(A,i) 有什么影响?
解: 以
i
i
i 为根结点的树不作任何变动。
6.2-5
MAX-HEAPIFY
\texttt{MAX-HEAPIFY}
MAX-HEAPIFY 的代码相当有效,除了最后一行的递归调用,它可能会使某些编译器产生低效的的代码。使用迭代控制结构(一个循环),而不是递归,写一个高效的
MAX-HEAPIFY
\texttt{MAX-HEAPIFY}
MAX-HEAPIFY。
解:
MAX-HEAPIFY
(
A
,
i
)
\texttt{MAX-HEAPIFY}(A, i)
MAX-HEAPIFY(A,i)
while
i
≤
A
.
h
e
a
p
-
s
i
z
e
i \le A.heap\text-size
i≤A.heap-size
l
=
LEFT
(
i
)
l = \texttt{LEFT}(i)
l=LEFT(i)
r
=
RIGHT
(
i
)
r = \texttt{RIGHT}(i)
r=RIGHT(i)
l
a
r
g
e
s
t
=
i
largest = i
largest=i
if
l
≤
A
.
h
e
a
p
-
s
i
z
e
l \le A.heap\text-size
l≤A.heap-size and
A
[
l
]
<
A
[
i
]
A[l] < A[i]
A[l]<A[i]
l
a
r
g
e
s
t
=
l
largest = l
largest=l
if
r
≤
A
.
h
e
a
p
-
s
i
z
e
r \le A.heap\text-size
r≤A.heap-size and
A
[
r
]
>
A
[
l
a
r
g
e
s
t
]
A[r] > A[largest]
A[r]>A[largest]
l
a
r
g
e
s
t
=
r
largest = r
largest=r
if
l
a
r
g
e
s
t
≠
i
largest \ne i
largest=i
exchange
A
[
i
]
A[i]
A[i] with
A
[
l
a
r
g
e
s
t
]
A[largest]
A[largest]
i
=
l
a
r
g
e
s
t
i = largest
i=largest
else break // 跳出循环
6.2-6 证明:在大小为
n
n
n 的堆中,
MAX-HEAPIFY
\texttt{MAX-HEAPIFY}
MAX-HEAPIFY 的最坏情况运行时间是
Ω
(
lg
n
)
\Omega(\lg n)
Ω(lgn)。(提示:对于一个有
n
n
n 个结点的堆,设定结点的值,使得在从根到叶子的简单路径上,每个结点都能够递归调用
MAX-HEAPIFY
\texttt{MAX-HEAPIFY}
MAX-HEAPIFY。)
证: 如果根上放置的值小于左右子树中的每个值,则将递归调用
MAX-HEAPIFY
\texttt{MAX-HEAPIFY}
MAX-HEAPIFY,直到到达叶子为止。
要使递归调用遍历到叶子的最长路径,选择值,使
MAX-HEAPIFY
\texttt{MAX-HEAPIFY}
MAX-HEAPIFY 始终在左子结点上递归。当左子节点大于或等于右子节点时,它沿左分支。
例如,将根结点的值设为 0,将其他的所有结点的值设置为 1。这样,
MAX-HEAPIFY
\texttt{MAX-HEAPIFY}
MAX-HEAPIFY 将被调用
h
h
h 倍(其中
h
h
h 是堆高度,即从根到叶的最长路径中的边数),又因为每次调用为
Θ
(
1
)
\Theta(1)
Θ(1),因此其运行时间为
Θ
(
h
)
\Theta(h)
Θ(h),即
Θ
(
lg
n
)
\Theta(\lg n)
Θ(lgn)。
因为上面已经假设了一种情况,使得
MAX-HEAPIFY
\texttt{MAX-HEAPIFY}
MAX-HEAPIFY 的运行时间为
Θ
(
lg
n
)
\Theta(\lg n)
Θ(lgn),因此最坏的运行时间为
Ω
(
lg
n
)
\Omega(\lg n)
Ω(lgn)。
6.3 建堆
自底向上地使用 MAX-HEAPIFY \texttt{MAX-HEAPIFY} MAX-HEAPIFY 将数组 A [ 1.. n ] A[1..n] A[1..n] 转换为一个最大堆。由练习 6.1-7 知,子数组 A [ ( ⌊ n / 2 ⌋ ) . . n ] A[(\lfloor n/2 \rfloor)..n] A[(⌊n/2⌋)..n] 是树的所有叶子,每个可看作是只含一个元素的堆。
BUILD-MAX-HEAP ( A ) \texttt{BUILD-MAX-HEAP}(A) BUILD-MAX-HEAP(A)
A . h e a p - s i z e = A . l e n g t h A.heap\text-size = A.length A.heap-size=A.length
for i = ⌊ A . l e n g t h / 2 ⌋ i = \lfloor A.length /2 \rfloor i=⌊A.length/2⌋ downto 1 1 1
MAX-HEAPIFY ( A , i ) \texttt{MAX-HEAPIFY}(A, i) MAX-HEAPIFY(A,i)
可以使用循环不变式证明
BUILD-MAX-HEAP
\texttt{BUILD-MAX-HEAP}
BUILD-MAX-HEAP 的正确性。
循环不变式:在 2~3 行中 for 循环的每一次迭代开始时,结点
i
+
1
,
i
+
2
,
…
,
n
i+1, i+2, \dots, n
i+1,i+2,…,n 都是一个最大堆的根。
分析
BUILD-MAX-HEAP
\texttt{BUILD-MAX-HEAP}
BUILD-MAX-HEAP 运行时间的紧确界:
性质1:有
n
n
n 个元素的堆的高度是
⌊
lg
n
⌋
\lfloor \lg n \rfloor
⌊lgn⌋,见练习6.1-2
性质2:在任意高度
h
h
h 上,最多有
⌈
n
/
2
h
+
1
⌉
\lceil n / 2^{h+1} \rceil
⌈n/2h+1⌉ 个结点,见练习6.3-3
在高度为
h
h
h 的结点上调用
MAX-HEAPIFY
\texttt{MAX-HEAPIFY}
MAX-HEAPIFY 需要时间
O
(
h
)
O(h)
O(h),
BUILD-MAX-HEAP
\texttt{BUILD-MAX-HEAP}
BUILD-MAX-HEAP 的总代价为
∑
h
=
0
⌊
lg
n
⌋
⌈
n
2
h
+
1
⌉
O
(
h
)
=
O
(
n
∑
h
=
0
⌊
lg
n
⌋
h
2
h
)
\sum_{h=0}^{\lfloor \lg n \rfloor} \lceil \frac{n}{2^{h+1}} \rceil O(h) = O(n\sum_{h=0}^{\lfloor \lg n \rfloor} \frac{h}{2^{h}})
h=0∑⌊lgn⌋⌈2h+1n⌉O(h)=O(nh=0∑⌊lgn⌋2hh)
通过替换公式 (A.8) 中的
x
=
1
/
2
x=1/2
x=1/2 计算后面的和式,得
∑
h
=
0
∞
h
2
h
=
1
/
2
(
1
−
1
/
2
)
2
=
2
\sum_{h=0}^{\infty} \frac{h}{2^{h}} = \frac{1/2}{(1-1/2)^2} = 2
h=0∑∞2hh=(1−1/2)21/2=2
所以,
BUILD-MAX-HEAP
\texttt{BUILD-MAX-HEAP}
BUILD-MAX-HEAP 的运行时间为
O
(
n
∑
h
=
0
⌊
lg
n
⌋
h
2
h
)
=
O
(
n
∑
h
=
0
∞
h
2
h
)
=
O
(
n
)
O(n\sum_{h=0}^{\lfloor \lg n \rfloor} \frac{h}{2^{h}}) = O(n\sum_{h=0}^{\infty} \frac{h}{2^{h}}) = O(n)
O(nh=0∑⌊lgn⌋2hh)=O(nh=0∑∞2hh)=O(n)
因此,可以在线性时间内,将一个无序数组建成一个最大堆。
练习
6.3-1 以图6.3 作为范例,作出
BUILD-MAX-HEAP
\texttt{BUILD-MAX-HEAP}
BUILD-MAX-HEAP 作用在数组
A
=
<
5
,
3
,
17
,
10
,
84
,
19
,
6
,
22
,
9
>
A = < 5, 3, 17, 10, 84, 19, 6, 22, 9 >
A=<5,3,17,10,84,19,6,22,9> 上的过程的图示。
解:
6.3-2 在
BUILD-MAX-HEAP
\texttt{BUILD-MAX-HEAP}
BUILD-MAX-HEAP 的第2行代码中,为什么循环下标
i
i
i 是从
⌊
A
.
l
e
n
g
t
h
/
2
⌋
\lfloor A.length /2 \rfloor
⌊A.length/2⌋ 降到
1
1
1,而不是从
1
1
1 增加到
⌊
A
.
l
e
n
g
t
h
/
2
⌋
\lfloor A.length /2 \rfloor
⌊A.length/2⌋?
解: 在调用函数
MAX-HEAPIFY
(
A
,
i
)
\texttt{MAX-HEAPIFY}(A, i)
MAX-HEAPIFY(A,i) 时,保证以结点
i
i
i 为根的子树是最大堆。
6.3-3 证明:在任一含有
n
n
n 个结点的堆中,最多有
⌈
n
/
2
h
+
1
⌉
\lceil n / 2^{h+1} \rceil
⌈n/2h+1⌉ 个高度为
h
h
h 的结点。
证: 由练习6.1-2 知,含有
n
n
n 个元素的堆的高度是
H
=
⌊
lg
n
⌋
H = \lfloor \lg n \rfloor
H=⌊lgn⌋。
设高度为
h
h
h 的结点在第
x
x
x 层,则
x
=
H
−
h
x = H - h
x=H−h。
第
x
x
x 层最多有
2
x
−
1
2^{x-1}
2x−1 个结点,
2
x
−
1
=
2
H
−
h
−
1
=
2
H
/
2
h
+
1
=
2
⌊
lg
n
⌋
/
2
h
+
1
≤
2
lg
n
/
2
h
+
1
=
n
/
2
h
+
1
≤
⌈
n
/
2
h
+
1
⌉
2^{x-1} = 2^{H-h-1} = 2^H/2^{h+1} = 2^{\lfloor \lg n \rfloor}/2^{h+1} \le 2^{\lg n}/2^{h+1} = n / 2^{h+1} \le \lceil n / 2^{h+1} \rceil
2x−1=2H−h−1=2H/2h+1=2⌊lgn⌋/2h+1≤2lgn/2h+1=n/2h+1≤⌈n/2h+1⌉。
6.4 堆排序算法
HEAPSORT ( A ) \texttt{HEAPSORT}(A) HEAPSORT(A)
1 BUILD-MAX-HEAP ( A ) \texttt{BUILD-MAX-HEAP}(A) BUILD-MAX-HEAP(A)
2 for i = A . l e n g t h i = A.length i=A.length downto 2 2 2
3 exchange A [ 1 ] A[1] A[1] with A [ i ] A[i] A[i]
4 A . h e a p - s i z e = A . h e a p - s i z e − 1 A.heap\text-size = A.heap\text-size - 1 A.heap-size=A.heap-size−1
5 MAX-HEAPIFY ( A , 1 ) \texttt{MAX-HEAPIFY} (A, 1) MAX-HEAPIFY(A,1)
HEAPSORT \texttt{HEAPSORT} HEAPSORT 过程运行时间为 O ( n lg n ) O(n \lg n) O(nlgn)。因为,调用 BUILD-MAX-HEAP \texttt{BUILD-MAX-HEAP} BUILD-MAX-HEAP 的时间为 O ( n ) O(n) O(n),调用 n − 1 n-1 n−1 次 MAX-HEAPIFY \texttt{MAX-HEAPIFY} MAX-HEAPIFY,每次调用需要时间 O ( lg n ) O(\lg n) O(lgn)。
C++ 实现堆排序算法:
#include <iostream>
using namespace std;
int heap_size;
void MaxHeapify(vector<int>& A, int i) {
int left = (i << 1) + 1;
int right = left + 1;
int largest = i;
if (left < heap_size && A[left] > A[i]) {
largest = left;
}
if (right < heap_size && A[right] > A[largest]) {
largest = right;
}
if (largest != i) {
swap(A[i], A[largest]);
MaxHeapify(A, largest);
}
}
// 迭代实现 MaxHeapify
void MaxHeapify2(vector<int>& A, int i) {
while (i < heap_size) {
int left = (i << 1) + 1;
int right = left + 1;
int largest = i;
if (left < heap_size && A[left] > A[i]) {
largest = left;
}
if (right < heap_size && A[right] > A[largest]) {
largest = right;
}
if (largest != i) {
swap(A[i], A[largest]);
i = largest;
}
else {
break;
}
}
}
void BuildMaxHeap(vector<int>& A) {
heap_size = A.size();
for (int i = A.size() / 2; i >= 0; --i) {
MaxHeapify(A, i);
}
}
void Heapsort(vector<int>& A) {
BuildMaxHeap(A);
for (int i = A.size() - 1; i >= 1; --i) {
swap(A[0], A[i]);
heap_size = heap_size - 1;
MaxHeapify(A, 0);
}
}
int main() {
vector<int> A{5, 13, 2, 25, 7, 17, 20, 8, 4};
Heapsort(A);
for (int i = 0; i < A.size(); ++i) {
cout << A[i] << " ";
}
cout << endl;
}
练习
6.4-1 以图6.4 作为模型,作出
HEAPSORT
\texttt{HEAPSORT}
HEAPSORT 作用在数组
A
=
<
5
,
13
,
2
,
25
,
7
,
17
,
20
,
8
,
4
>
A = < 5, 13, 2, 25, 7, 17, 20, 8, 4 >
A=<5,13,2,25,7,17,20,8,4> 上操作的图示。
解:
6.4-2 如果使用下面的循环不变式,讨论
HEAPSORT
\texttt{HEAPSORT}
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 个最大元素,且已排序。
解: 正确。
初始化: 在第一迭代前,
i
=
n
i = n
i=n。因为已调用
BUILD-MAX-HEAP
\texttt{BUILD-MAX-HEAP}
BUILD-MAX-HEAP,所以
A
[
1..
n
]
A[1..n]
A[1..n] 是一个最大堆,包含
n
n
n 个最小元素,子数组
A
[
n
+
1..
n
]
A[n+1 .. n]
A[n+1..n] 为空,包含 0 个已排序的最大元素。
保持:
A
[
1
]
A[1]
A[1] 是
A
[
1..
i
]
A[1..i]
A[1..i] 中最大的元素,且
A
[
1..
i
]
A[1..i]
A[1..i] 中的元素都比
A
[
i
+
1..
n
]
A[i+1..n]
A[i+1..n] 中的元素小,所以交换
A
[
1
]
A[1]
A[1] 和
A
[
i
]
A[i]
A[i] 的值,得
A
[
i
.
.
n
]
A[i..n]
A[i..n] 包含
n
−
i
+
1
n-i+1
n−i+1 个元素,且已排序。
MAX-HEAPIFY
\texttt{MAX-HEAPIFY}
MAX-HEAPIFY 的调用保持了
A
[
1..
i
−
1
]
A[1..i-1]
A[1..i−1] 的最大堆性质。在 for 循环中递减,为下一次迭代重新建立了循环不变式。
终止: 过程终止时,
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] 是已排序的
n
−
1
n-1
n−1 个元素,所以
A
[
1..
n
]
A[1..n]
A[1..n] 是已排序的
n
n
n 个元素。
6.4-3 数组
A
A
A 元素个数为
n
n
n,如果
A
A
A 已按升序排序,那么作用在
A
A
A 上的
HEAPSORT
\texttt{HEAPSORT}
HEAPSORT 的运行时间是多少?如果
A
A
A 降序排序呢?
解: 如果
A
A
A 是升序排序,
HEAPSORT
\texttt{HEAPSORT}
HEAPSORT 过程运行时间为
O
(
n
lg
n
)
O(n \lg n)
O(nlgn)。因为,建堆时每次调用
MAX-HEAPIFY
\texttt{MAX-HEAPIFY}
MAX-HEAPIFY 都要进行最大次数的交换,时间为
O
(
n
)
O(n)
O(n);接着调用
n
−
1
n-1
n−1 次
MAX-HEAPIFY
\texttt{MAX-HEAPIFY}
MAX-HEAPIFY,每次调用需要时间
O
(
lg
n
)
O(\lg n)
O(lgn)。
如果
A
A
A 是降序排序,
HEAPSORT
\texttt{HEAPSORT}
HEAPSORT 过程运行时间为
O
(
n
lg
n
)
O(n \lg n)
O(nlgn)。因为,建堆时每次调用
MAX-HEAPIFY
\texttt{MAX-HEAPIFY}
MAX-HEAPIFY 只进行一次交换,共
⌊
n
/
2
⌋
\lfloor n /2 \rfloor
⌊n/2⌋ 次,时间为
O
(
n
)
O(n)
O(n);接着调用
n
−
1
n-1
n−1 次
MAX-HEAPIFY
\texttt{MAX-HEAPIFY}
MAX-HEAPIFY,每次调用需要时间
O
(
lg
n
)
O(\lg n)
O(lgn)。
6.4-4 证明:
HEAPSORT
\texttt{HEAPSORT}
HEAPSORT 的最坏情况运行时间是
Ω
(
n
lg
n
)
\Omega(n \lg n)
Ω(nlgn)。
证: 由练习6.2-6,知
MAX-HEAPIFY
\texttt{MAX-HEAPIFY}
MAX-HEAPIFY 的最坏情况的运行时间为
Ω
(
lg
n
)
\Omega(\lg n)
Ω(lgn)。
BUILD-MAX-HEAP
\texttt{BUILD-MAX-HEAP}
BUILD-MAX-HEAP 调用
⌊
n
/
2
⌋
\lfloor n /2 \rfloor
⌊n/2⌋ 次
MAX-HEAPIFY
\texttt{MAX-HEAPIFY}
MAX-HEAPIFY,所以,它运行时间为
Ω
(
n
lg
n
)
\Omega(n \lg n)
Ω(nlgn)。
接着调用
n
−
1
n-1
n−1 次
MAX-HEAPIFY
\texttt{MAX-HEAPIFY}
MAX-HEAPIFY,运行时间为
Ω
(
n
lg
n
)
\Omega(n \lg n)
Ω(nlgn)。
所以,
HEAPSORT
\texttt{HEAPSORT}
HEAPSORT 的最坏情况运行时间是
Ω
(
n
lg
n
)
\Omega(n \lg n)
Ω(nlgn)。
6.4-5 证明:当所有元素都不同时,
HEAPSORT
\texttt{HEAPSORT}
HEAPSORT 的最佳情况运行时间是
Ω
(
n
lg
n
)
\Omega(n \lg n)
Ω(nlgn)。
证: 当所有元素都不同时,建队后,因为将
A
[
1
]
A[1]
A[1] 和
A
[
i
]
A[i]
A[i] 进行了交换,所以每次调用
MAX-HEAPIFY
\texttt{MAX-HEAPIFY}
MAX-HEAPIFY 需要时间为
Θ
(
lg
i
)
\Theta(\lg i)
Θ(lgi),其中,
i
=
n
−
1
,
n
−
2
,
.
.
.
,
1
i = n-1,n-2,...,1
i=n−1,n−2,...,1。
所以建队后的运行时间为
∑
i
=
n
−
1
1
Θ
(
lg
i
)
=
Θ
(
lg
(
(
n
−
1
)
!
)
)
\sum_{i=n-1}^{1} \Theta(\lg i) = \Theta(\lg((n-1)!))
∑i=n−11Θ(lgi)=Θ(lg((n−1)!))。
由斯特林公式
可得出
n
!
n!
n! 与
n
n
n^n
nn 复杂度相同,所以建队后的运行时间为
Θ
(
lg
(
n
n
)
)
=
Θ
(
n
lg
n
)
=
Ω
(
n
lg
n
)
\Theta(\lg(n^n)) = \Theta(n\lg n) = \Omega(n \lg n)
Θ(lg(nn))=Θ(nlgn)=Ω(nlgn)。