[Alg]排序算法之归并排序

归并排序是一种在任何情况下都能保证Nlg(N)时间复杂度的排序算法,尤其适用于大数据量。本文详细介绍了两路归并排序算法,包括算法M和H,以及自然分段和人工分段的两路归并排序算法。此外,还探讨了链表归并排序,该方法节省了移动数据操作,只需N+2的额外空间。
摘要由CSDN通过智能技术生成

[Alg]排序算法之归并排序

作者:屎壳郎 miaosg01@163.com

日期:Aug 2021

版次:初版


简介: 归并排序是一类在任何情况下都能保证 N lg ⁡ ( N ) N\lg(N) Nlg(N)的排序算法,它在数据量比较小时体现的不明显,但在大数据量时优势明显。归并排序在外部排序中尤其重要,由于受到外部存储条件的限制,归并排序几乎是唯一选择。

1、两路归并

设有两个有序的序列1 4 6 8与2 3 5 7,我们比较头部两个较小的项,然后输出最小者,然后重复这个步骤直到两个序列都清空。
∣ 1 ‾ 2 ‾ 4 3 6 5 8 7 \bigg|{\underline1\atop\underline2} {4\atop3}{6\atop5}{8\atop7} 21345678
1 ∣ 4 ‾ 2 ‾ 6 3 8 5 7 1\bigg|{\underline4\atop\underline2}{6\atop3}{8\atop5}{ \atop7} 12436587
1    2 ∣ 4 ‾ 3 ‾ 6 5 8 7 1\;2\bigg|{\underline4\atop\underline3}{6\atop5}{8\atop7} 12345678

当有一个序列耗尽时,需要特别处理,详细算法描述如下:

算法 M:(两路归并)

两个非空的已排好序的序列 x 1 ≤ x 2 ≤ ⋯ ≤ x m x_1\leq x_2\leq\cdots\leq x_m x1x2xm y 1 ≤ y 2 ≤ ⋯ ≤ y n y_1\leq y_2\leq\cdots\leq y_n y1y2yn 归并为一个有序序列 ( z 1 ≤ z 2 ≤ ⋯ ≤ z m + n ) (z_1\leq z_2\leq\cdots\leq z_{m+n}) (z1z2zm+n)

  • M1.[初始化] 置 i ← 1 i\gets1 i1 j ← 1 j\gets1 j1 k ← 1 k\gets1 k1
  • M2.[比较] 如果 x i ≤ y j x_i\leq y_j xiyj,转至M3,否则转至M5。
  • M3.[输出 x i x_i xi] 置 z k ← x i z_k\gets x_i zkxi k ← k + 1 k\gets k+1 kk+1 i ← i + 1 i\gets i+1 ii+1。如果 i ≤ m i\leq m im,返回M2。
  • M4.[整体输出 y j , … , y n y_j,\ldots,y_n yj,,yn] 置 ( z k , … , z m + n ) ← ( y j , … , y n ) (z_k,\ldots,z_{m+n})\gets(y_j,\ldots,y_n) (zk,,zm+n)(yj,,yn)
  • M5.[输出 y j y_j yj] 置 z k ← y j z_k\gets y_j zkyj k ← k + 1 k\gets k+1 kk+1 j ← j + 1 j\gets j+1 jj+1。如果 j ≤ n j\leq n jn,返回M2。
  • M6.[整体输出 x i , … , x m x_i,\ldots,x_m xi,,xm] 置 ( z k , … , z m + n ) ← ( x i , … , x m ) (z_k,\ldots,z_{m+n})\gets(x_i,\ldots,x_m) (zk,,zm+n)(xi,,xm),并结束。

为使得代码更加简洁,我们可以设置 x m + 1 ← ∞ x_{m+1}\gets\infty xm+1,$ y_{n+1}\gets\infty$,这样我们就不用特别处理一个序列先耗尽的情况。在《[Alg]排序算法之插入排序》介绍直接插入排序算法时,为了提高效率,介绍了一种一次插入两个或更多项的改进措施,其实这就是归并排序的思想。同样,如果只归并一个数据项,也就是插入排序了,可以把它看作是归并排序的一个特例。

上面这个算法在 m ≈ n m\approx n mn时是相当高效的,但是当 m > > n m>>n m>>n m < < n m<<n m<<n时,就要进行大量冗余的比较,使得算法性能相当低下。下面介绍一个专门应对这种情况的算法。

考虑 A ( 2 , 9 ) A(2,9) A(2,9) B ( 1 , 3 , 4 , 5 , 6 , 7 , 8 , 10 , 11 , 12 ) B(1,3,4,5,6,7,8,10,11,12) B(1,3,4,5,6,7,8,10,11,12)归并, m = 2 < < n = 10 m=2<<n=10 m=2<<n=10的情况,归并结果放入 C C C中。假设 A A A B B B中的分布是均匀一致的,那 A A A中的两个数 ( 2 , 9 ) (2,9) (2,9)应该把 B B B分成三段,那插入位置就是 B B B 1 3 {1\over3} 31 2 3 {2\over3} 32的位置,那我们就选取这些位置作为起点来确定比较的基点是合理的。这有点类似于shell排序和Batcher排序,增大插入和交换的跨度,我们在这采用的方法更优秀,第一,确定最合理的比较基点,第二,并以此基点圈定范围采用二叉搜索加速进度。采用如下方法确定位置:

t ← ⌊ lg ⁡ ( n / m ) ⌋ = 2 t\gets\lfloor\lg(n/m)\rfloor=2 tlg(n/m)=2,则第一个比较基点即为 n + 1 − 2 t = 10 + 1 − 4 = 7 {n+1-2^t}=10+1-4=7 n+12t=10+14=7 B 7 = 8 < 9 B_7=8<9 B7=8<9,然后用二叉法在 { B 7 , B 8 , B 9 , B 10 } \{B_7,B_8,B_9,B_{10}\} {B7,B8,B9,B10}中确定位置,输出 C ( − , − , − , − , − , − , − , 8 , 9 , 10 , 11 , 12 ) C(-,-,-,-,-,-,-,8,9,10,11,12) C(,,,,,,,8,9,10,11,12)

剩余 A ( 2 ) A(2) A(2) B ( 1 , 3 , 4 , 5 , 6 , 7 , 8 ) B(1,3,4,5,6,7,8) B(1,3,4,5,6,7,8)归并: t ← ⌊ lg ⁡ ( 7 / 1 ) ⌋ = 2 t\gets\lfloor\lg(7/1)\rfloor=2 tlg(7/1)=2,确定位置 n + 1 − 2 t = 7 + 1 − 4 = 4 n+1-2^t=7+1-4=4 n+12t=7+14=4 B 4 = 5 > 2 B_4=5>2 B4=5>2,输出 { B 4 , B 5 , B 6 , B 7 } \{B_4,B_5,B_6,B_7\} {B4,B5,B6,B7} C ( − , − , − , − , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 ) C(-,-,-,-,5,6,7,8,9,10,11,12) C(,,,,5,6,7,8,9,10,11,12)

剩余 A ( 2 ) A(2) A(2) B ( 1 , 3 , 4 ) B(1,3,4) B(1,3,4)归并: t ← ⌊ lg ⁡ ( 3 / 1 ) ⌋ = 1 t\gets\lfloor\lg(3/1)\rfloor=1 tlg(3/1)=1,确定位置 3 + 1 − 2 = 2 3+1-2=2 3+12=2 B 2 = 3 > 2 B_2=3>2 B2=3>2,输出 { B 2 , B 3 } \{B_2,B_3\} {B2,B3} C ( − , − , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 ) C(-,-,3,4,5,6,7,8,9,10,11,12) C(,,3,4,5,6,7,8,9,10,11,12)

至此只剩 A ( 2 ) A(2) A(2) B ( 1 ) B(1) B(1) t ← 0 t\gets0 t0,唯一选择 n + 1 − 2 0 = 1 + 1 − 1 = 1 n+1-2^0=1+1-1=1 n+120=1+11=1归并。

总结算法如下:

算法H:(二叉归并)

设两个有序序列 A 1 , … , A m A_1,\ldots,A_m A1,,Am B 1 , … , B n B_1,\ldots,B_n B1,,Bn

  • H1.[初始化] 如果 m m m n n n为0,结束;否则,如果 m > n m>n m>n,置 t ← ⌊ lg ⁡ ( m / n ) ⌋ t\gets\lfloor\lg(m/n)\rfloor tlg(m/n) 并跳转至H4。如果 n > m n>m n>m,置 t ← ⌊ lg ⁡ ( n / m ) ⌋ t\gets\lfloor\lg(n/m)\rfloor tlg(n/m)
  • H2.[比较 A m : B n + 1 − 2 t A_m:B_{n+1-2^t} Am:Bn+12t] 如果 A m < B n + 1 − 2 t A_m<B_{n+1-2^t} Am<Bn+12t,输出 ( B n + 1 − 2 t , … , B n ) (B_{n+1-2^t},\ldots,B_n) (Bn+12t,,Bn),置 n ← n − 2 t n\gets n-2^t nn2t,并返回H1。
  • H3.[二叉查找]应用二叉查找 ( B n + 1 − 2 t , … , B n ) (B_{n+1-2^t},\ldots,B_n) (Bn+12t,,Bn),确定 k k k为满足 B k < A m B_k<A_m Bk<Am条件的最大值,则输出 B k + 1 , … B n B_{k+1},\ldots B_n Bk+1,Bn A m A_m Am,并置 m ← m − 1 m\gets m-1 mm1 n ← k n\gets k nk。返回H1。
  • H4.[比较 B n : A m + 1 − 2 t B_n:A_{m+1-2^t} Bn:Am+12t] (H4和H5与H2 H3执行的操作一样,只是交换了 m m m n n n的角色)如果 B n < A m + 1 − 2 t B_n<A_{m+1-2^t} Bn<Am+12t,输出 ( A m + 1 − 2 t , … , A m ) (A_{m+1-2^t},\ldots,A_m) (Am+12t,,Am),置 m ← m − 2 t m\gets m-2^t mm2t,并返回H1。
  • H5.[二叉查找]应用二叉查找 ( A m + 1 − 2 t , … , A m ) (A_{m+1-2^t},\ldots,A_m) (Am+12t,,Am),确定 k k k为满足 A k < B n A_k<B_n Ak<Bn条件的最大值,则输出 A k + 1 , … A m A_{k+1},\ldots A_m Ak+1,Am B n B_n Bn,并置 n ← n − 1 n\gets n-1 nn1 m ← k m\gets k mk。返回H1。

这个算法对m(假设 m < < n m<<n m<<n)均匀分布一致分布在 m + n m+n m+n中的情况特别高效。它融合了二叉和归并两方面的优势。

2、自然分段的两路归并排序

上面讲了归并两个序列,要求序列在归并前就排好序,下面讲解利用归并思想进行排序。自然分段的归并排序就是利用序列自然形成的上升段和下降段归并,采用上面提到的归并方法形成更大的升段和降段,并最终完成排序。

采用两头点蜡的方法,从左端取升段从右侧取降段进行一次归并,并把结果放置到另外的 N N N空间,我们给它起个名字DEST,后面好进行描述,原来的空间命名为ORIG。
O R I G 5    9 → ∣    1    8 → ∣    2    ∣ 6    4 ←    ∣ 7    3 ← ORIG\qquad\underrightarrow{5\;9}|\underrightarrow{\;1\;8}|\;2\;|\overleftarrow{6\;4}\;|\overleftarrow{7\;3} ORIG 59 18264 73

首先归并左侧 5    9 5\;9 59与右侧的 7    3 7\;3 73,输出空间为:
D E S T 3    5    7    9 → ∣    −    −    −    −    −    DEST\qquad\underrightarrow{3\;5\;7\;9}|{\;-\;-\;-\;-\;-\;} DEST 3579

接着归并左侧 1    8 1\;8 18与右侧 6    4 6\;4 64,输出空间为:
D E S T 3    5    7    9 → ∣ − ∣    8    6    4    1 ← DEST\qquad\underrightarrow{3\;5\;7\;9}|-|\overleftarrow{\;8\;6\;4\;1} DEST 35798641

最后归并 2 2 2:
D E S T 3    5    7    9 → ∣    2    ∣    8    6    4    1 ← DEST\qquad\underrightarrow{3\;5\;7\;9}|\;2\;|\overleftarrow{\;8\;6\;4\;1} DEST 357928641

然后进行空间反转,从DEST向ORIG归并输出:
O R I G 1    3    4    5    6    7    8    9 → ∣    2 ← ORIG\qquad\underrightarrow{1\;3\;4\;5\;6\;7\;8\;9}|\overleftarrow{\;2} ORIG 134567892

再次反转空间,ORIG向DEST输出:
D E S T 1    2    3    4    5    6    7    8    9 → DEST\qquad\underrightarrow{1\;2\;3\;4\;5\;6\;7\;8\;9} DEST 123456789

详细的算法描述如下:

算法N:(自然分段两路归并排序)

设记录 R 1 , R 2 , … , R N R_1,R_2,\ldots,R_N R1,R2,,RN,其对应的键值 K 1 , K 2 , … , K N K_1,K_2,\ldots,K_N K1,K2,,KN。需要额外的 N N N空间。

  • N1.[初始化] 置 s ← 0 s\gets0 s0。(输出空间切换)
  • N2.[准备遍历] 如果 s = 0 s=0 s=0,置 i ← 1 i\gets1 i1 j ← N j\gets N jN k ← N + 1 k\gets N+1 kN+1 l ← 2 N l\gets2N l2N。如果 s = 1 s=1 s=1,置 i ← N + 1 i\gets N+1 iN+1 j ← 2 N j\gets2N j2N k ← 1 k\gets1 k1 l ← N l\gets N lN。并置 d ← 1 d\gets1 d1 f ← 1 f\gets1 f1。( d d d与换边相关,头向尾接收数据时 d = 1 d=1 d=1,尾向头时 d = − 1 d=-1 d=1
  • N3.[比较 K i : K j K_i:K_j Ki:Kj] 如果 K i > K j K_i>K_j Ki>Kj,跳转至N8。如果 i = j i=j i=j,置 R k ← R i R_k\gets R_i RkRi,并跳转至N13.
  • N4.[输出 R i R_i Ri] 置 R k ← R i R_k\gets R_i RkRi k ← k + d k\gets k+d kk+d
  • N5.[是否段尾?] i ← i + 1 i\gets i+1 ii+1。如果 K i − 1 ≤ K i K_{i-1}\leq K_i Ki1Ki,转至N3。
  • N6.[输出 R j R_j Rj] 置 R k ← R j R_k\gets R_j RkRj k ← k + d k\gets k+d kk+d
  • N7.[是否段尾?] j ← j − 1 j\gets j-1 jj1。如果 K j + 1 ≤ K j K_{j+1}\leq K_j Kj+1Kj,转至N6;否则跳转至N12。
  • N8.[输出 R j R_j Rj] 置 R k ← R j R_k\gets R_j RkRj k ← k + d k\gets k+d kk+d
  • N9.[是否段尾?] j ← j − 1 j\gets j-1 jj1,如果 K j + 1 ≤ K j K_{j+1}\leq K_j Kj+1Kj,跳转至N3。
  • N10.[输出 R i R_i Ri] 置 R k ← R i R_k\gets R_i RkRi k ← k + d k\gets k+d kk+d
  • N11.[是否段尾?] i ← i + 1 i\gets i+1 ii+1,如果 K i − 1 ≤ K i K_{i-1}\leq K_i Ki1Ki,返回N10.
  • N12.[切换输出方向] 置 f ← 0 f\gets0 f0 d ← − d d\gets-d dd,交换 k ↔ l k\leftrightarrow l kl。返回N3。
  • N13.[切换输出空间] 如果 f = 0 f=0 f=0,置 s ← 1 − s s\gets1-s s1s并返回N2。否则 f = 1 f=1 f=1,结束。( f f f控制着算法结束,在最后一趟归并阶段N2设置 f = 1 f=1 f=1,运行过程中不发生换边N12,说明完成归并,在N13推出。

从概率上来看, K i > K j K_i>K_j Ki>Kj K i < K j K_i<K_j Ki<Kj各占 1 / 2 1/2 1/2的概率,每趟遍历归并操作,段减少一半(两两合并),所以总的遍历次数为 lg ⁡ ( 1 2 N ) = lg ⁡ N − 1 \lg({1\over2}N)=\lg N -1 lg(21N)=lgN1,每次遍历都要移动 N N N个数据,复杂度 O ( N lg ⁡ ( N ) ) O(N\lg(N)) O(Nlg(N))

3、人工分段的两路归并排序

上面提到,自然分段依赖于假设所有记录服从一致分布, K i > K j K_i>K_j Ki>Kj K i < K j K_i<K_j Ki<Kj各占 1 / 2 1/2 1/2的概率。当待排序的序列中存在大量的升序段或降序段时,该算法处理的速度非常快。但是段太多时,主循环运行大量的分段测试语句,这拖慢了主循环。下面引入一种不依赖于自然分段的归并排序算法。我们人为的进行分段,先从1开始,然后2,4,8,…,k次遍历后分段长度为 2 k 2^k 2k。虽然人为分段长度为2的指数倍,同样能处理 N N N不是2的指数倍的情况,后面讲解具体如何实现。

人工分段与自然分段没有什么本质上的区别,所以基本上程序也一样,只是在分段检测时的条件改变了。

算法S:(人工分段归并排序)

  • S1.[初始化] 置 s ← 0 s\gets0 s0 p ← 1 p\gets1 p1
  • S2.[准备遍历] 如果 s = 0 s=0 s=0,置 i ← 1 i\gets1 i1 j ← N j\gets N jN k ← N + 1 k\gets N+1 kN+1 l ← 2 N = 1 l\gets2N=1 l2N=1。如果 s = 1 s=1 s=1,置 i ← N + 1 i\gets N+1 iN+1 j ← 2 N j\gets2N j2N k ← 0 k\gets0 k0 l ← N = 1 l\gets N=1 lN=1。并置 d ← 1 d\gets1 d1 q ← p q\gets p qp r ← p r\gets p rp
  • S3.[比较 K i : K j K_i:K_j Ki:Kj] 如果 K i > K j K_i>K_j Ki>Kj,跳转至S8。
  • S4.[输出 R i R_i Ri] k ← k + d k\gets k+d kk+d R k ← R i R_k\gets R_i RkRi
  • S5.[是否段尾?] i ← i + 1 i\gets i+1 ii+1 q ← q − 1 q\gets q-1 qq1。如果 q > 0 q>0 q>0,返回S3。
  • S6.[输出 R j R_j Rj] k ← k + d k\gets k+d kk+d,如果 k = l k=l k=l,跳转至S13;否则置 R k ← R j R_k\gets R_j RkRj
  • S7.[是否段尾?] j ← j − 1 j\gets j-1 jj1 r ← r − 1 r\gets r-1 rr1。如果 r > 0 r>0 r>0,返回S6;否则转至S12。
  • S8.[输出 R j R_j Rj] k ← k + d k\gets k+d kk+d R k ← R j R_k\gets R_j RkRj
  • S9.[是否段尾?] j ← j − 1 j\gets j-1 jj1 r ← r − 1 r\gets r-1 rr1。如果 r > 0 r>0 r>0,返回S3。
  • S10.[输出 R i R_i Ri] k ← k + d k\gets k+d kk+d。如果 k = l k=l k=l,转至S13;否则置 R k ← R i R_k\gets R_i RkRi
  • S11.[是否段尾?] i ← i + 1 i\gets i+1 ii+1 q ← q − 1 q\gets q-1 qq1。如果 q > 0 q>0 q>0,返回S10。
  • S12.[切换输出方向] 置 q ← p q\gets p qp r ← p r\gets p rp d ← − d d\gets-d dd,交换 k ↔ l k\leftrightarrow l kl。如果 j − i < p j-i<p ji<p,返回S10。否则返回S3。
  • S13.[切换输出空间] 置 p ← p + p p\gets p+p pp+p。如果 p < n p<n p<n,置 s ← 1 − s s\gets1-s s1s并返回S2。否则,结束。

现在我们详细解释人工分段归并如何解决 N N N不是 2 p 2p 2p的倍数的情况:

在最后阶段,总是位于中间有一个 < 2 p <2p <2p的序列,设长度为 t t t,当 0 ≤ t < p 0\leq t<p 0t<p时,在S12中 j − i < p j-i<p ji<p会跳转至S10,把它和一个空序列归并。

如果剩余一个长度 p p p的序列和长度为 t < p t<p t<p的序列,会有以下情况: x 1 ≤ x 2 ≤ … ≤ x p ∣ y t ≥ … y 1 x_1\leq x_2\leq\ldots\leq x_p|y_t\geq\ldots y_1 x1x2xpyty1。如果 x p ≤ y t x_p\leq y_t xpyt,左侧首先耗尽,处理右侧,当右侧处理完成时,有条件 k = l k=l k=l成立,跳转S13转换输出空间。如果 x p > y t x_p>y_t xp>yt右侧先耗尽后 K j = x p K_j=x_p Kj=xp为最大值,剩余的 x i , x i + 1 , … , x p − 1 x_i,x_{i+1},\ldots,x_{p-1} xi,xi+1,,xp1依次输出,直到满足S6中 k = l k=l k=l跳转至S13。所以在任何情况下,上述程序都能圆满运行。

也可以像在快速排序中采取的办法一样优化,快排在末尾阶段采用直接插入排序来避免快排的低效。也可以采用同样的办法来加速人工分段的归并排序,只是放在开始阶段(归并排序像是直接插入排序的反操作)。可以把16个或32个(2的指数倍)排好序,然后再归并。

虽然人工分段减小了主循环中的分段检测,但机械的分段也丧失了自然分段中的一个优势,在自然分段中,如果存在 m m m段,归并后可能会出现 < m 2 <{m\over2} <2m的情况,见下面的例子:

初始序列存在8个段,经一次趟归并后应该还有4个段,但实际上只存在2个段,有两个消失了。这就是自然分段的优势,不过这种情况的出现是不可预期的。
2    6    ∣ 4    10    ∣ 8    14    ∣ 12    16    ∣ 15    11    ∣ 13    7    ∣ 9    3    ∣ 5    1 2\; 6\;| 4\; 10\;| 8\; 14\; |12\; 16\; |15\; 11\; |13\; 7\;| 9\; 3\;| 5\; 1 26410814121615111379351
1    2    5    6    7    8    13    14 ∣    16    15    12    11    10    9    4    3 1\;2 \;5\; 6\;7\;8\;13\;14|\;16\;15\;12\;11\;10\;9\;4\;3 12567813141615121110943

4、链表归并排序

上面讨论的归并排序都需要 2 N 2N 2N的空间,下面介绍链表排序,虽然它也需要额外的链间空间,但省去了移动数据操作。这个算法需要 N + 2 N+2 N+2的额外空间, N N N用来存放链接地址,2个空间用来作为两个链接的表头。

首先让两个表头分别指向 L 0 ← 1 L_0\gets1 L01 L N + 1 ← 2 L_{N+1}\gets2 LN+12,链接地址 L i ← − ( i + 2 ) L_i\gets -(i+2) Li(i+2),这样一个数组就被分成了两个链表, L 0 L_0 L0串起索引地址为奇数的数组项 R 1 , R 3 , R 5 , … R_1,R_3,R_5,\ldots R1,R3,R5, L N + 1 L_{N+1} LN+1串起数组偶数项。负号作为分段的标志。这种把数组索引地址作为链接的方法我们在《排列与反序》中也应用过,不妨称为相对链接(对应于绝对地址指针),这是链表技术在顺序存储中聪明的应用。

还是以 ( 5 , 9 , 1 , 8 , 2 , 6 , 4 , 7 , 3 ) (5,9,1,8,2,6,4,7,3) (5,9,1,8,2,6,4,7,3)为例说明。经过上面的处理,有如下结构
在这里插入图片描述
整理后两个链表:

在这里插入图片描述

第一趟遍历,依次比较归并 p p p q q q所指向的两个链表。因为不好理解,下图给出了第一趟遍历归并的每一步操作图示,请参照算法,一步一步对照。红、蓝分别代便了两个链表分段重组过程。

在这里插入图片描述

第一趟遍历完成后,所形成的新结构:
在这里插入图片描述

算法L:(链表归并排序)

  • L1.[准备两个链表] 置 L 0 ← 1 L_0\gets1 L01 L N + 1 ← 2 L_{N+1}\gets2 LN+12 L i ← − ( i + 2 ) L_i\gets -(i+2) Li(i+2)for 1 ≤ i ≤ N − 2 1\leq i\leq N-2 1iN2,并置 L N − 1 ← L N ← 0 L_{N-1}\gets L_N\gets0 LN1LN0
  • L2.[准备遍历] 置 s ← 0 s\gets0 s0 t ← N + 1 t\gets N+1 tN+1 p ← L s p\gets L_s pLs q ← L t q\gets L_t qLt。如果 q = 0 q=0 q=0,算法结束。
  • L3.[比较 K p : K q K_p:K_q Kp:Kq] 如果 K p > K q K_p>K_q Kp>Kq,转至L6。
  • L4.[前进p] 置 ∣ L s ∣ ← p |L_s|\gets p Lsp s ← p s\gets p sp p ← L p p\gets L_p pLp。如果 p > 0 p>0 p>0,返回L3。
  • L5.[完成剩余链表] 置 L s ← p L_s\gets p Lsp s ← t s\gets t st。然后置 t ← q t\gets q tq q ← L q q\gets L_q qLq重复直到 q ≤ 0 q\leq0 q0,转至L8。
  • L6.[前进q] 置 ∣ L s ∣ ← q |L_s|\gets q Lsq s ← q s\gets q sq q ← L q q\gets L_q qLq。如果 q > 0 q>0 q>0,返回L3。
  • L7.[完成生于链表] 置 L s ← p L_s\gets p Lsp s ← t s\gets t st。然后 t ← p t\gets p tp p ← L p p\gets L_p pLp重复直到 p ≤ 0 p\leq0 p0
  • L8.[遍历是否结束?] 置 p ← − p p\gets-p pp q ← − q q\gets-q qq。如果 q = 0 q=0 q=0,置 ∣ L s ∣ ← p |L_s|\gets p Lsp ∣ L t ∣ ← 0 |L_t|\gets0 Lt0,并返回L2。否则返回L3。

这个算法要比上面提到的自然分段、人工分段的归并排序快约 10 % 10\% 10% 20 % 20\% 20%。上面的方法是采用了两路归并,进一步考虑,如果分成三个链表,采用三路归并效果会如何呢?回想在《[Alg]排序算法之选择排序》中提到的堆排序也有同样的问题,如果堆排序中,也选择三分会如何呢?上面提到,两路归并,一趟段数减少一半,那三路合并就是原来的 1 3 {1\over3} 31,故运行的趟数 log ⁡ 3 N \log_3N log3N,复杂度为 ( N log ⁡ 3 N ) (N\log_3N) (Nlog3N),堆排序也是如此。虽然复杂度稍有提高,但多了好多 Θ ( N ) \Theta(N) Θ(N)项。

链表归并排序还顺便把数组排列的逆求出来了(注意逆序和反序的区别)。例如 ( 2 , 3 , 1 ) (2,3,1) (2,3,1),其逆为: ( 3 , 1 , 2 ) (3,1,2) (3,1,2)
( 2 1    3 2    1 3 ) ⇒ ( 1 3    2 1    3 2 ) \biggl({2\atop1}\;{3\atop2}\;{1\atop3}\biggr)\Rightarrow\biggl({1\atop3}\;{2\atop1}\;{3\atop2}\biggr) (122331)(311223)

( 2 R 1    3 R 2    1 R 3 ) \biggl({2\atop R_1}\;{3\atop R_2}\;{1\atop R_3}\biggr) (R12R23R31)

排好序后有:
H E A D → R 3 → R 1 → R 2 HEAD\to R_3\to R_1\to R_2 HEADR3R1R2
可见链接地址即为 ( 2 , 3 , 1 ) (2,3,1) (2,3,1)的逆 ( 3 , 1 , 2 ) (3,1,2) (3,1,2)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值