数学建模社区-数学中国

标题: 归并排序 [打印本页]

作者: 韩冰    时间: 2004-10-4 05:16
标题: 归并排序
<>可以运用分而治之方法来解决排序问题,该问题是将n 个元素排成非递减顺序。分而治之方法通常用以下的步骤来进行排序算法:若n 为1,算法终止;否则,将这一元素集合分割成两个或更多个子集合,对每一个子集合分别排序,然后将排好序的子集合归并为一个集合。
5 W( _6 T6 [5 k& R# D5 c! b/ e8 j* I* f2 R
假设仅将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)的递归算法。
- ?& u6 w2 L- y; D1 s7 G4 D: G6 O" F4 ^4 o% b0 M
假如用冒泡过程(见程序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)。
- I5 Q( @% Q" ?! ~3 k- A% a' V, e4 c. q* n2 x$ `
上述分割方案将n 个元素分成两个极不平衡的集合A和B。A有n- 1个元素,而B仅含一个元素。下面来看一看采用平衡分割法会发生什么情况: A集合中含有n/k 个元素,B中包含其余的元素。递归地使用分而治之方法对A和B进行排序。然后采用一个被称之为归并( m e rg e)的过程,将已排好序的A和B合并成一个集合。
' R0 M/ _1 t8 I$ W5 V: L1 C8 b* P' ], k7 v% w. y4 C
例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 ]。当这两个排好序的序列被归并后,即可得所需要的排序序列。
+ D2 I( V7 Y" D( y' C
6 _; u; ?7 g5 L& T& a! @' N图2 - 6给出了分而治之排序算法的伪代码。算法中子集合的数目为2,A中含有n/k个元素。  ]# u, ^7 ?) n" u
8 c8 d  ^7 p3 u! l
template<CLASS T>+ V7 M5 \& T& Z- I& S/ W0 b

) ^! p) [. a' {; v5 f6 V# Ivoid sort( T E, int n)! m. c4 T: m. t" y

* c' l; [4 c) t% }8 h7 q% D{ / /对E中的n 个元素进行排序, k为全局变量
/ M& d$ ?2 I7 B/ J/ L- y4 }
/ ~+ o0 u4 I7 N& e2 B! g  u+ \if (n &gt;= k) {/ r- ?2 K9 M: G4 k+ ]+ ~
: U2 t4 p; a, P7 i: @0 r
i = n/k;! M" d8 y7 D6 c7 \+ b) {+ Z; U! O
* Z- |3 p( P# }
j = n-i;0 {( r' t2 o% [! u

$ c+ q% j4 D8 b7 l- i令A 包含E中的前i 个元素
  {/ H3 f' H- r! W' ^* W5 W( p4 H% M! _
令B 包含E中余下的j 个元素
4 D1 ~: C4 s2 S5 R+ Y, S' G$ z8 F+ i2 j; T3 d6 Q
s o r t ( A , i ) ;: q' {) a. d6 x3 \5 K& R- \
6 y3 |: u# B6 j2 [; }" e
s o r t ( B , j ) ;! v) S# Z% _5 |3 Z7 l

% A  I$ ?7 W( im e rge(A,B,E,i,j,); //把A 和B 合并到E
5 E3 [  j& F0 O; N" t
0 s4 r" w) e+ e3 {0 D- q}
7 d  v! W& {2 I+ {( ?2 ^" D$ L2 m. }+ y3 i
else 使用插入排序算法对E 进行排序0 L3 L: D  u8 B* }- a

! s% }% A3 |0 |+ A) W8 D}! u: d$ O  r: n7 C5 J: c  d; X

" \  O4 n+ g. Z# W# R# e图14-6 分而治之排序算法的伪代码
, X# b  s0 ?' y. z1 H9 q4 u8 y+ P5 H" d* m2 Y" C2 P/ g
5 m3 U7 O! {3 N0 G' K

$ o( ?/ t8 s) K0 d* [2 D  Q' g从对归并过程的简略描述中,可以明显地看出归并n个元素所需要的时间为O (n)。设t (n)为分而治之排序算法(如图1 4 - 6所示)在最坏情况下所需花费的时间,则有以下递推公式:
6 G$ x$ c& V9 g( D% \
% k1 x3 K! D  Y6 G其中c 和d 为常数。当n / k≈n-n / k 时,t (n) 的值最小。因此当k= 2时,也就是说,当两个子集合所包含的元素个数近似相等时, t (n) 最小,即当所划分的子集合大小接近时,分而治之算法通常具有最佳性能。% ]. l, F5 z4 G8 u

- O& d+ V* w, i; J可以用迭代方法来计算这一递推方式,结果为t(n)= (nl o gn)。虽然这个结果是在n为2的幂时得到的,但对于所有的n,这一结果也是有效的,因为t(n) 是n 的非递减函数。t(n) =(nl o gn) 给出了归并排序的最好和最坏情况下的复杂性。由于最好和最坏情况下的复杂性是一样的,因此归并排序的平均复杂性为t (n)= (nl o gn)。
& C8 x  @" S; `
4 g. ^  L8 i$ N: k! 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分成两个大致相等的链表。* G! O' V" R: k

  L% @: I) [1 E: n* s3 Z归并过程应能将两个已排序的链表归并在一起。如果希望把所得到C + +程序与堆排序和插入排序进行性能比较,那么就不能使用链表来实现归并排序,因为后两种排序方法中都没有使用链表。为了能与前面讨论过的排序函数作比较,归并排序函数必须用一个数组a来存储元素集合E,并在a 中返回排序后的元素序列。为此按照下述过程来对图1 4 - 6的伪代码进行细化:当集合E被化分成两个子集合时,可以不必把两个子集合的元素分别复制到A和B中,只需简单地在集合E中保持两个子集合的左右边界即可。接下来对a 中的初始序列进行排序,并将所得到的排序序列归并到一个新数组b中,最后将它们复制到a 中。图1 4 - 6的改进版见图1 4 - 7。8 v& _0 E0 s# S7 ]% _) v2 J+ L

; Z6 a; k% n# B1 m. r6 T- P% T& m+ A! B6 w, E4 `
) l) ?: {1 }" e, I/ }6 }: g) g, E
template<CLASS T>
( ]' e; s& W: D% T) m3 [8 @
7 w0 Y3 B- k( o; [3 v" Y" }M e rgeSort( T a[], int left, int right)$ C9 L/ l  C* F5 z7 h4 _
+ u; V* b7 _! p3 M" X: H4 E5 u
{ / /对a [ l e f t : r i g h t ]中的元素进行排序
2 F' }2 @. V) b& [+ {, K$ k, ]4 c9 _3 e9 w1 @
if (left &lt; right) {//至少两个元素9 p+ C" J$ y2 F0 A% \2 \

9 r& s1 ~9 f+ W8 H- i% `int i = (left + right)/2; //中心位置
$ {# h% F' q2 k3 Q$ _
% P- g, P+ Q  G( eM e rgeSort(a, left, i);4 F/ n+ v2 x: B

. P  X6 V, ]6 k  tM e rgeSort(a, i+1, right);7 ]. I" k0 I' B4 u5 K

* i: o; W% u; Q; [! DM e rge(a, b, left, i, right); //从a 合并到b8 I3 l+ @" a% {! u' |
  `( [. }. e2 t) V! w: g
Copy(b, a, left, right); //结果放回a6 q( ~( Q* R2 ]

% c* w$ d; A- }; c$ f1 @  O0 r}8 S( F/ q( j8 k; x
& [" m) |9 A( H6 k& {. Z
}
6 b0 ?% e8 v; n! w) M, E$ o9 `
4 ?$ o( y4 c( w# S0 J图14-7 分而治之排序算法的改进) s4 m3 ~. ^" O9 ~% \
& X: d! y/ T! ]3 v+ ^. J/ [

1 l8 m' Z# C" E# U2 b1 z- ~3 z4 Q8 w# h( K% c9 G
可以从很多方面来改进图1 4 - 7的性能,例如,可以容易地消除递归。如果仔细地检查图1 4 - 7中的程序,就会发现其中的递归只是简单地重复分割元素序列,直到序列的长度变成1为止。当序列的长度变为1时即可进行归并操作,这个过程可以用n 为2的幂来很好地描述。长度为1的序列被归并为长度为2的有序序列;长度为2的序列接着被归并为长度为4的有序序列;这个过程不断地重复直到归并为长度为n 的序列。图1 4 - 8给出n= 8时的归并(和复制)过程,方括号表示一个已排序序列的首和尾。
, D9 x; R% n7 k- U9 h/ [3 _5 C) ]$ n2 x, ?( U, `8 \/ r, e

8 d, j% p4 `& Q8 a  F
( I7 E, ~2 Z3 j! ^' y初始序列[8] [4] [5] [6] [2] [1] [7] [3]; H2 }6 R  s4 |) I7 k, H! G6 s/ @4 H+ G

4 B2 N( ~2 O8 T# U4 a归并到b [4 8] [5 6] [1 2] [3 7]
' w; Z# S# o5 f
9 o) _- b4 r7 r+ r/ O, X复制到a [4 8] [5 6] [1 2] [3 7]
2 O: h9 }4 o1 K8 i2 e8 @. S8 D- E+ b3 }# @
归并到b [4 5 6 8] [1 2 3 7]
+ j3 E; [$ R; Q" J7 h6 c6 y$ b& t' Q
复制到a [4 5 6 8] [1 2 3 7]
" ^( l0 s* Q) k) x& w' J' h. I# z$ T
归并到b [1 2 3 4 5 6 7 8]+ [0 P, g1 y& R  X) Y! |. }: _9 n
4 l. H& K4 v' p! |
复制到a [1 2 3 4 5 6 7 8]
% ?- u8 Z6 o4 e6 N7 `% u# O
) A' y0 L, \. \: f图14-8 归并排序的例子; W2 z4 h( \8 _7 ~' Q. I0 ]
6 ~1 ?+ z/ M$ I" w

1 @: z, ?; ~/ Y' h) \8 @3 j2 C# g5 D/ j# ~; E% c3 O- U5 K
另一种二路归并排序算法是这样的:首先将每两个相邻的大小为1的子序列归并,然后对上一次归并所得到的大小为2的子序列进行相邻归并,如此反复,直至最后归并到一个序列,归并过程完成。通过轮流地将元素从a 归并到b 并从b 归并到a,可以虚拟地消除复制过程。二路归并排序算法见程序1 4 - 3。' F: p! V/ o7 I" `. Y+ ?: L

- D9 ?  M/ O5 p, a% X4 l& ^* Q程序14-3 二路归并排序, z8 e% H4 P: o  ]

$ U! G0 {7 i" Qtemplate<CLASS T>
  Z$ L+ R( a% s  P# P) I5 k# v' E4 o) Z. T9 R, C0 g
void MergeSort(T a[], int n)
( p, M9 T" v3 J$ |. {2 {2 j1 Q
& j% D, \* V: u( V/ g9 \, C9 @& R8 w{// 使用归并排序算法对a[0:n-1] 进行排序
/ h* S; x" `5 n: H1 ^8 A8 y
6 {. E* i. O6 ^4 D( iT *b = new T [n];* g& N/ O3 ?8 z# \; [- D9 `% l
$ ]5 L8 o: [( a' y: _& k
int s = 1; // 段的大小
$ B7 D! g! G: W6 y9 a) h2 f6 f# V. M9 s! S
while (s &lt; n) {2 I  [1 R" t6 B, K1 C
6 z9 @3 x, {4 f$ ]) W, U( t3 ^/ |& M
MergePass(a, b, s, n); // 从a归并到b
- N: T7 s' n# X9 m( }+ f& H& c- P& s% X& K5 j
s += s;- s* m( \/ h3 I' g! v+ p5 l

& G( h% I9 ^  q& qMergePass(b, a, s, n); // 从b 归并到a
8 i5 P. K, l6 `3 N. Z+ P8 w$ \8 U$ f% G; a1 ]/ T- |
s += s;
! P! u! I  N1 w3 O
2 T! y. Q$ C' G( g* j  q0 F% ?}: i! b: ~' B$ J, ]

! @, w0 {' |1 d}
1 q- n" J$ u. |- z) J/ u# z1 A+ a) h" c; e' i, w9 Y/ u. N8 N
为了完成排序代码,首先需要完成函数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定义一个操作符&lt; =。如果需要排序的数据类型是用户自定义类型,则必须重载操作符&lt; =。这种设计方法允许我们按元素的任一个域进行排序。重载操作符&lt; =的目的是用来比较需要排序的域。# K6 l# a2 G' W( \
% s7 P  ?2 {- T) c
程序14-4 MergePass函数
" f( k0 }% [- f$ _, b9 i# d0 h$ V3 Q+ j, {: u8 T
template<CLASS T>
; _. c  r2 u: s% ^  m1 N; i! Q+ D
void MergePass(T x[], T y[], int s, int n)* x- _' c1 @& ~2 `% b
7 V4 ]! l. S2 C& s+ f& V
{// 归并大小为s的相邻段
$ R. @* E! o  D
' j( ?* C# M- M# @6 d, Y$ c2 \, Pint i = 0;3 ^* k5 p1 P  R- ]# a

5 N; K" p6 {& G1 R' Ywhile (i &lt;= n - 2 * s) {
2 _3 R' W4 a& X2 n5 ?
+ {6 _; r* s7 }  X0 W// 归并两个大小为s的相邻段
$ s2 Q+ z) h8 @3 N6 q% P; C) W: E5 B5 n, j# x4 o1 ~" c
Merge(x, y, i, i+s-1, i+2*s-1);
; I: |- L/ b! P5 F1 O5 t$ _$ N1 ]5 w4 |7 ]; g
i = i + 2 * s;
+ x4 n4 Z+ Y% r6 |
: o" K1 G, D+ o. Q. b0 i}
' x# K1 X; s; Q: \# {; r' M; l6 L0 V' L
// 剩下不足2个元素" h# D: `: a/ X2 _; ?0 i

, E7 M8 Z% X1 J# z* v7 B) `if (i + s &lt; n) Merge(x, y, i, i+s-1, n-1);: K' G* n  ]4 t! B) J3 l$ T

* J$ N3 ^8 o, j* E8 g# Xelse for (int j = i; j &lt;= n-1; j++)' S$ l& c  ^1 K' V0 s. l
5 u2 N4 S1 K0 R+ r2 l$ D  N0 R
// 把最后一段复制到y
; B2 N9 H, K9 S/ [* s' ^' M
9 _9 o5 s( G4 h7 j5 ly[j] = x[j];1 h4 p8 Y- M( [7 V2 J! ]( [5 h
# B* s- M$ p. J! T! P6 f' X) M
}& Q; P( F( w, S$ j
, P" x3 q' `% n3 Y5 s) {. W$ P0 Z1 p
程序14-5 Merge函数
$ N. {$ \( |; b2 y0 ^& X
: e3 m# E; J3 y5 n9 M3 M' jtemplate<CLASS T>
7 \; D% H, k! s0 I! y
6 ?3 z- u( h# mvoid Merge(T c[], T d[], int l, int m, int r)0 Y0 N+ T" y3 H' S, k
4 i# t) ^- V% H' z( o  ?" y
{// 把c[l:m]] 和c[m:r] 归并到d [ l : r ] .
  d$ p* C1 U; H7 g7 W9 a  r* Y# i8 B6 \6 _/ S8 {3 G
int i = l, // 第一段的游标
2 [2 g6 l% ?) V
1 F8 q, a8 |1 e" _# A! ]& a& M/ Yj = m+1, // 第二段的游标
2 `4 H  |4 @& q% C: W) c: {: ^8 i; k/ o6 l& g! S
k = l; // 结果的游标) J6 f$ j# p! Y: C

, u# |5 z- a4 v0 \0 G/ /只要在段中存在i和j,则不断进行归并
: g& o7 p, B( g+ l2 ?& Y8 q- E: k5 @' _9 b
while ((i &lt;= m) &amp;&amp; (j &lt;= r))
! w7 n/ L' |$ Y. C: Q+ y
" u8 H* A( c5 l1 H  I& N( E: {5 Tif (c &lt;= c[j]) d[k++] = c[i++];
0 m' J' c( I2 b  O2 G2 Q9 A% L# E/ j" w8 L9 K. f
else d[k++] = c[j++];# |' {' K% M0 Y# |. j

. E9 Z" }5 e$ @" c+ B3 J1 n6 F0 J// 考虑余下的部分
4 O# l( P2 B) A. l1 F$ R7 o* y0 {/ w. ^6 f+ f  V
if (i &gt; m) for (int q = j; q &lt;= r; q++)
- j6 U; r* t) \* m2 a- o  t! o' S  z; ]1 z% D6 Y8 ?
d[k++] = c[q];
( M5 F5 T" I, z) z# @# k4 ]
* u5 a0 M  ]' c; n3 K( felse for (int q = i; q &lt;= m; q++)# m; c0 u: V& z+ t

3 S& n9 Y" p' [: w8 D. bd[k++] = c[q];
- u* X4 o- U! j! e2 X3 i. ~
- ^7 t* }  F5 D( v4 r}5 k# p2 T9 z7 ~
5 G  u( Y, y( T  C: B
自然归并排序(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>




欢迎光临 数学建模社区-数学中国 (http://www.madio.net/) Powered by Discuz! X2.5