数学建模社区-数学中国

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

作者: 韩冰    时间: 2004-10-4 05:16
标题: 归并排序
<>可以运用分而治之方法来解决排序问题,该问题是将n 个元素排成非递减顺序。分而治之方法通常用以下的步骤来进行排序算法:若n 为1,算法终止;否则,将这一元素集合分割成两个或更多个子集合,对每一个子集合分别排序,然后将排好序的子集合归并为一个集合。4 m. k( u  x! M0 L
  m5 P3 v" {1 G6 U
假设仅将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)的递归算法。
/ [: l( p/ D4 @$ b  E6 x. x
% C. v' i* T2 t" `4 T% n4 O! j假如用冒泡过程(见程序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)。/ B* @0 e" A/ U# f* h% k
) Q0 ?. J8 R# y0 ^8 y
上述分割方案将n 个元素分成两个极不平衡的集合A和B。A有n- 1个元素,而B仅含一个元素。下面来看一看采用平衡分割法会发生什么情况: A集合中含有n/k 个元素,B中包含其余的元素。递归地使用分而治之方法对A和B进行排序。然后采用一个被称之为归并( m e rg e)的过程,将已排好序的A和B合并成一个集合。
+ l' Y5 J: m$ A" W! G  T
, C6 S8 q, U' _例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 ]。当这两个排好序的序列被归并后,即可得所需要的排序序列。; ?3 n6 {; i  g0 T; ?

; u+ r) v+ v, _; w图2 - 6给出了分而治之排序算法的伪代码。算法中子集合的数目为2,A中含有n/k个元素。3 \2 K5 C; j0 W+ p4 {. P
+ H5 b" h: T' ^- ?
template<CLASS T>8 M. Z/ M: G2 l0 S

8 T3 E/ V- i  L4 N  ~void sort( T E, int n)' t5 R8 _& s% {5 D1 s4 B/ u% ?/ E
0 U0 A% X9 N3 `+ B' R9 K
{ / /对E中的n 个元素进行排序, k为全局变量2 i; Q% Y& q/ x& |
9 R( z/ u$ v, B+ g8 Z8 ^
if (n &gt;= k) {
# L6 ~: ~$ S: @7 l
2 M5 \6 |! C! C9 ui = n/k;* P7 i$ [7 X/ e$ {. N& r
- P/ I( n& @2 f: K5 E+ ]
j = n-i;
6 T8 ]+ j. N" }9 Y) i# Q7 _! ~& L3 P0 A7 f) @+ o
令A 包含E中的前i 个元素
2 j! ]" B1 S" U# N: `; Q7 W" _& S- N/ c) e. J9 \  Z
令B 包含E中余下的j 个元素3 J5 Z; P6 q1 @/ T9 L* r8 A

( \" P5 q4 ?( Z/ T5 X' hs o r t ( A , i ) ;' z2 E, |% F2 o( b

+ K9 u8 A2 }6 gs o r t ( B , j ) ;
! C: v) O( s0 _. c' ^7 E& m1 ?
1 N% I- A% X: ]6 pm e rge(A,B,E,i,j,); //把A 和B 合并到E) y# Z+ m; l( B) I
" W* y9 j% f& |
}7 s/ K& a+ t3 X$ Z3 ^

. T  R; E! _) j- g' c7 x4 `6 u' kelse 使用插入排序算法对E 进行排序( ]4 M7 u2 T6 p: v# ~. i
/ e+ @2 U0 r6 m7 Q! O! z+ P% I6 b9 `
}( {7 e6 u. `4 h# |- q4 Y  ~

& w  D( d0 u8 a) G2 a* t" x图14-6 分而治之排序算法的伪代码
2 b: C$ P% S9 [2 t4 n9 C* D. |1 E# `: O+ _! L
  h7 ~/ Y1 G$ \' }
' S$ p3 U2 o+ B
从对归并过程的简略描述中,可以明显地看出归并n个元素所需要的时间为O (n)。设t (n)为分而治之排序算法(如图1 4 - 6所示)在最坏情况下所需花费的时间,则有以下递推公式:
/ K: A: `3 M: s! k/ }+ F- t) f8 ^' A' f6 ]' c, N1 u1 l
其中c 和d 为常数。当n / k≈n-n / k 时,t (n) 的值最小。因此当k= 2时,也就是说,当两个子集合所包含的元素个数近似相等时, t (n) 最小,即当所划分的子集合大小接近时,分而治之算法通常具有最佳性能。/ k( d3 a3 C1 N! k; H

% q9 q: P4 O- V  w可以用迭代方法来计算这一递推方式,结果为t(n)= (nl o gn)。虽然这个结果是在n为2的幂时得到的,但对于所有的n,这一结果也是有效的,因为t(n) 是n 的非递减函数。t(n) =(nl o gn) 给出了归并排序的最好和最坏情况下的复杂性。由于最好和最坏情况下的复杂性是一样的,因此归并排序的平均复杂性为t (n)= (nl o gn)。' t' ]  i% e: O  t+ ~0 r* V* W& S2 {" Z
% @) i# A. R' q. p. o
图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分成两个大致相等的链表。2 C+ u+ {# g! k

3 Q) u, \8 `' r2 u归并过程应能将两个已排序的链表归并在一起。如果希望把所得到C + +程序与堆排序和插入排序进行性能比较,那么就不能使用链表来实现归并排序,因为后两种排序方法中都没有使用链表。为了能与前面讨论过的排序函数作比较,归并排序函数必须用一个数组a来存储元素集合E,并在a 中返回排序后的元素序列。为此按照下述过程来对图1 4 - 6的伪代码进行细化:当集合E被化分成两个子集合时,可以不必把两个子集合的元素分别复制到A和B中,只需简单地在集合E中保持两个子集合的左右边界即可。接下来对a 中的初始序列进行排序,并将所得到的排序序列归并到一个新数组b中,最后将它们复制到a 中。图1 4 - 6的改进版见图1 4 - 7。
* Q% O' }9 E" V/ H6 o
/ |9 o7 L0 c' d  Y. H% J2 `- w% n
" Y* ?- k7 E. ?+ ~/ l# b# x
8 m5 Y9 y* U/ W8 D5 M$ S! v" ttemplate<CLASS T>' a9 P6 d+ G* G

1 E: S  _# _2 O& T& L. ?M e rgeSort( T a[], int left, int right)
3 _, D  L7 q( ]& E$ e4 A: H* w. @6 L3 `
$ b% M* ~3 x5 I9 D{ / /对a [ l e f t : r i g h t ]中的元素进行排序2 O1 \9 X/ b* ^/ Q% U8 q9 m
3 }, W- R! U+ `; Z
if (left &lt; right) {//至少两个元素! h( k0 t4 }. U! W! b) p

' T3 M* I7 {4 K  E2 R3 Nint i = (left + right)/2; //中心位置/ |2 w) s  s  c4 y6 o

: r) a. y' z2 X5 f" IM e rgeSort(a, left, i);* Z! G/ Z. U) U

# X$ U$ N4 [2 W% e; TM e rgeSort(a, i+1, right);# ]( n6 D+ m( B; ]) P
5 F& S" }1 E) V& P& g& j/ Y6 H0 \
M e rge(a, b, left, i, right); //从a 合并到b3 ]+ p( {0 y3 I% v) c/ m. G1 z

. S' U3 r. ?+ H( zCopy(b, a, left, right); //结果放回a
8 r5 ]* j  x& U: D
8 x1 v8 h) s3 g9 M4 `8 r/ L' k' g}* g. F9 i, E5 h) S, o8 Z

# N# D% \# W3 N, d0 j7 a}6 P* C3 L8 i5 i# ?1 p3 h& j6 ]
( B5 c8 X+ j! @5 j& l5 p. z
图14-7 分而治之排序算法的改进& |9 C. o4 |# m7 y2 x, J! ]

! a* n% ~4 P5 g" r: _+ c5 x$ M
9 w, Q+ f( S+ j% ?* c( z6 Q+ H1 a; i# J" S3 e
可以从很多方面来改进图1 4 - 7的性能,例如,可以容易地消除递归。如果仔细地检查图1 4 - 7中的程序,就会发现其中的递归只是简单地重复分割元素序列,直到序列的长度变成1为止。当序列的长度变为1时即可进行归并操作,这个过程可以用n 为2的幂来很好地描述。长度为1的序列被归并为长度为2的有序序列;长度为2的序列接着被归并为长度为4的有序序列;这个过程不断地重复直到归并为长度为n 的序列。图1 4 - 8给出n= 8时的归并(和复制)过程,方括号表示一个已排序序列的首和尾。4 w) o& k! ~0 w- l
) A6 H8 E2 C  Z+ R1 }2 I
% x$ a( i; ?" G+ p- h* ~

# |. ?) q; e1 S( t' u初始序列[8] [4] [5] [6] [2] [1] [7] [3]
. ~1 y( {/ S* Q$ D# I! v
8 y2 L) l6 B; ]0 v' ~+ Z( o归并到b [4 8] [5 6] [1 2] [3 7]
/ l" ^, J- u! d" P
7 G' K+ @  F0 K复制到a [4 8] [5 6] [1 2] [3 7]+ n3 o3 o; M" [) V2 t3 ~9 G

) Q3 n6 K  |/ G: n/ p归并到b [4 5 6 8] [1 2 3 7]
8 |! z; v* W  q+ H8 @( Z; S; v+ F9 w6 ]* m  t+ N6 L
复制到a [4 5 6 8] [1 2 3 7]( m+ v8 [7 n; f) k. p- K) L; L
4 k3 W* b6 b, O" A8 E8 N( Z. j
归并到b [1 2 3 4 5 6 7 8]
" N/ d* ?8 \* z+ m8 ~
7 X( ^0 T( @/ I5 @: F复制到a [1 2 3 4 5 6 7 8]
  {# u+ ^' O( H* U6 w; J. ~3 f
+ i4 U5 z8 @' p+ Q$ n. w" H# S# F图14-8 归并排序的例子3 j& a; M5 i8 r& G0 \9 ]9 I7 X

- c8 q! y# ~2 n/ E3 {* t: r! ~5 c, F) D4 g" B+ N! u1 v, t

' [) t2 P! Y1 f* p  L: {; D+ f另一种二路归并排序算法是这样的:首先将每两个相邻的大小为1的子序列归并,然后对上一次归并所得到的大小为2的子序列进行相邻归并,如此反复,直至最后归并到一个序列,归并过程完成。通过轮流地将元素从a 归并到b 并从b 归并到a,可以虚拟地消除复制过程。二路归并排序算法见程序1 4 - 3。( V8 m) H" F# a; Z% W/ Y6 e9 q5 F

8 w2 n& s* ?6 ]  U/ e" `程序14-3 二路归并排序
+ i! V0 w9 t" K# l. F5 F
8 X$ j2 B( A* ]4 utemplate<CLASS T>, j0 X2 u" ^6 |) c

/ d3 Y, P+ k- |1 \+ cvoid MergeSort(T a[], int n)) ^2 v! u2 c+ \% A, d

) ^& E5 t3 {& J. F2 T  t, u{// 使用归并排序算法对a[0:n-1] 进行排序+ Y1 D9 r. w0 m- }! Q$ q2 ^7 j
' Q1 G% \5 _! ~* w" y
T *b = new T [n];
1 S, |! T. W7 a; r' O+ f* m* S% ~6 k
int s = 1; // 段的大小3 P' `, x, d3 i3 S

4 ]% u$ J$ }: swhile (s &lt; n) {
0 D" h5 A4 o& P& z) w4 w
! ?2 ~! l3 h+ G' H2 oMergePass(a, b, s, n); // 从a归并到b
6 ~4 G! h3 t! U4 u6 p0 B
8 n/ z, m; [9 L7 [, s, h5 c3 @s += s;
# K' N$ d% U% {" S+ h
# V! w, Z4 O& I/ bMergePass(b, a, s, n); // 从b 归并到a0 z7 F0 ?! f0 q3 P' w

9 A$ F+ j% k0 U/ w6 a' Js += s;' `6 A/ `4 ^* \) y. r* t- [9 c1 C
! ^7 L3 H& q9 @2 H2 }+ [; K
}
3 _7 E% i" x+ K: K/ L* f" ?6 s  ^0 e+ Q  n5 p+ Q; l
}
# ]+ _+ P. G' ?. _2 ?  }8 e0 z3 f, L
2 h6 e( P* r* M) N7 p/ _, d为了完成排序代码,首先需要完成函数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; =的目的是用来比较需要排序的域。9 p2 \; x! h% w/ K% S

1 j2 q# h" y$ p) y$ o! y! P9 k程序14-4 MergePass函数
4 ^5 c% Z7 {+ Z+ K) N
) B& V# g4 G1 l% |template<CLASS T># n- \. a2 T, Y
1 q% g' U* g0 _: i- _5 i( o5 q
void MergePass(T x[], T y[], int s, int n)0 x* h. b3 B3 D0 k5 G" A. ~
* x) _. I& M. J0 U8 I- Y
{// 归并大小为s的相邻段
! W% N: ~& g4 R( J% n5 q; n; u4 M' w5 h7 J
int i = 0;# u# \4 x+ J$ R- k; M( A* ~* b
4 L: _. `; L" b7 y( @
while (i &lt;= n - 2 * s) {
! b  z! U7 e/ [2 b2 T1 ^1 U
1 n$ R1 l, ?& v; k4 ?3 K  Y// 归并两个大小为s的相邻段
, `8 y% Z. K+ l4 V# R. [
3 s1 p8 A! u+ c2 HMerge(x, y, i, i+s-1, i+2*s-1);
8 U3 g& a/ t5 q& @3 }. h2 t* @$ P+ {' I
i = i + 2 * s;
' j8 p: c7 }0 c  \) R0 p! r) F1 g3 y# n6 g
}
% y8 p) s6 n& M: ^
$ s( r$ w; @1 f# k// 剩下不足2个元素
) J2 y0 S" ^$ W
3 C$ P$ O5 g5 _7 T% h0 V2 Zif (i + s &lt; n) Merge(x, y, i, i+s-1, n-1);8 _4 d3 s. W" S/ _( n
$ v9 O( U0 i* p$ }! _- }0 H+ L" }
else for (int j = i; j &lt;= n-1; j++)1 Q# _! c9 o- W8 f5 a

( O2 v6 q1 u$ j# D) @, w  J// 把最后一段复制到y6 J2 v6 O7 x  R9 `  z$ _

- f) z0 K7 d: l- K% v4 ky[j] = x[j];) Q8 X) {$ O8 ^- e
0 P% m" r8 i$ n7 I
}
2 a! @1 ~- |. t) @0 O, A, R1 B8 J$ J& v5 A* D5 J3 w) `
程序14-5 Merge函数
( J5 w+ l1 A3 Y9 K: l
7 d6 N4 c% S' i' z+ @1 V; i" J. U0 Utemplate<CLASS T>
' c+ n$ g0 C. o4 h7 f" C" x6 a: \3 f( j6 L, s, Z+ Z- x
void Merge(T c[], T d[], int l, int m, int r), o% ]% E% G" }# |& M3 p
4 F9 m% V5 u- U9 |7 f0 M' ^) _, Q3 [
{// 把c[l:m]] 和c[m:r] 归并到d [ l : r ] .- y0 h; w- G' V" t5 v4 t8 _

0 y3 P8 l. ]; E! wint i = l, // 第一段的游标" R  T. P  S! }% o4 m

& B6 @8 s9 \2 O$ I+ I* fj = m+1, // 第二段的游标
7 f* \/ b, |% k+ |$ T& @
( p, s5 j( _6 r% U- H! Bk = l; // 结果的游标
4 e  b5 M, j1 C3 x) b4 t- Z0 T5 N$ Z3 a: R. C+ u9 ^
/ /只要在段中存在i和j,则不断进行归并
2 N& G) K5 f- e: @
1 q: X" F; n8 F/ {$ Bwhile ((i &lt;= m) &amp;&amp; (j &lt;= r))
0 h0 B& Z! N1 v3 c( b# b4 g4 J* |- Y2 ^- D2 a
if (c &lt;= c[j]) d[k++] = c[i++];; S- w) x9 W7 l, y) |

3 R2 a) E3 h9 S4 u# C; L1 @else d[k++] = c[j++];
! V* G& j) E+ @" Y, v: e. V: v$ Q+ h
3 n9 U$ n, y9 O7 Z" F. _2 k// 考虑余下的部分8 k7 T! q& m1 I& C4 ~

0 d# U) b+ n' }0 eif (i &gt; m) for (int q = j; q &lt;= r; q++)% u9 k9 M9 l/ t  P' W! s; e0 z
4 ], z: O) G6 O- X* x
d[k++] = c[q];4 p2 W9 U. R: P* |9 N
9 V: a+ I  U, Y1 n* C# I
else for (int q = i; q &lt;= m; q++)7 X8 W- I: n, @# B7 d

. Y2 S  q* @7 P$ Yd[k++] = c[q];
, R5 R) E, G' G; `# P7 b2 d' }. D: a, m: Y
}
9 @4 A; Q! C/ Z1 m3 K! w) _6 Z: r" k- [* t1 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>




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