< align=center><B></B> </P># j! f" w3 M9 P" `$ r/ U) L
<>分而治之方法还可以用于实现另一种完全不同的排序方法,这种排序法称为快速排序(quick sort)。在这种方法中, n 个元素被分成三段(组):左段l e f t,右段r i g h t和中段m i d d l e。中段仅包含一个元素。左段中各元素都小于等于中段元素,右段中各元素都大于等于中段元素。因此l e f t和r i g h t中的元素可以独立排序,并且不必对l e f t和r i g h t的排序结果进行合并。m i d d l e中的元素被称为支点( p i v o t )。图1 4 - 9中给出了快速排序的伪代码。 % ^8 ^) S, o }& ~0 K w8 ?0 y, }5 B0 f& B
# f# U/ ~9 ]4 K. m/ /使用快速排序方法对a[ 0 :n- 1 ]排序* h4 L) Q! s2 D$ B
# s9 S1 q1 w) Y从a[ 0 :n- 1 ]中选择一个元素作为m i d d l e,该元素为支点 C# G5 w& q% e! E$ _4 s: V) q" @# d9 F: N l( M
把余下的元素分割为两段left 和r i g h t,使得l e f t中的元素都小于等于支点,而right 中的元素都大于等于支点 ( |0 _" }% `2 c% p1 `6 a. u* G8 C( u) ]
递归地使用快速排序方法对left 进行排序! B1 M% ]" t% w
3 Z; F& z7 m! O* z
递归地使用快速排序方法对right 进行排序- C1 d( e4 w6 l. @% I
# I6 W& E, K; l: b6 ~
所得结果为l e f t + m i d d l e + r i g h t 5 O0 D, z8 y+ s7 k2 V# J6 c* @2 H7 r* R& g! d: @' C
图14-9 快速排序的伪代码 ~1 t0 K$ @5 X + A1 Y1 `& U$ }' L' C. X! m2 X- X+ v1 I% c& \
考察元素序列[ 4 , 8 , 3 , 7 , 1 , 5 , 6 , 2 ]。假设选择元素6作为支点,则6位于m i d d l e;4,3,1,5,2位于l e f t;8,7位于r i g h t。当left 排好序后,所得结果为1,2,3,4,5;当r i g h t排好序后,所得结果为7,8。把right 中的元素放在支点元素之后, l e f t中的元素放在支点元素之前,即可得到最终的结果[ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ]。5 k5 C2 j6 k/ Y( i* e
! a7 X# [/ V, u3 Q2 c) P把元素序列划分为l e f t、m i d d l e和r i g h t可以就地进行(见程序1 4 - 6)。在程序1 4 - 6中,支点总是取位置1中的元素。也可以采用其他选择方式来提高排序性能,本章稍后部分将给出这样一种选择。 6 j0 t8 N1 w: t. J % U! O7 N& A4 K2 G- w4 X6 q6 W程序14-6 快速排序$ _/ w7 i" ]! O* c
* ^; C3 [- F8 T3 p
template<CLASS T>- z b4 p% B( ^ o
+ U `4 s% d4 I: r- K( F3 Q3 K
void QuickSort(T*a, int n). N+ v) V' V& \! \) h/ k
* c( ^1 I5 f' K' U8 w" I: O2 b: w+ x{// 对a[0:n-1] 进行快速排序, b2 K$ P' s9 w- v2 ]
. K9 H6 \; V( D* J9 t3 a{// 要求a[n] 必需有最大关键值. `) b# L8 L. r; x( x" I
% O I" M3 M3 B, y" I
quickSort(a, 0, n-1); / m0 Z2 b) ]5 V' m& o" t Z( G3 I# x" v. r3 Y$ l8 {; h, g
template<CLASS T> 0 d/ ~2 d/ S: Y3 C5 ~ + U# `5 S+ y! A; p' Avoid quickSort(T a[], int l, int r) 2 r( g( _! L, k7 ~$ H ( g- u) ]* u# A7 n- u& a: S0 M{// 排序a [ l : r ], a[r+1] 有大值# A `$ D( p+ B6 b; c% V1 m
) c0 F# }6 q3 j2 A3 x" M' H9 M: M# Jif (l >= r) return; $ T' x% S( W. e8 I: W3 ~+ y2 n" A, E8 [$ q& z1 ?* \7 D+ \
int i = l, // 从左至右的游标6 c5 h* [ L. |; Z5 n
/ e, W5 B. m* o2 O! _j = r + 1; // 从右到左的游标 % ^) c$ ~ B& l$ _8 t / `/ M$ T8 s' g4 FT pivot = a[l]; 8 t( k- z7 M/ J9 y/ T6 T; I 9 l' G5 u5 H) U' [4 l// 把左侧>= pivot的元素与右侧<= pivot 的元素进行交换$ o: X w3 W: X5 a
`* C" b8 c$ B+ r8 ? L E* s
while (true) { ' s6 Z1 b) t8 c - F f. Q8 C! p6 M6 g2 edo {// 在左侧寻找>= pivot 的元素 ) A5 ]' }) o' B! u \2 N p ( n# Z4 x3 ?5 D pi = i + 1; ! U2 t* n- ^+ E s$ Y% g9 E) P4 ]" e1 M} while (a < pivot); 4 M0 F% q) j; y" c9 z/ C / A) n# }* ^) @7 B9 e2 \do {// 在右侧寻找<= pivot 的元素 ( ]) R, p w% O8 |) X% h; M& X( t) O2 P; ?* \5 r1 `+ f
j = j - 1; ; G2 O$ z4 S8 V$ H! K: X ( y# u8 ]$ K) j# y} while (a[j] > pivot); 3 W" w# r9 s4 B; y# A2 R3 G: `! p, o! w0 ?' ^
if (i >= j) break; // 未发现交换对象 & \$ A9 H) w6 a; }2 F! c$ P7 @( Z! P2 X, Z: t
Swap(a, a[j]); 8 S! a! q A0 \; R; g; G0 ]1 E. d
}6 Q% t( c- r6 ^( `3 B, c; K6 H
; B4 j* S( r$ u7 I9 R7 Z// 设置p i v o t 1 n0 A" g s0 Z" @6 g6 p2 f H/ z& R e5 u6 ~* o( l. A- G4 b
a[l] = a[j]; ) F+ s# }/ K9 h4 |! F 5 B2 U( T! Z5 r0 C( O& E, ia[j] = pivot; 1 q- n4 r2 R5 Z+ Y . y8 X0 T4 @6 j! u8 x! t0 dquickSort(a, l, j-1); // 对左段排序 : |! A( J8 l- w7 F) G$ L# Z! m: t: p* f) k* I9 V( ~( B5 N
quickSort(a, j+1, r); // 对右段排序% t6 f0 y# s+ T3 O' U9 d
) Q* e1 }4 R4 X( q2 s& x
} 0 d( L5 F* A3 U, x! R/ |5 s' ^- \4 Y + i: j8 ]5 X/ G5 K2 W若把程序1 4 - 6中d o - w h i l e条件内的<号和>号分别修改为< =和> =,程序1 4 - 6仍然正确。实验结果表明使用程序1 4 - 6的快速排序代码可以得到比较好的平均性能。为了消除程序中的递归,必须引入堆栈。不过,消除最后一个递归调用不须使用堆栈。消除递归调用的工作留作练习(练习1 3)。程序1 4 - 6所需要的递归栈空间为O (n)。若使用堆栈来模拟递归,则可以把这个空间减少为O ( l o gn)。在模拟过程中,首先对left 和right 中较小者进行排序,把较大者的边界放入堆栈中。在最坏情况下l e f t总是为空,快速排序所需的计算时间为(n2 )。在最好情况下, l e f t和r i g h t中的元素数目大致相同,快速排序的复杂性为(nl o gn)。令人吃惊的是,快速排序的平均复杂性也是(nl o gn)。5 L9 P& M4 ?: N* g2 \2 @
. H6 ^5 l2 {4 }( e8 W定理2-1 快速排序的平均复杂性为(nl o gn)。# `+ @5 d) e# f
* v/ V% g* x1 [证明用t (n) 代表对含有n 个元素的数组进行排序的平均时间。当n≤1时,t (n)≤d,d为某一常数。当n <1时,用s 表示左段所含元素的个数。由于在中段中有一个支点元素,因此右段中元素的个数为n-s- 1。所以左段和右段的平均排序时间分别为t (s), t (n-s- 1 )。分割数组中元素所需要的时间用cn 表示,其中c 是一个常数。因为s 有同等机会取0 ~n- 1中的任何一个值. " a1 C7 T! D1 [ ' Y L7 k2 |' g- x如对(2 - 8)式中的n 使用归纳法,可得到t (n)≤kn l o ge n,其中n> 1且k=2(c+d),e~2 . 7 1 8为自然对数的基底。在归纳开始时首先验证n= 2时公式的正确性。根据公式( 1 4 - 8),可以得到t( 2 )≤2c+ 2d≤k nl o ge 2。在归纳假设部分,假定t(n)≤kn l o ge n(当2≤n<m 时,m 是任意一个比2大的整数=.3 |8 ^) S2 B" i) w4 K" F
& j+ S7 x( V+ y计数排序n2 n29 P5 a1 f- }, |9 ?
/ j; w- f3 Q1 i' w/ Z7 i; v o
插入排序n2 n2; W4 S2 D7 H7 ?( [) I* n0 O$ ?
; t- h; |' S) L3 R: H
选择排序n2 n26 n0 b, i' L/ b8 Q0 ?
1 M+ h0 i' n' \* b% i' ^+ F
堆排序nl o gn nl o gn 1 i! Y# o5 b2 w5 M: t6 r$ i1 P6 h% z4 k9 T6 `# t$ \# Q$ V
归并排序nl o gn nl o gn 2 t) R3 s, R0 e3 c) L4 s3 w$ }0 V( [ i0 [$ h
快速排序n2 nl o gn . e6 {3 {" o, v' q( x/ \0 K8 Z # R% g3 b0 A: I图14-10 各种排序算法的比较 8 c. n& ~" A$ m1 e3 Z7 y3 A3 s. p/ S: ?0 y& T: ]
( S% L% C2 x) m
中值快速排序( median-of-three quick sort)是程序1 4 - 6的一种变化,这种算法有更好的平均性能。注意到在程序1 4 - 6中总是选择a [ 1 ]做为支点,而在这种快速排序算法中,可以不必使用a [ 1 ]做为支点,而是取{a[1],a[(1+r)/2],a[r]} 中大小居中的那个元素作为支点。例如,假如有三个元素,大小分别为5,9,7,那么取7为支点。为了实现中值快速排序算法,一种最简单的方式就是首先选出中值元素并与a[1] 进行交换,然后利用程序1 4 - 6完成排序。如果a [ r ]是被选出的中值元素,那么将a[1] 与a[r] 进行交换,然后将a [ 1 ](即原来的a [ r ])赋值给程序1 4 - 6中的变量p i v o t,之后继续执行程序1 4 - 6中的其余代码。 , d( g4 L9 o% p' q( B: p8 ~5 p6 ?3 M) T
图2 - 11中分别给出了根据实验所得到的归并排序、堆排序、插入排序、快速排序的平均时间。对于每一个不同的n, 都随机产生了至少1 0 0组整数。随机整数的产生是通过反复调用s t d l i b . h库中的r a n d o m函数来实现的。如果对一组整数进行排序的时间少于1 0个时钟滴答,则继续对其他组整数进行排序,直到所用的时间不低于1 0个时钟滴答。在图2 - 11中的数据包含产生随机整数的时间。对于每一个n,在各种排序法中用于产生随机整数及其他开销的时间是相同的。因此,图2 - 11中的数据对于比较各种排序算法是很有用的。 ! _) F1 l. L, \9 b' m* ~ M6 D2 m$ j/ h" A/ c$ D' K8 L
对于足够大的n,快速排序算法要比其他算法效率更高。从图中可以看到快速排序曲线与插入排序曲线的交点横坐标比2 0略小,可通过实验来确定这个交点横坐标的精确值。可以分别用n = 1 5 , 1 6 , 1 7 , 1 8 , 1 9进行实验,以寻找精确的交点。令精确的交点横坐标为nBr e a k。当n≤nBreak 时,插入排序的平均性能最佳。当n>nBreak 时,快速排序性能最佳。当n>nBreak 时,把插入排序与快速排序组合为一个排序函数,可以提高快速排序的性能,实现方法是把程序1 4 - 6中的以下语句: % u& A' i# E/ {9 ?' @# k+ F0 z 3 X g0 k% {8 P& `! `: w3 Pif(l >= r)r e t u r n ; 0 y, ?5 ]4 T7 f& r( U9 ^' J% r8 } , a1 a& ]7 B2 |& o- l% }替换为# H6 _* \1 W3 o& A- d! r! i
4 O: q6 n+ b& t$ ^- F$ rif (r-1<NBREAK) {InsertionSort(a,l,r); return;} 5 r5 L4 M) o3 ]. a6 P 3 g/ }3 k5 x* B) L2 r这里I n s e r t i o n S o r t ( a , l , r )用来对a [ 1 : r ]进行插入排序。测量修改后的快速排序算法的性能留作练习(练习2 0)。用更小的值替换n B r e a k有可能使性能进一步提高(见练习2 0)。 8 V. n9 T5 l+ `" O- W0 J' y$ e8 Y$ g# e9 M5 h
大多数实验表明,当n>c时(c为某一常数),在最坏情况下归并排序的性能也是最佳的。而当n≤c时,在最坏情况下插入排序的性能最佳。通过将插入排序与归并排序混合使用,可以提高归并排序的性能(练习2 1)。</P>