数学建模社区-数学中国

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

作者: 韩冰    时间: 2004-10-4 05:16
标题: 归并排序
<>可以运用分而治之方法来解决排序问题,该问题是将n 个元素排成非递减顺序。分而治之方法通常用以下的步骤来进行排序算法:若n 为1,算法终止;否则,将这一元素集合分割成两个或更多个子集合,对每一个子集合分别排序,然后将排好序的子集合归并为一个集合。
( k( J  M5 o, _7 n6 c
6 e$ V- j  }9 ]5 Z+ Z假设仅将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)的递归算法。* O- F5 H* e; z( k0 u$ ~" C

' F  Y( `8 }! N6 H假如用冒泡过程(见程序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)。
1 u; D' Q! W4 o. v9 V# h- \- c
) V2 w  H: D6 Y: d4 O; m0 t上述分割方案将n 个元素分成两个极不平衡的集合A和B。A有n- 1个元素,而B仅含一个元素。下面来看一看采用平衡分割法会发生什么情况: A集合中含有n/k 个元素,B中包含其余的元素。递归地使用分而治之方法对A和B进行排序。然后采用一个被称之为归并( m e rg e)的过程,将已排好序的A和B合并成一个集合。
! a+ [3 K- i, [  U, u+ ~7 ^; g4 d' X# F* N
例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 m) u% q7 _4 x7 U& U: l
' E; b1 r9 @' u( I! i6 T图2 - 6给出了分而治之排序算法的伪代码。算法中子集合的数目为2,A中含有n/k个元素。
. `# L. H5 H" Q( s/ G8 B3 K, P0 N4 E& n
template<CLASS T>
- s  G+ o+ ?) a" Q5 \8 j' F. t5 [" @# z- g$ U2 F
void sort( T E, int n)
7 l7 H' u2 K* _2 S2 I8 G& P8 W
8 \+ L" P  @! R$ P{ / /对E中的n 个元素进行排序, k为全局变量0 {3 V8 c# B& x+ d7 b' F  E& o0 m3 y
5 G" h: N! ]  T, G, l' b2 B1 W$ ?
if (n &gt;= k) {
( I8 j0 ]2 Z: U; [! i( a5 [4 M+ H9 o1 }" K
i = n/k;4 `! j) {# k3 Z9 ~2 ~9 `

; M2 @+ I) Y) E7 W5 P! |( {3 jj = n-i;8 \, M# _/ u; F0 T

) ]. H+ |* K" ?! s) E令A 包含E中的前i 个元素
4 `" ]* L. `- w7 `
" U- N7 f' H: D; k, g- c  [" o0 b令B 包含E中余下的j 个元素
6 [. O# q" R, t1 ~8 I
# Y# X1 y3 f* Cs o r t ( A , i ) ;2 e" g& O+ z9 s7 [# {6 i$ _. j
' I7 n  c* E5 V& h
s o r t ( B , j ) ;2 w3 i( j3 A3 ?) ^, H
' r% I9 x% ?' m% |+ y1 V8 n4 l9 G
m e rge(A,B,E,i,j,); //把A 和B 合并到E
" n" }& s9 o  m# p  t' a! N0 H# h
}9 d7 t! W$ e0 E2 D& K- R. n
! P$ z, S) C4 s5 S, c; ^
else 使用插入排序算法对E 进行排序
) q% w2 s; }0 n3 ]# t; R
) B  n2 R, M0 [' a9 W6 b}3 H2 G; s% N8 q9 ~6 J8 w
( S2 @3 _! h, a8 l0 Y
图14-6 分而治之排序算法的伪代码
/ m. m% _( {$ o' d' [4 C  v7 _( @( |5 q1 x

5 S  I; [* L% e1 e, [) I6 C6 \# w9 v' R5 ~8 T+ y6 a
从对归并过程的简略描述中,可以明显地看出归并n个元素所需要的时间为O (n)。设t (n)为分而治之排序算法(如图1 4 - 6所示)在最坏情况下所需花费的时间,则有以下递推公式:
* s& e! H. I& h! g
7 h- L7 b8 M; J/ T, t其中c 和d 为常数。当n / k≈n-n / k 时,t (n) 的值最小。因此当k= 2时,也就是说,当两个子集合所包含的元素个数近似相等时, t (n) 最小,即当所划分的子集合大小接近时,分而治之算法通常具有最佳性能。* T; A# m0 f0 [
' d- Z# j: N2 g9 L1 p# h$ }
可以用迭代方法来计算这一递推方式,结果为t(n)= (nl o gn)。虽然这个结果是在n为2的幂时得到的,但对于所有的n,这一结果也是有效的,因为t(n) 是n 的非递减函数。t(n) =(nl o gn) 给出了归并排序的最好和最坏情况下的复杂性。由于最好和最坏情况下的复杂性是一样的,因此归并排序的平均复杂性为t (n)= (nl o gn)。
  j8 u9 @  x  u+ _9 t- Z( |( [+ g' G7 L' l" F$ m
图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分成两个大致相等的链表。
" L! u% S" ^* `6 F" [- z# i' e- l& O  b1 M
归并过程应能将两个已排序的链表归并在一起。如果希望把所得到C + +程序与堆排序和插入排序进行性能比较,那么就不能使用链表来实现归并排序,因为后两种排序方法中都没有使用链表。为了能与前面讨论过的排序函数作比较,归并排序函数必须用一个数组a来存储元素集合E,并在a 中返回排序后的元素序列。为此按照下述过程来对图1 4 - 6的伪代码进行细化:当集合E被化分成两个子集合时,可以不必把两个子集合的元素分别复制到A和B中,只需简单地在集合E中保持两个子集合的左右边界即可。接下来对a 中的初始序列进行排序,并将所得到的排序序列归并到一个新数组b中,最后将它们复制到a 中。图1 4 - 6的改进版见图1 4 - 7。
+ |" L! S1 B# P. H3 H: E. d5 p; V1 @# z2 D6 n/ v: T; V
) y4 e( c  c+ ^5 C* \7 Q
0 f( Y+ l4 P" Z+ P
template<CLASS T>' L: Q5 v* ]# L! U0 }; r: {

, i$ l5 B3 A% ^' }) x2 n7 XM e rgeSort( T a[], int left, int right)
1 D# e3 y7 M& \5 F
9 p3 K1 a; r& w) Q{ / /对a [ l e f t : r i g h t ]中的元素进行排序
7 s% d% F9 l' \  B. x7 U; u, c% s% P2 _1 s5 [! g0 g
if (left &lt; right) {//至少两个元素
5 q/ |1 L  c/ }- e- [& E/ Q
" h4 b. V  L( w, i2 H, w1 Q. cint i = (left + right)/2; //中心位置
; J' c! [4 u3 O0 d: N
) G; V4 ~, V0 P: B7 NM e rgeSort(a, left, i);
4 i! k% b1 N: Y+ ~8 }9 d
) m  g+ y( d8 c6 }M e rgeSort(a, i+1, right);
# i; I' N8 |4 r) \% ~, X8 g0 P# e/ v8 v7 {8 s4 {
M e rge(a, b, left, i, right); //从a 合并到b
1 x# k  @# R% u8 c
7 o$ {2 A5 [6 S' ]+ t5 pCopy(b, a, left, right); //结果放回a
: R1 J5 u  F; f4 Z1 w5 t
/ R* D' r8 G  ^0 Z; o}
" B" t- F! N$ v% {* t* U
6 [- b' `$ j4 W! l5 b}3 [; u  t- q$ m, \
0 {' g  A. U0 x" C; t/ E8 `+ c
图14-7 分而治之排序算法的改进
* H5 H0 M/ H. V, p" ^+ C# a" R2 Q. |
4 S- x0 Z7 X5 h7 l/ M9 T7 h# R9 e+ U6 N, M$ D5 O! g. q& P

  k4 c" b1 \# c1 h$ G7 f可以从很多方面来改进图1 4 - 7的性能,例如,可以容易地消除递归。如果仔细地检查图1 4 - 7中的程序,就会发现其中的递归只是简单地重复分割元素序列,直到序列的长度变成1为止。当序列的长度变为1时即可进行归并操作,这个过程可以用n 为2的幂来很好地描述。长度为1的序列被归并为长度为2的有序序列;长度为2的序列接着被归并为长度为4的有序序列;这个过程不断地重复直到归并为长度为n 的序列。图1 4 - 8给出n= 8时的归并(和复制)过程,方括号表示一个已排序序列的首和尾。
, w/ I9 _) Z! F  q) a, [
5 h: F: }9 B& u' l4 q: A7 k5 A5 c8 L2 t, U5 q3 e

" t. N  u5 O' \) X3 i7 k4 s初始序列[8] [4] [5] [6] [2] [1] [7] [3]
9 \7 x3 l+ x* f- c
' O5 B* h! Z' T# g归并到b [4 8] [5 6] [1 2] [3 7]
, n4 E5 C9 |1 r8 S; y' L! q6 q  o# }: F
复制到a [4 8] [5 6] [1 2] [3 7]  z# p2 G0 k- j6 g9 k
' r8 x( v# _3 u- F: D" W5 j2 S) q
归并到b [4 5 6 8] [1 2 3 7]
, n; Z) k" r; ?* x# D% b4 `- F4 |* e, W4 R& B* y/ V) d9 g  i
复制到a [4 5 6 8] [1 2 3 7]
4 b. D& `& C3 A; v0 B( c" I$ S( a0 P$ ~; _
归并到b [1 2 3 4 5 6 7 8]
8 k& |+ K( W- {3 p5 Q6 f: j& q: N& y+ `# T+ t
复制到a [1 2 3 4 5 6 7 8]7 }! ?* ~1 b! S+ H: t/ W0 Z
" f) j8 {5 N7 q- x- B0 V
图14-8 归并排序的例子
$ D! w, [; }# Z9 C; j4 R; I( e; L
6 g# ~3 o8 j; R
" T5 s/ I3 k9 `( U5 l# q3 i% d2 @8 O( k2 v* t+ Y/ T/ c1 c# C  S
另一种二路归并排序算法是这样的:首先将每两个相邻的大小为1的子序列归并,然后对上一次归并所得到的大小为2的子序列进行相邻归并,如此反复,直至最后归并到一个序列,归并过程完成。通过轮流地将元素从a 归并到b 并从b 归并到a,可以虚拟地消除复制过程。二路归并排序算法见程序1 4 - 3。
6 c& i) T0 M3 d
9 k  D; p- ?( D/ `& c' a) B程序14-3 二路归并排序' S/ H7 T% J" v$ o+ [9 K
1 H: O5 N" f) Z0 E+ O" {
template<CLASS T>) C" ?, I+ Z9 h2 C
+ A, a* W( X+ q5 T
void MergeSort(T a[], int n), S* I) E+ @' |* ?! w* ~0 v6 ?

# p+ o+ M" @; F, M3 O! d{// 使用归并排序算法对a[0:n-1] 进行排序
: t. P* O# b/ P  Y3 M  T8 l9 o- t9 l2 @7 W4 r$ F" i+ W; `: C
T *b = new T [n];5 E) f: y, ?& K! v- m* N+ ^$ D0 W
$ [4 l5 q  Y; P
int s = 1; // 段的大小& M2 {# F3 L6 p( k- W  n: A- l8 L
7 U. V* m1 w. Q: {& F7 G- W. x3 `0 @
while (s &lt; n) {
) {0 W9 s# c% t
+ l! i! m9 c& x* `' v$ W4 I  q& tMergePass(a, b, s, n); // 从a归并到b" S. [/ ?  r8 B" i

2 K2 a  d* i5 k8 c, Xs += s;8 T8 w+ H, d( N# R" J

+ w7 _: R' |  v/ _) D+ T/ xMergePass(b, a, s, n); // 从b 归并到a+ G, h" {. n8 K+ b( |/ n- L

; \9 h, J9 z. ~/ z* m# N! f" k5 Ys += s;
' ?2 [# ?! g: m
! |( o, D  b1 w}1 C, U2 {2 N1 u

! o/ R+ s# f4 N1 r* H}+ b+ o: H# M% `: D- o
/ I  G0 {; M; a0 l0 C  {) x. y' W
为了完成排序代码,首先需要完成函数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; =的目的是用来比较需要排序的域。
% f3 ]$ o; z6 o3 S; r3 \* n0 G
. {1 k, V  E% g5 F- [  o9 z程序14-4 MergePass函数) H/ T% J4 y% ~1 X1 h/ i

7 G! U) t% q7 f( n! d" Z% `# Ctemplate<CLASS T>; k! Z3 O% n# g- ~1 n2 y

: P  g/ x+ ]3 I# qvoid MergePass(T x[], T y[], int s, int n)) R5 K8 W! B8 k* v" [

7 z- A$ K. N) V4 D{// 归并大小为s的相邻段
3 ?, `* ]0 w# L: z- R8 i( I# {( U% J- u% P
int i = 0;3 M* M. D2 u1 |/ {
$ M: o, X2 y" U/ f
while (i &lt;= n - 2 * s) {% Q: @7 t6 q0 S  m
9 {) M( u/ C4 _' q0 @3 v! u4 V
// 归并两个大小为s的相邻段
; Q( d: @. k, d$ Y0 b5 T4 x) U& J* C. z
Merge(x, y, i, i+s-1, i+2*s-1);
# l1 F0 J( L( L2 o7 I( E) {1 |$ n' L+ k  [( |: J
i = i + 2 * s;
' f4 F$ U5 T. D) c
" p" [5 D+ r1 z9 [}7 @' ?! n; K( Z. p

- U4 C# P& g# t7 O6 D4 f6 n// 剩下不足2个元素
8 _5 H* e, A  h( W  R
3 u+ e6 {4 X' F4 C% ^) Nif (i + s &lt; n) Merge(x, y, i, i+s-1, n-1);
4 Q$ m2 u  v. K; M4 [6 J6 `' u. X8 y6 ~+ m: f8 u7 x* t
else for (int j = i; j &lt;= n-1; j++). p& H' L/ Q( S! P8 r
+ p( w" r0 Z! [+ m3 J7 k: H8 b
// 把最后一段复制到y  S7 V: d! J6 d6 p# A

/ }# ^9 l8 }+ E, O' n2 Ay[j] = x[j];2 n2 z# Z9 L) h5 Y: n( I) P* p

7 `# i& |# @) B( k5 D7 e}4 _! C# W+ Y& E& [' i: @% x9 z" g/ L, r5 |

8 V: b! \, `, X程序14-5 Merge函数, t  F/ V, W7 d8 }3 w5 v
+ W4 s& ~2 w, H! o; W$ ?( X6 w
template<CLASS T>
  _7 p7 Z  o# I
: `3 i& F. E. Qvoid Merge(T c[], T d[], int l, int m, int r)
& z* t4 l2 C  c/ \' j7 p7 m, ]% i; N; Z
{// 把c[l:m]] 和c[m:r] 归并到d [ l : r ] .
- C# i( g# K7 M, J. t3 i
2 n8 S+ ~: s( I* E) H( t, Hint i = l, // 第一段的游标- E" t; R; }: j8 Y6 k

0 \& q! ~7 u$ U7 ~* u2 q8 [3 Fj = m+1, // 第二段的游标# M1 j3 @. E0 C

4 G% l/ w) \3 G1 Bk = l; // 结果的游标7 @; \; H  T& c5 F1 H7 L3 o. ]- w
8 U( O7 W7 a) y) w
/ /只要在段中存在i和j,则不断进行归并& T4 ~) a: w5 E+ k/ v8 O7 y+ M
  _7 ^4 \" i% M0 f$ u* Q. D) Q
while ((i &lt;= m) &amp;&amp; (j &lt;= r))
7 W* x7 u8 N! |/ i9 a
5 o1 _% n9 d6 k6 E2 ?) rif (c &lt;= c[j]) d[k++] = c[i++];
" l  I$ g+ D6 i- T; U+ ]
2 |2 t9 w: ^; e. L" Jelse d[k++] = c[j++];
! s, C* R9 c  C8 @# s8 S6 l3 n; |+ I3 @3 ~; [+ y
// 考虑余下的部分0 P, {! y6 P( t! f: Y& r, T- O7 n
/ Y- ~) h( G3 n( K- C- s
if (i &gt; m) for (int q = j; q &lt;= r; q++)1 Q4 v9 V8 u* [* x  D' F$ J
; M/ h; b0 _8 @( c: {
d[k++] = c[q];
4 k' t3 t. v: K1 q
! p1 _% J7 b" ~$ @* z+ Kelse for (int q = i; q &lt;= m; q++)* `" ^. Q. L& L, w% {. h
" Y# E  z' C* V2 |9 r; U
d[k++] = c[q];  i2 ?5 p: f: B2 o! ^6 o. @( j" I

7 B  D7 I7 J" J5 w7 ]}9 y' F6 T/ F9 J' c) H6 ^  _
, D$ a  D1 h6 D' I. d8 a
自然归并排序(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