1 引言
网上对堆排序都已经讲得很清楚,唯一不清楚的部分是运行时间即时间复杂度的分析,因此本篇博客通过《算法导论》里面堆排序的内容,详细讲解堆排序的时间复杂度。
2 堆排序
堆是一个完全二叉树,任意一节点的值大于子节点的值称为大根堆,任意一节点的值小于子节点的值称为小根堆。一般来说,堆可以用一个一维数组 A [ n ] A[n] A[n]表示。
2.1 基本操作
2.1.1 获取父、子节点
由于堆由数组表示,因此可以根据下标计算父子节点,其运行时间为
Θ
(
1
)
\Theta(1)
Θ(1)
PARENT(i)
return i/2
LEFT(i)
return 2i
RIGHT(i)
return 2i+1
2.1.2 维护堆的性质
操作MAX-HEAPIFY(A,i)是维护大根堆的性质,其作用是,在元素
i
i
i的子树都是大根堆的情况下,使以
i
i
i为根节点的子树遵循大根堆的性质;此操作的伪代码如下:
MAX-HEAPIFY(A,i)
i = LEFT(i)
r = RIGHT(i)
if l <= A.heap-size and A[l] > A[i]
largest = l
else
largest = i
if r <= A.heap-size and A[r] > A[largest]
largest = r
if largest != i
exchange A[i] with A[largest]
MAX-HEAPIFY(A,largest)
伪代码前两个if是找到元素i以及其子节点三者中最大值的节点的下标,第三个if是将i与子节点中最大值交换,并且递归调用MAX-HEAPIFY。
操作MAX-HEAPIFY(A,i)中除了递归调用MAX-HEAPIFY(A,i)的操作之外,其余的操作的运行时间均为
Θ
(
1
)
\Theta(1)
Θ(1);那么MAX-HEAPIFY对于子树的时间代价最多为多少呢,可以用如下递归式表示:
T
(
n
)
≤
T
(
2
n
/
3
)
+
Θ
(
1
)
T(n) \leq T(2n/3) + \Theta(1)
T(n)≤T(2n/3)+Θ(1)
其中
Θ
(
1
)
\Theta(1)
Θ(1)代表着其余非递归的操作,
T
(
2
n
/
3
)
T(2n/3)
T(2n/3)代表子树递归调用的最大值,因为每个孩子节点的子树最多为
2
3
n
\frac{2}{3}n
32n(当最底层恰好半满的时候)。
如上图,就是一个半满的完全二叉树,假设节点的右子树节点个数为
2
k
−
1
2^k - 1
2k−1,那么左子树节点个数为
2
k
+
1
−
1
2^{k+1} - 1
2k+1−1,总节点个数为
2
k
+
2
k
+
1
−
1
2^k + 2^{k+1} -1
2k+2k+1−1,左子树占总数的比例为:
2
k
+
1
−
1
2
k
+
2
k
+
1
−
1
\frac{2^{k+1} - 1}{2^k + 2^{k+1} -1}
2k+2k+1−12k+1−1,当
k
→
∞
k\rightarrow \infty
k→∞时,这个比例为
2
3
\frac23
32。因此可以用表达式
T
(
n
)
≤
T
(
2
n
/
3
)
+
Θ
(
1
)
T(n) \leq T(2n/3) + \Theta(1)
T(n)≤T(2n/3)+Θ(1)来刻画MAX-HEAPIFY操作的运行时间;根据主定理(主定理参考之前的分治策略)求解得到运行时间为
T
(
n
)
=
O
(
l
g
n
)
T(n)=O(lgn)
T(n)=O(lgn),即高为
h
h
h的堆的运行时间为
O
(
h
)
O(h)
O(h)。
2.2 建堆
建堆是从堆从小到大的非叶子节点开始,堆中
⌊
n
2
⌋
\lfloor \frac n2\rfloor
⌊2n⌋以上的都是叶子节点。因此建堆过程BUILD-MAX-HEAP的伪代码如下:
BUILD-MAX-HEAP(A)
A.heap-size = A.length
for i = A.length/2 downto 1
MAX-HEAPIFY(A,i)
由上述伪代码中,每次调用MAX-HEAPIFY的时间为
O
(
l
g
n
)
O(lgn)
O(lgn),总共调用
O
(
n
)
O(n)
O(n)次,因此可以估算建堆的运行时间为
O
(
n
l
g
n
)
O(nlgn)
O(nlgn),但是很明显,这个上界不够精确。
MAX-HEAPIFY的时间与树的高度有关,而大多数节点高度都不高。利用如下性质可以得到更精确的运行时间:高度为
h
h
h的堆最多包含
⌈
n
2
h
+
1
⌉
\lceil \frac{n}{2^{h+1}} \rceil
⌈2h+1n⌉个节点;
在一个高度为
h
h
h的节点上运行MAX-HEAPIFY的代价是
O
(
h
)
O(h)
O(h),因此建堆(BUILD-MAX-HEAP)的总代价为:
∑
h
=
0
⌊
l
g
n
⌋
⌈
n
2
h
+
1
⌉
O
(
h
)
=
O
(
n
∑
h
=
0
⌊
l
g
n
⌋
h
2
h
)
\sum_{h=0}^{\lfloor lgn \rfloor}\lceil \frac{n}{2^{h+1}} \rceil O(h)=O(n\sum_{h=0}^{\lfloor lgn \rfloor}\frac{h}{2^h})
h=0∑⌊lgn⌋⌈2h+1n⌉O(h)=O(nh=0∑⌊lgn⌋2hh)
其中有:
∑
h
=
0
∞
h
2
h
=
2
\sum_{h=0}^{\infty}\frac{h}{2^h}=2
∑h=0∞2hh=2(根据公式:
∑
k
=
0
∞
k
x
k
=
x
(
1
−
x
)
2
\sum_{k=0}^{\infty}kx^k=\frac{x}{(1-x)^2}
∑k=0∞kxk=(1−x)2x);
因此有:
O
(
n
∑
h
=
0
⌊
l
g
n
⌋
h
2
h
)
=
O
(
n
∑
h
=
0
∞
h
2
h
)
=
O
(
n
)
O(n\sum_{h=0}^{\lfloor lgn \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)
所以,建堆所花时间为
O
(
n
)
O(n)
O(n)
2.3 堆排序算法
堆排序算法先建立堆,然后通过堆与末尾元素互换,调整堆,重复这个过程,最终完成排序,其伪代码如下:
HEAPSORT(A)
BUILD-MAX-HEAP(A)
for i = A.length downto 2
exchange A[1] with A[i]
A.heap-size = A.heap-size - 1
MAX-HEAPIFY(A, 1)
其中,BUILD-MAX-HEAP运行时间为 O ( n ) O(n) O(n),执行了 n − 1 n-1 n−1次MAX-HEAPIFY,每次时间为 O ( l g n ) O(lgn) O(lgn),因此,堆排序的时间复杂度为 O ( n l g n ) O(nlgn) O(nlgn)。