在线时间 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,算法终止;否则,将这一元素集合分割成两个或更多个子集合,对每一个子集合分别排序,然后将排好序的子集合归并为一个集合。) b0 ~3 y& j$ z1 H, t1 F
# Y+ t# h; W" B' P- \0 e- v 假设仅将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)的递归算法。$ u+ k, I$ @" S
& f0 [$ {/ B) ]) K
假如用冒泡过程(见程序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)。
- Q1 Y2 y; A' x" E/ |
o5 a F, G' ]1 \$ @& J 上述分割方案将n 个元素分成两个极不平衡的集合A和B。A有n- 1个元素,而B仅含一个元素。下面来看一看采用平衡分割法会发生什么情况: A集合中含有n/k 个元素,B中包含其余的元素。递归地使用分而治之方法对A和B进行排序。然后采用一个被称之为归并( m e rg e)的过程,将已排好序的A和B合并成一个集合。
) l0 B3 h, W2 _2 T! ?' u
, {* Q4 H0 R: W9 ^; o/ K 例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 ]。当这两个排好序的序列被归并后,即可得所需要的排序序列。
# u/ _* e# [$ D3 v/ A. }8 Z% Z
$ t8 r+ M# P+ J( x _! ]) x 图2 - 6给出了分而治之排序算法的伪代码。算法中子集合的数目为2,A中含有n/k个元素。
* r( K! [" e: Z* z; k
, Z# h6 U' s8 V u/ R template<CLASS T>$ x5 y |* M+ T, V% w) _
. w! s" U. L; u0 r, t
void sort( T E, int n)
- T; v! {$ @! W5 F
6 R" G( ]4 B4 A! K7 U, N6 Q* g% z { / /对E中的n 个元素进行排序, k为全局变量
/ I; p: M5 J* T' x( f# w
4 _: G# ]9 {$ y+ D- R; l, _ if (n >= k) {& Q& c0 ~7 ?8 n/ H
1 s5 t2 T! I! k* @7 H/ Z7 D
i = n/k;& s3 \9 X0 N( g' z3 ^0 q! j) e
8 W# X/ |" G$ b1 ]# n4 c
j = n-i;( y& H! f. j. a! N0 \% Q+ F, X
8 A, O; h+ ?8 J8 A 令A 包含E中的前i 个元素# Q5 K2 k" U; Z/ v' e9 y8 L
$ u2 F8 n5 {" _3 A$ @; O4 u! i
令B 包含E中余下的j 个元素* K w- Y- P( x. Y
5 j1 u, Y- U; Y8 w( N& h7 W7 M
s o r t ( A , i ) ;
; Z( ^" P# A: y' R % S( ]& j5 W, R0 ~' A3 K# N
s o r t ( B , j ) ;
' \$ ]# x- }8 t% [ 3 m1 c1 K: t2 Y% Y) {1 e
m e rge(A,B,E,i,j,); //把A 和B 合并到E2 W; E7 H5 l) n2 q$ A
( C0 @/ Q' W6 x. F( _
}
5 m3 Q2 o% T2 ] p : u& z$ y! u; K) E9 j
else 使用插入排序算法对E 进行排序
6 q4 q4 o& _- h 7 i8 _+ |# Y! s0 j8 s
}
Z! Y2 m6 `8 g* K0 r& d & M1 X, }& n7 a* z
图14-6 分而治之排序算法的伪代码$ T, _$ R) L" |' G8 w) _/ V. A
- Z/ [/ O# q6 j- d, P
+ A+ l, i4 S1 y, t0 N( t" z S9 M
, E, U. w4 I' o3 a3 \ 从对归并过程的简略描述中,可以明显地看出归并n个元素所需要的时间为O (n)。设t (n)为分而治之排序算法(如图1 4 - 6所示)在最坏情况下所需花费的时间,则有以下递推公式:3 s4 `8 w# P! j7 b0 R0 p* I
# ~, @/ _9 J2 d8 Y
其中c 和d 为常数。当n / k≈n-n / k 时,t (n) 的值最小。因此当k= 2时,也就是说,当两个子集合所包含的元素个数近似相等时, t (n) 最小,即当所划分的子集合大小接近时,分而治之算法通常具有最佳性能。 I0 ]/ v$ G1 o
) L2 W8 {, B( z$ m! x2 q# X
可以用迭代方法来计算这一递推方式,结果为t(n)= (nl o gn)。虽然这个结果是在n为2的幂时得到的,但对于所有的n,这一结果也是有效的,因为t(n) 是n 的非递减函数。t(n) =(nl o gn) 给出了归并排序的最好和最坏情况下的复杂性。由于最好和最坏情况下的复杂性是一样的,因此归并排序的平均复杂性为t (n)= (nl o gn)。# I; o1 S( X) c& T# ^
9 J. V2 K: Y5 k2 S6 {+ Y1 N! t) a 图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分成两个大致相等的链表。* ~% m( {8 ~' ~& H9 ?
6 Z: d" W2 t& A; f, \1 |5 _
归并过程应能将两个已排序的链表归并在一起。如果希望把所得到C + +程序与堆排序和插入排序进行性能比较,那么就不能使用链表来实现归并排序,因为后两种排序方法中都没有使用链表。为了能与前面讨论过的排序函数作比较,归并排序函数必须用一个数组a来存储元素集合E,并在a 中返回排序后的元素序列。为此按照下述过程来对图1 4 - 6的伪代码进行细化:当集合E被化分成两个子集合时,可以不必把两个子集合的元素分别复制到A和B中,只需简单地在集合E中保持两个子集合的左右边界即可。接下来对a 中的初始序列进行排序,并将所得到的排序序列归并到一个新数组b中,最后将它们复制到a 中。图1 4 - 6的改进版见图1 4 - 7。
; G @- W, _ {3 J! k! T. |3 p
; y4 e/ B. B/ X3 G$ X2 x
/ e2 ] \( U' U
" G& W9 [2 f t template<CLASS T>
- T* `; b1 l/ V, c) Y
- Q: d5 U9 m) K/ }) a- c6 A2 U M e rgeSort( T a[], int left, int right); ^: I) b9 o2 o3 ^
`. h0 Z& t! w0 `" m! i% D# E, |
{ / /对a [ l e f t : r i g h t ]中的元素进行排序5 S% g1 W! m% x7 \0 Z+ e5 _
0 w Y- [6 A+ z2 q
if (left < right) {//至少两个元素0 h* H4 ~$ U; m( Y+ i! [- h4 s# L$ r
) H* ^# o% V/ D6 N: q3 N. u int i = (left + right)/2; //中心位置
$ ~ F/ ]) f- g- ?2 T: J/ }, P1 O 7 Z ]7 E( G7 U
M e rgeSort(a, left, i);" X3 v1 P5 h+ A7 }
7 x+ ?8 K4 ^) J7 d& F
M e rgeSort(a, i+1, right);
: i! H1 a3 n6 M' g* ?9 c+ F$ l
# {3 I0 ~: P# P ~- l7 N; x* A& [1 q M e rge(a, b, left, i, right); //从a 合并到b
$ W% q+ m! ~, ` * n- `$ }4 V2 t: R" i! `
Copy(b, a, left, right); //结果放回a
: H2 k+ `4 O# q( V4 V ( L% P3 T+ j: ?
}
1 x/ V0 G8 j/ f5 G, g
) j: ?2 j6 `% l7 t0 f( J% B }/ J0 P' @7 L1 j% o1 ?& U3 ~
5 ^8 i9 x5 T# a6 q4 o# h* t& X% \ 图14-7 分而治之排序算法的改进9 Q- k* I- @ E+ n1 r
7 M8 k: @' |' S- N" N# N" F
0 t- \2 R7 A4 i- A% T
: j! Q; i6 v* t# j' i3 h( e1 K 可以从很多方面来改进图1 4 - 7的性能,例如,可以容易地消除递归。如果仔细地检查图1 4 - 7中的程序,就会发现其中的递归只是简单地重复分割元素序列,直到序列的长度变成1为止。当序列的长度变为1时即可进行归并操作,这个过程可以用n 为2的幂来很好地描述。长度为1的序列被归并为长度为2的有序序列;长度为2的序列接着被归并为长度为4的有序序列;这个过程不断地重复直到归并为长度为n 的序列。图1 4 - 8给出n= 8时的归并(和复制)过程,方括号表示一个已排序序列的首和尾。
3 y% ?) i2 y' j m 4 |4 n2 L/ j" m9 Q. t
& d& B; b2 Z, p' c( c4 G+ w |
8 G* r6 d9 P4 S* ^' z# u1 n5 d5 T 初始序列[8] [4] [5] [6] [2] [1] [7] [3] T) `5 n4 T) p# B6 v+ I/ M& x
) ~2 ^ K/ M+ a q 归并到b [4 8] [5 6] [1 2] [3 7]
) X# V3 n! P/ D
[( |. D+ D4 ]% o+ \! ~2 U 复制到a [4 8] [5 6] [1 2] [3 7]: X( V# Q5 F; F) |$ m
7 k, X( p$ T8 |) i+ `3 Q
归并到b [4 5 6 8] [1 2 3 7]
( n. ^7 u- H7 Y. G2 \ * I7 u" a2 z/ ^: q G& i
复制到a [4 5 6 8] [1 2 3 7]: X o8 s% i+ ?$ O/ X* U
( s& m: v* R# P9 L* v$ W9 V
归并到b [1 2 3 4 5 6 7 8]
* @) [3 ^$ D, Q; X6 B - {/ }: ]$ |% N) X8 V( I, w8 |3 y
复制到a [1 2 3 4 5 6 7 8]9 K6 e5 g( F) T, E2 f
- \' C5 D6 M _
图14-8 归并排序的例子
( ^8 P5 \0 C; F5 w
4 @) f! o# g7 |% U4 d/ @7 |- p
: ]4 {# o \3 ~9 J6 K , ?! o4 h7 W' z7 i5 {4 g8 g; V2 i
另一种二路归并排序算法是这样的:首先将每两个相邻的大小为1的子序列归并,然后对上一次归并所得到的大小为2的子序列进行相邻归并,如此反复,直至最后归并到一个序列,归并过程完成。通过轮流地将元素从a 归并到b 并从b 归并到a,可以虚拟地消除复制过程。二路归并排序算法见程序1 4 - 3。4 E3 `7 \) H# T& R4 v
/ W2 a1 f) M3 I/ j
程序14-3 二路归并排序
3 F& U/ ]$ U7 \5 z& r( F+ s2 S
% q+ n0 k7 K2 \5 a template<CLASS T>$ @, W0 m0 q$ ]9 F- q2 Y
. t1 I1 V! E: D% `/ n void MergeSort(T a[], int n)8 N; y/ k; q# u7 g
. R L$ h1 K4 ]/ X, \" o! n {// 使用归并排序算法对a[0:n-1] 进行排序
# X5 O/ c& E% P6 o+ X1 p 7 d) m* p* @3 U* V
T *b = new T [n];* W6 k6 E5 g( p# Y1 Q) Y
! C! t( Q3 E7 e, p int s = 1; // 段的大小3 V3 F8 w( Z( q. M" N7 o
|( m2 x( b" V, i/ f3 V+ F7 V8 K
while (s < n) {
. O. _ j5 p* _$ _5 v 0 @+ C$ _$ |& v4 K% o6 `* v3 }
MergePass(a, b, s, n); // 从a归并到b
# M/ `. a3 V2 Y9 N4 v) r
j. l+ }" T+ Y s += s;( z9 E ^8 r* [6 n" n
2 x* T' p6 @6 m; K" s
MergePass(b, a, s, n); // 从b 归并到a% s- s" u: _0 v, \
z( U ?' y O" c s += s;7 X# ^9 T( T! a* w. f
8 X( z8 y2 |: g) C& B! K" X4 O7 c
}+ a5 L9 }+ V! j! q- ]& a) Z
5 C( e( ?& o7 i* i3 Y }
8 b# J1 E& {7 f! J& e: { . D, T* h3 h4 ]# ~: z
为了完成排序代码,首先需要完成函数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定义一个操作符< =。如果需要排序的数据类型是用户自定义类型,则必须重载操作符< =。这种设计方法允许我们按元素的任一个域进行排序。重载操作符< =的目的是用来比较需要排序的域。0 G& } ~$ b( l) v
8 O; x) R% o) I' a' h
程序14-4 MergePass函数9 |0 Y7 A& l2 z& f X
& N. y6 l$ Z6 @" s: V: O template<CLASS T># Y; k, W9 {5 h$ m
9 @3 |& N0 c, p, C& k$ `5 e
void MergePass(T x[], T y[], int s, int n)
% l9 g! X+ L6 [7 Z0 R : `6 t i( `) k3 ?! M9 B
{// 归并大小为s的相邻段
0 U B3 X, h) S2 d
, g! s' P: V2 t3 d int i = 0;, u/ w ?- [. T6 y
: s G8 i* E: ], w while (i <= n - 2 * s) {
, P3 V6 }1 r7 o! [! u1 E
9 i$ ]+ I5 Q4 |7 k" u' O2 ? // 归并两个大小为s的相邻段
$ y/ @' K+ p- J( B/ E( K) X
0 Q7 ~9 }- `9 J Merge(x, y, i, i+s-1, i+2*s-1);
9 v! b2 h, b0 @$ i 7 u) O. S* ?9 M. k1 i. [( g
i = i + 2 * s;
/ f& R( p( T: M& r
5 M/ w6 A3 ]) o2 m }4 u s2 A& p( H. X3 M
0 W/ O+ x2 a8 b6 g2 j+ f% J // 剩下不足2个元素+ O l9 Y0 e& D% B
6 v7 _" K# A; t( d/ ^/ A
if (i + s < n) Merge(x, y, i, i+s-1, n-1);5 ^% i: U9 j* V: p. K+ D% P
4 b# p2 j/ f# N9 L else for (int j = i; j <= n-1; j++)- {! Z: T+ v, a4 }+ X
. N1 X* M O; q( |7 V7 l1 Y
// 把最后一段复制到y y" n+ S# D# r
/ E) |5 p4 t8 o$ w y[j] = x[j];
3 u0 t1 _7 B( @) l! Q% H; B* ^
4 ?* |; C3 M: ~' }! I# e' j }
% }1 t0 j- [. f& M / g5 b# _6 F' o) I! B. g+ E+ m
程序14-5 Merge函数& W8 D/ g/ ~1 F1 K/ P# i
7 H7 c$ M6 M+ R3 X0 B$ X template<CLASS T>6 s5 [- B8 u! K# |1 ?
# K& p& c4 v1 y3 M: v void Merge(T c[], T d[], int l, int m, int r)
; |! a1 G4 X1 D# s+ \ - l" t6 l2 M# S- m L& M/ @" _* q
{// 把c[l:m]] 和c[m:r] 归并到d [ l : r ] .
6 ]3 y6 v) ~( W6 j8 _ 8 i6 h: h2 a |- k- c
int i = l, // 第一段的游标
. A; L) j8 T0 Y " l) t& P, s; `4 I4 E0 L, z
j = m+1, // 第二段的游标- M: O1 T, b& Y
6 O( A) ~ d% g& a% @ k = l; // 结果的游标! ~+ ]# t# V# l2 E( Y3 r L
7 Z! b; Q" u1 y _! E / /只要在段中存在i和j,则不断进行归并
; ^( f+ x4 Q/ W1 z( C' ?
" G' v' e% t3 A3 H! d5 A while ((i <= m) && (j <= r))
) T' |; d, e4 P9 G6 c) U; C. J
- x; \5 a$ i1 B# y if (c <= c[j]) d[k++] = c[i++];
$ @$ o8 _) Y8 x$ U0 n7 _3 k
3 R* h7 H; O* n& r {& Q5 E7 \ else d[k++] = c[j++];5 c3 F6 x% d& u& f2 U5 w
; O/ i8 K, \& r. S Y- j // 考虑余下的部分) |/ x. Z+ Q+ U7 y! O* P" m' d
0 _5 Z) Y+ U" p: C( |; w" l
if (i > m) for (int q = j; q <= r; q++)
& @1 e6 ~- v6 i2 E , l$ b; ]* T% p4 o; r1 ]
d[k++] = c[q];
; @3 [1 K0 f+ m, t1 \
3 h7 p' m T5 W( E( X1 P3 S else for (int q = i; q <= m; q++)+ V$ i9 ]7 o+ l. X3 V
- l+ J- Y } s! J# h' f+ n" t
d[k++] = c[q];; X& x' Z ]3 O$ s' R
& x( G% o( u# L, x- I
}
, f* p5 A6 m( R8 \4 R2 c
# H6 T: q6 F- G* x 自然归并排序(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