在线时间 0 小时 最后登录 2007-9-23 注册时间 2004-9-10 听众数 3 收听数 0 能力 0 分 体力 9975 点 威望 7 点 阅读权限 150 积分 4048 相册 0 日志 0 记录 0 帖子 1893 主题 823 精华 2 分享 0 好友 0
我的地盘我做主
该用户从未签到
< >可以运用分而治之方法来解决排序问题,该问题是将n 个元素排成非递减顺序。分而治之方法通常用以下的步骤来进行排序算法:若n 为1,算法终止;否则,将这一元素集合分割成两个或更多个子集合,对每一个子集合分别排序,然后将排好序的子集合归并为一个集合。
6 S4 j5 P2 a h# y& H0 { 7 G' Q3 p8 }( q) p% I4 o: d; m
假设仅将n 个元素的集合分成两个子集合。现在需要确定如何进行子集合的划分。一种可能性就是把前面n- 1个元素放到第一个子集中(称为A),最后一个元素放到第二个子集里(称为B)。按照这种方式对A递归地进行排序。由于B仅含一个元素,所以它已经排序完毕,在A排完序后,只需要用程序2 - 1 0中的函数i n s e r t将A和B合并起来。把这种排序算法与I n s e r t i o n S o r t(见程序2 - 1 5)进行比较,可以发现这种排序算法实际上就是插入排序的递归算法。该算法的复杂性为O (n 2 )。把n 个元素划分成两个子集合的另一种方法是将含有最大值的元素放入B,剩下的放入A中。然后A被递归排序。为了合并排序后的A和B,只需要将B添加到A中即可。假如用函数M a x(见程序1 - 3 1)来找出最大元素,这种排序算法实际上就是S e l e c t i o n S o r t(见程序2 - 7)的递归算法。
0 [! ?* M1 W1 o# |* l& Z- G : a# _+ ^4 o2 I6 K7 l0 Y, K! W6 w
假如用冒泡过程(见程序2 - 8)来寻找最大元素并把它移到最右边的位置,这种排序算法就是B u b b l e S o r t(见程序2 - 9)的递归算法。这两种递归排序算法的复杂性均为(n2 )。若一旦发现A已经被排好序就终止对A进行递归分割,则算法的复杂性为O(n2 )(见例2 - 1 6和2 - 1 7)。& e; r; `3 r- u9 m! w
3 a. h1 c0 g! ~3 ]8 u
上述分割方案将n 个元素分成两个极不平衡的集合A和B。A有n- 1个元素,而B仅含一个元素。下面来看一看采用平衡分割法会发生什么情况: A集合中含有n/k 个元素,B中包含其余的元素。递归地使用分而治之方法对A和B进行排序。然后采用一个被称之为归并( m e rg e)的过程,将已排好序的A和B合并成一个集合。
. N3 R4 W2 G" g3 d' a 5 m3 T# t. \) w! v) s' x$ k s
例2-5 考虑8个元素,值分别为[ 1 0,4,6,3,8,2,5,7 ]。如果选定k = 2,则[ 1 0 , 4 , 6 , 3 ]和[ 8 , 2 , 5 , 7 ]将被分别独立地排序。结果分别为[ 3 , 4 , 6 , 1 0 ]和[ 2 , 5 , 7 , 8 ]。从两个序列的头部开始归并这两个已排序的序列。元素2比3更小,被移到结果序列;3与5进行比较,3被移入结果序列;4与5比较,4被放入结果序列;5和6比较,.。如果选择k= 4,则序列[ 1 0 , 4 ]和[ 6 , 3 , 8 , 2 , 5 , 7 ]将被排序。排序结果分别为[ 4 , 1 0 ]和[ 2 , 3 , 5 , 6 , 7 , 8 ]。当这两个排好序的序列被归并后,即可得所需要的排序序列。
7 J v2 H s( x6 W+ K % x9 R) z: @- g. J; ?, g) T
图2 - 6给出了分而治之排序算法的伪代码。算法中子集合的数目为2,A中含有n/k个元素。
3 {& \1 R4 K8 S8 d& w
% Z. b5 W8 z, s6 ~3 S$ K5 N7 H+ k, a template<CLASS T>
; F5 w& y& [; h7 J) _) P0 u3 L
6 P0 p, |" R S: i6 B) t0 j2 m2 G void sort( T E, int n)
4 N0 _5 o- T+ b# M, v9 Y* R
' y! {1 ?1 W/ U# | { / /对E中的n 个元素进行排序, k为全局变量
0 i) S5 p3 R0 p+ O) f
" f' s" s; n7 ^7 m: P if (n >= k) {7 |7 D; p# i6 w* L5 _( d0 A
1 R# [7 e" q; C
i = n/k;" m. _9 o7 j6 v F9 U
4 O& S l7 T( o+ [ j = n-i;
; A/ T2 G7 m* {+ k. k% \$ V( \0 x/ ?- k
. c1 i1 ?) V' y! ` T/ n, s 令A 包含E中的前i 个元素9 |: n+ I1 x# i( k& Z8 u
0 b& H6 I0 K9 o9 w6 {
令B 包含E中余下的j 个元素
, ]8 D9 @$ i/ }0 l2 W( f: e2 n 9 W2 U5 L2 f- Q( J/ a
s o r t ( A , i ) ;5 W: l: F6 p3 S/ r5 H- ~
5 O8 o. ~+ R; M3 R
s o r t ( B , j ) ;0 K) m4 m% ?0 o" y6 J; ~: Y* s4 _4 R
7 z' q. `' R; ^* i' F+ M' l
m e rge(A,B,E,i,j,); //把A 和B 合并到E o$ n( W8 x+ V- k
) _7 R1 J, n# Z; O2 t \. H
}/ m: p6 w1 g) e' Y
3 O: |2 s+ D+ P) n6 | else 使用插入排序算法对E 进行排序
5 P6 i8 k! ` U/ m; q' n
4 n: h# r# w% Q( D3 W) w9 [8 Q }
. q: U1 V2 L3 V, X & s! v: }6 M) `$ A- m6 ^) K0 C5 J4 z
图14-6 分而治之排序算法的伪代码. f M& U! p ]3 t) `
. A1 Z( A: e* J9 H
! j! O9 A& a2 v8 m! a1 S. L
, O7 z# R- O: o+ y+ e, J 从对归并过程的简略描述中,可以明显地看出归并n个元素所需要的时间为O (n)。设t (n)为分而治之排序算法(如图1 4 - 6所示)在最坏情况下所需花费的时间,则有以下递推公式:! Y1 V+ A2 j: @9 P% ^
7 p4 c! w) B; c2 c+ C9 \ 其中c 和d 为常数。当n / k≈n-n / k 时,t (n) 的值最小。因此当k= 2时,也就是说,当两个子集合所包含的元素个数近似相等时, t (n) 最小,即当所划分的子集合大小接近时,分而治之算法通常具有最佳性能。
: ]6 E+ L9 Y( b9 l+ Z* t. ~ p: f
0 n$ a5 M! k) z7 Q" X( q! o 可以用迭代方法来计算这一递推方式,结果为t(n)= (nl o gn)。虽然这个结果是在n为2的幂时得到的,但对于所有的n,这一结果也是有效的,因为t(n) 是n 的非递减函数。t(n) =(nl o gn) 给出了归并排序的最好和最坏情况下的复杂性。由于最好和最坏情况下的复杂性是一样的,因此归并排序的平均复杂性为t (n)= (nl o gn)。* i( }+ O f. f& i% v
! z# e" h& l& ]/ z
图2 - 6中k= 2的排序方法被称为归并排序( m e rge sort ),或更精确地说是二路归并排序(two-way merge sort)。下面根据图1 4 - 6中k= 2的情况(归并排序)来编写对n 个元素进行排序的C + +函数。一种最简单的方法就是将元素存储在链表中(即作为类c h a i n的成员(程序3 -8))。在这种情况下,通过移到第n/ 2个节点并打断此链,可将E分成两个大致相等的链表。! A0 L% h- M s+ {
) R# y( D( d6 a+ w* Z7 K; k, g 归并过程应能将两个已排序的链表归并在一起。如果希望把所得到C + +程序与堆排序和插入排序进行性能比较,那么就不能使用链表来实现归并排序,因为后两种排序方法中都没有使用链表。为了能与前面讨论过的排序函数作比较,归并排序函数必须用一个数组a来存储元素集合E,并在a 中返回排序后的元素序列。为此按照下述过程来对图1 4 - 6的伪代码进行细化:当集合E被化分成两个子集合时,可以不必把两个子集合的元素分别复制到A和B中,只需简单地在集合E中保持两个子集合的左右边界即可。接下来对a 中的初始序列进行排序,并将所得到的排序序列归并到一个新数组b中,最后将它们复制到a 中。图1 4 - 6的改进版见图1 4 - 7。
! O6 b- D: ?- Y5 J3 N; n * z! Y3 y0 u& q
! D$ Q) W9 H* T9 t: }
4 Z% B, C! W" k. Y3 t0 Q
template<CLASS T>: T; r2 h1 R: Z+ V5 Q+ F
9 L/ [7 f* Z0 x: l- ]% o* e M e rgeSort( T a[], int left, int right)# k& b' W$ I1 D
0 D8 V2 \" H. _0 B% z* f, k' t0 N, @ { / /对a [ l e f t : r i g h t ]中的元素进行排序: _7 I: w, Z0 @$ d
9 x' Z8 v5 J+ s V" x7 s3 f if (left < right) {//至少两个元素6 J _! m1 |& D$ m4 h* c, \
" Z2 I, ^; I2 b' W
int i = (left + right)/2; //中心位置! D+ ?; K8 T. f+ P. s' ^4 S
) z6 f5 C Y" Z# M M e rgeSort(a, left, i);
J0 Z0 W6 {" D' ?) f/ D4 w + z; U3 n. U6 N3 i2 j4 a
M e rgeSort(a, i+1, right);! m4 ` M0 {; y2 q
0 n6 {" i& K0 q/ |0 X6 D M e rge(a, b, left, i, right); //从a 合并到b
8 T$ i5 L5 T0 v; m; T* ?" o) {
5 V5 c9 ` O% Q4 T- b3 ` Copy(b, a, left, right); //结果放回a
' d3 H+ T1 m5 p; A6 n- @# V, D0 P 6 y! {) _" S$ C! B( f6 g
}
( v; E0 I7 }' C! a) O0 L 4 y; k* u, w# x3 k! ~4 ^
}& ~1 U4 {) S7 Q6 T; v6 v& d
/ n) Q# ]& f7 _: F7 z, P" X
图14-7 分而治之排序算法的改进- W8 E" c" [& b1 t
. F( y0 d: s8 M
) h, v- `5 Y- q4 h) M
: f4 j5 ~) G2 S6 T 可以从很多方面来改进图1 4 - 7的性能,例如,可以容易地消除递归。如果仔细地检查图1 4 - 7中的程序,就会发现其中的递归只是简单地重复分割元素序列,直到序列的长度变成1为止。当序列的长度变为1时即可进行归并操作,这个过程可以用n 为2的幂来很好地描述。长度为1的序列被归并为长度为2的有序序列;长度为2的序列接着被归并为长度为4的有序序列;这个过程不断地重复直到归并为长度为n 的序列。图1 4 - 8给出n= 8时的归并(和复制)过程,方括号表示一个已排序序列的首和尾。) D6 b, Z7 @% q' l+ Z
1 F5 E* u: l8 x4 w( J, a
0 |/ b2 F( j6 I7 P2 a
- ]& j% C+ j, f( K* }0 J 初始序列[8] [4] [5] [6] [2] [1] [7] [3]* {/ U+ v3 z: P- E' L7 R
+ w, W& V N5 X; J# s
归并到b [4 8] [5 6] [1 2] [3 7]/ x: N* U3 i+ |4 X9 J" g4 i! W
# f4 ^: y: m) f4 P6 p; W @ 复制到a [4 8] [5 6] [1 2] [3 7]
5 A! s& ~ f) h0 S- f* ?- X( f0 l
5 N2 _. x2 s6 s; f. o$ e 归并到b [4 5 6 8] [1 2 3 7]- D0 e$ h$ H" d) ]$ |/ k* |
3 d6 }! O+ v9 c1 |% f' ?
复制到a [4 5 6 8] [1 2 3 7]# r# m, d5 |' C' T( @6 j3 l" I
9 b/ J6 n/ I. M L$ T
归并到b [1 2 3 4 5 6 7 8]: w, M5 c* d9 N: y6 I
: Q$ o) _( A' h, S% J8 q% n
复制到a [1 2 3 4 5 6 7 8]% g/ s; s1 M7 O; X
" j2 Y) d( K1 Y y d0 X4 w( V# O
图14-8 归并排序的例子: u2 ~9 s! z( `& b# T
9 v* {1 Q- W# l! R3 z- g! s
, B! v/ M+ X e Q
q; @( ~. ~+ o7 ~ 另一种二路归并排序算法是这样的:首先将每两个相邻的大小为1的子序列归并,然后对上一次归并所得到的大小为2的子序列进行相邻归并,如此反复,直至最后归并到一个序列,归并过程完成。通过轮流地将元素从a 归并到b 并从b 归并到a,可以虚拟地消除复制过程。二路归并排序算法见程序1 4 - 3。
- j( \1 U+ S* J( o' I L# u/ k) S" m8 P3 F2 v
程序14-3 二路归并排序
- e8 Y' m8 M& g$ |3 _6 p/ r6 X
( T7 W* s( D$ u template<CLASS T>2 R) A7 z: P- z3 ^7 @# T6 K- a
- h( h8 S2 Q9 k: p
void MergeSort(T a[], int n)
, H& a! `1 ^* }
8 _0 c7 j( c [8 ?# T: { {// 使用归并排序算法对a[0:n-1] 进行排序; L1 e4 j$ c2 _( L+ d
( y6 p+ l5 r C$ w9 R T *b = new T [n];5 U$ k% G7 r: w" M% P; L
0 d8 u- t) U* d5 Q) ^7 L int s = 1; // 段的大小6 l) s. u ]' r; O& A
& c: |2 q$ t/ D6 w
while (s < n) {, ?7 ^; ^9 m8 W; G% v; A
8 ?4 p( ]. j9 C MergePass(a, b, s, n); // 从a归并到b
8 H' L' q6 F/ h% W
1 `. F- x1 Z( |9 { s += s;
9 _; a8 r& s$ e3 P6 v
! n( j/ }3 j, Z5 a+ v1 z MergePass(b, a, s, n); // 从b 归并到a
- e( g- W$ w1 ~) X * N& a1 _/ _: j# d
s += s;
7 S5 u1 X: Z1 g$ q4 T/ b& E1 I
/ l2 ^3 k. C t% t* ?) S/ k5 S9 H5 [ }$ ~ S" ~8 o4 h6 T5 L% l( @
, @! Z, B1 e% K+ m* O
}' N% m- W- s6 h3 E% i7 ?
6 p1 v0 n; E& ]; e 为了完成排序代码,首先需要完成函数M e rg e P a s s。函数M e rg e P a s s(见程序1 4 - 4)仅用来确定欲归并子序列的左端和右端,实际的归并工作由函数M e rg e (见程序1 4 - 5 )来完成。函数M e rg e要求针对类型T定义一个操作符< =。如果需要排序的数据类型是用户自定义类型,则必须重载操作符< =。这种设计方法允许我们按元素的任一个域进行排序。重载操作符< =的目的是用来比较需要排序的域。4 }7 i# |, W) L9 Z- D
8 O a0 t; N& A0 B4 _: b# K4 \" i
程序14-4 MergePass函数0 c: t0 T1 ^8 T0 T. s$ `
, b" a& V0 w2 e8 W! u
template<CLASS T>
: @6 g, O1 V. r1 F1 b; o1 `+ y. r* v% g
: B2 Y/ K0 R* Z void MergePass(T x[], T y[], int s, int n)- Z& a9 Q3 F: n
2 ^0 U! X) c$ M0 e {// 归并大小为s的相邻段5 }& g, A( q1 N4 f% B8 n3 u
7 n- k) }; O2 @3 H int i = 0;4 f7 a( p1 p2 J, q
) E0 w$ d, F) m' c6 E. o while (i <= n - 2 * s) {
: \- Q% b7 h* ^6 s
& @0 V3 m" ]: \$ X // 归并两个大小为s的相邻段$ B: i/ c0 Q% [0 \" O
s6 f2 N& L! `9 u x( Z' v, a Merge(x, y, i, i+s-1, i+2*s-1);1 P' }' N# a+ {5 P7 Z- f1 W+ I
7 {9 J D ^0 c4 c/ U3 Z) P i = i + 2 * s;
9 e( G! W. P* {8 q) j" z
6 S; V+ b7 Q% G }2 ?% u m2 B9 g' h2 g
9 ~# x" h: }1 e% a6 [) _+ I, e5 V // 剩下不足2个元素" o$ ]+ P h' K) Q/ n" E, r
+ X a- Y- I) n r if (i + s < n) Merge(x, y, i, i+s-1, n-1);. y$ W' e3 c" s! }0 A3 y2 |
% K: ^& A- _' w8 `2 @
else for (int j = i; j <= n-1; j++)
" D1 y5 S6 m& u
- l" u P" u# K! z // 把最后一段复制到y4 C( y$ Z0 [) L, a" K( `3 n: e
9 ~* ]# i8 x$ P4 E4 n9 X
y[j] = x[j]; Q2 T& S7 Q! D* `# j, I
1 Q" r7 |4 A9 t0 H1 n0 ^" @5 i }: R, D" ~9 t8 S) V3 ^% l
+ a: `5 U( ]4 Q, V 程序14-5 Merge函数. \8 `) D1 g6 p
3 ?( ^: i" C6 Q. Y template<CLASS T>9 z, @% k4 _9 x
( T9 v% k9 k; v2 l O+ M void Merge(T c[], T d[], int l, int m, int r)/ s& C9 Y) j: J8 L7 b
# x; S: b0 K& p/ h' x
{// 把c[l:m]] 和c[m:r] 归并到d [ l : r ] .) N& Y& S7 k, w D, J' p1 f
" F9 W) C5 u0 N- d# y
int i = l, // 第一段的游标
; T+ b4 b1 |: v- I0 x% a9 V , X `5 @& j7 y2 H
j = m+1, // 第二段的游标* N, X1 l* U3 n
+ a2 X; e7 K. N1 }
k = l; // 结果的游标
- }* k/ L1 k; z8 m" i( f. j ! J, z: n) W' f8 Y% S6 e" g! W
/ /只要在段中存在i和j,则不断进行归并
' ? }, p0 O. d) C* _- } % v% Q9 J# ~6 y' \
while ((i <= m) && (j <= r))8 \; E; N! C! R4 j. ^
- t' s" |$ Q: F. a6 n0 K/ C if (c <= c[j]) d[k++] = c[i++];
/ a0 V/ |: o( |& \8 N
# ?7 q- _- g W else d[k++] = c[j++];' `, d; X6 s5 N0 {' g0 ~9 }
2 |9 X2 h$ j0 m: D
// 考虑余下的部分
: q* \& `9 Y: c# U* ~2 Z
@: X5 s6 C v if (i > m) for (int q = j; q <= r; q++)
7 _ {6 O' e/ ^3 H9 F ; h1 G- x8 n" W6 \# W% x- o: F# u
d[k++] = c[q];7 l7 k6 z- |; ^$ W2 a- r7 a) w; O I
- V, w- H2 V9 J3 Y% c' O7 z
else for (int q = i; q <= m; q++)
- ]) P' Z6 }. O$ d. J+ ^7 ]; i! k2 } 8 T- a% ~5 I/ ^* C# u4 v- Z
d[k++] = c[q];
* L- w: ?+ ~& ^
9 {; t; O) O8 L( c/ G }/ D* E& r0 \ |. Z/ \
4 |# X% n: Z6 C+ U( w
自然归并排序(natural merge sort)是基本归并排序(见程序1 4 - 3)的一种变化。它首先对输入序列中已经存在的有序子序列进行归并。例如,元素序列[ 4,8,3,7,1,5,6,2 ]中包含有序的子序列[ 4,8 ],[ 3,7 ],[ 1,5,6 ]和[ 2 ],这些子序列是按从左至右的顺序对元素表进行扫描而产生的,若位置i 的元素比位置i+ 1的元素大,则从位置i 进行分割。对于上面这个元素序列,可找到四个子序列,子序列1和子序列2归并可得[ 3 , 4 , 7 , 8 ],子序列3和子序列4归并可得[ 1 , 2 , 5 , 6 ],最后,归并这两个子序列得到[ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ]。因此,对于上述元素序列,仅仅使用了两趟归并,而程序1 4 - 3从大小为1的子序列开始,需使用三趟归并。作为一个极端的例子,假设输入的元素序列已经排好序并有n个元素,自然归并排序法将准确地识别该序列不必进行归并排序,但程序1 4 - 3仍需要进行[ l o g2 n] 趟归并。因此自然归并排序将在(n) 的时间内完成排序。而程序1 4 - 3将花费(n l o gn) 的时间。</P>
zan