[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}
1∣∣∣∣2436587
1
2
∣
4
‾
3
‾
6
5
8
7
1\;2\bigg|{\underline4\atop\underline3}{6\atop5}{8\atop7}
12∣∣∣∣345678
当有一个序列耗尽时,需要特别处理,详细算法描述如下:
算法 M:(两路归并)
两个非空的已排好序的序列 x 1 ≤ x 2 ≤ ⋯ ≤ x m x_1\leq x_2\leq\cdots\leq x_m x1≤x2≤⋯≤xm和 y 1 ≤ y 2 ≤ ⋯ ≤ y n y_1\leq y_2\leq\cdots\leq y_n y1≤y2≤⋯≤yn 归并为一个有序序列 ( z 1 ≤ z 2 ≤ ⋯ ≤ z m + n ) (z_1\leq z_2\leq\cdots\leq z_{m+n}) (z1≤z2≤⋯≤zm+n)。
- M1.[初始化] 置 i ← 1 i\gets1 i←1, j ← 1 j\gets1 j←1, k ← 1 k\gets1 k←1。
- M2.[比较] 如果 x i ≤ y j x_i\leq y_j xi≤yj,转至M3,否则转至M5。
- M3.[输出 x i x_i xi] 置 z k ← x i z_k\gets x_i zk←xi, k ← k + 1 k\gets k+1 k←k+1, i ← i + 1 i\gets i+1 i←i+1。如果 i ≤ m i\leq m i≤m,返回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 zk←yj, k ← k + 1 k\gets k+1 k←k+1, j ← j + 1 j\gets j+1 j←j+1。如果 j ≤ n j\leq n j≤n,返回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 m≈n时是相当高效的,但是当 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 t←⌊lg(n/m)⌋=2,则第一个比较基点即为 n + 1 − 2 t = 10 + 1 − 4 = 7 {n+1-2^t}=10+1-4=7 n+1−2t=10+1−4=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 t←⌊lg(7/1)⌋=2,确定位置 n + 1 − 2 t = 7 + 1 − 4 = 4 n+1-2^t=7+1-4=4 n+1−2t=7+1−4=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 t←⌊lg(3/1)⌋=1,确定位置 3 + 1 − 2 = 2 3+1-2=2 3+1−2=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 t←0,唯一选择 n + 1 − 2 0 = 1 + 1 − 1 = 1 n+1-2^0=1+1-1=1 n+1−20=1+1−1=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 t←⌊lg(m/n)⌋ 并跳转至H4。如果 n > m n>m n>m,置 t ← ⌊ lg ( n / m ) ⌋ t\gets\lfloor\lg(n/m)\rfloor t←⌊lg(n/m)⌋。
- H2.[比较 A m : B n + 1 − 2 t A_m:B_{n+1-2^t} Am:Bn+1−2t] 如果 A m < B n + 1 − 2 t A_m<B_{n+1-2^t} Am<Bn+1−2t,输出 ( B n + 1 − 2 t , … , B n ) (B_{n+1-2^t},\ldots,B_n) (Bn+1−2t,…,Bn),置 n ← n − 2 t n\gets n-2^t n←n−2t,并返回H1。
- H3.[二叉查找]应用二叉查找 ( B n + 1 − 2 t , … , B n ) (B_{n+1-2^t},\ldots,B_n) (Bn+1−2t,…,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 m←m−1, n ← k n\gets k n←k。返回H1。
- H4.[比较 B n : A m + 1 − 2 t B_n:A_{m+1-2^t} Bn:Am+1−2t] (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+1−2t,输出 ( A m + 1 − 2 t , … , A m ) (A_{m+1-2^t},\ldots,A_m) (Am+1−2t,…,Am),置 m ← m − 2 t m\gets m-2^t m←m−2t,并返回H1。
- H5.[二叉查找]应用二叉查找 ( A m + 1 − 2 t , … , A m ) (A_{m+1-2^t},\ldots,A_m) (Am+1−2t,…,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 n←n−1, m ← k m\gets k m←k。返回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}
ORIG59∣18∣2∣64∣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}|{\;-\;-\;-\;-\;-\;}
DEST3579∣−−−−−
接着归并左侧
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}
DEST3579∣−∣8641
最后归并
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}
DEST3579∣2∣8641
然后进行空间反转,从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}
ORIG13456789∣2
再次反转空间,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}
DEST123456789
详细的算法描述如下:
算法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 s←0。(输出空间切换)
- N2.[准备遍历] 如果 s = 0 s=0 s=0,置 i ← 1 i\gets1 i←1, j ← N j\gets N j←N, k ← N + 1 k\gets N+1 k←N+1, l ← 2 N l\gets2N l←2N。如果 s = 1 s=1 s=1,置 i ← N + 1 i\gets N+1 i←N+1, j ← 2 N j\gets2N j←2N, k ← 1 k\gets1 k←1, l ← N l\gets N l←N。并置 d ← 1 d\gets1 d←1, f ← 1 f\gets1 f←1。( 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 Rk←Ri,并跳转至N13.
- N4.[输出 R i R_i Ri] 置 R k ← R i R_k\gets R_i Rk←Ri, k ← k + d k\gets k+d k←k+d。
- N5.[是否段尾?] i ← i + 1 i\gets i+1 i←i+1。如果 K i − 1 ≤ K i K_{i-1}\leq K_i Ki−1≤Ki,转至N3。
- N6.[输出 R j R_j Rj] 置 R k ← R j R_k\gets R_j Rk←Rj, k ← k + d k\gets k+d k←k+d。
- N7.[是否段尾?] j ← j − 1 j\gets j-1 j←j−1。如果 K j + 1 ≤ K j K_{j+1}\leq K_j Kj+1≤Kj,转至N6;否则跳转至N12。
- N8.[输出 R j R_j Rj] 置 R k ← R j R_k\gets R_j Rk←Rj, k ← k + d k\gets k+d k←k+d。
- N9.[是否段尾?] j ← j − 1 j\gets j-1 j←j−1,如果 K j + 1 ≤ K j K_{j+1}\leq K_j Kj+1≤Kj,跳转至N3。
- N10.[输出 R i R_i Ri] 置 R k ← R i R_k\gets R_i Rk←Ri, k ← k + d k\gets k+d k←k+d。
- N11.[是否段尾?] i ← i + 1 i\gets i+1 i←i+1,如果 K i − 1 ≤ K i K_{i-1}\leq K_i Ki−1≤Ki,返回N10.
- N12.[切换输出方向] 置 f ← 0 f\gets0 f←0, d ← − d d\gets-d d←−d,交换 k ↔ l k\leftrightarrow l k↔l。返回N3。
- N13.[切换输出空间] 如果 f = 0 f=0 f=0,置 s ← 1 − s s\gets1-s s←1−s并返回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)=lgN−1,每次遍历都要移动 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 s←0, p ← 1 p\gets1 p←1。
- S2.[准备遍历] 如果 s = 0 s=0 s=0,置 i ← 1 i\gets1 i←1, j ← N j\gets N j←N, k ← N + 1 k\gets N+1 k←N+1, l ← 2 N = 1 l\gets2N=1 l←2N=1。如果 s = 1 s=1 s=1,置 i ← N + 1 i\gets N+1 i←N+1, j ← 2 N j\gets2N j←2N, k ← 0 k\gets0 k←0, l ← N = 1 l\gets N=1 l←N=1。并置 d ← 1 d\gets1 d←1, q ← p q\gets p q←p, r ← p r\gets p r←p。
- 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 k←k+d, R k ← R i R_k\gets R_i Rk←Ri
- S5.[是否段尾?] i ← i + 1 i\gets i+1 i←i+1, q ← q − 1 q\gets q-1 q←q−1。如果 q > 0 q>0 q>0,返回S3。
- S6.[输出 R j R_j Rj] k ← k + d k\gets k+d k←k+d,如果 k = l k=l k=l,跳转至S13;否则置 R k ← R j R_k\gets R_j Rk←Rj。
- S7.[是否段尾?] j ← j − 1 j\gets j-1 j←j−1, r ← r − 1 r\gets r-1 r←r−1。如果 r > 0 r>0 r>0,返回S6;否则转至S12。
- S8.[输出 R j R_j Rj] k ← k + d k\gets k+d k←k+d, R k ← R j R_k\gets R_j Rk←Rj。
- S9.[是否段尾?] j ← j − 1 j\gets j-1 j←j−1, r ← r − 1 r\gets r-1 r←r−1。如果 r > 0 r>0 r>0,返回S3。
- S10.[输出 R i R_i Ri] k ← k + d k\gets k+d k←k+d。如果 k = l k=l k=l,转至S13;否则置 R k ← R i R_k\gets R_i Rk←Ri。
- S11.[是否段尾?] i ← i + 1 i\gets i+1 i←i+1, q ← q − 1 q\gets q-1 q←q−1。如果 q > 0 q>0 q>0,返回S10。
- S12.[切换输出方向] 置 q ← p q\gets p q←p, r ← p r\gets p r←p, d ← − d d\gets-d d←−d,交换 k ↔ l k\leftrightarrow l k↔l。如果 j − i < p j-i<p j−i<p,返回S10。否则返回S3。
- S13.[切换输出空间] 置 p ← p + p p\gets p+p p←p+p。如果 p < n p<n p<n,置 s ← 1 − s s\gets1-s s←1−s并返回S2。否则,结束。
现在我们详细解释人工分段归并如何解决 N N N不是 2 p 2p 2p的倍数的情况:
在最后阶段,总是位于中间有一个 < 2 p <2p <2p的序列,设长度为 t t t,当 0 ≤ t < p 0\leq t<p 0≤t<p时,在S12中 j − i < p j-i<p j−i<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 x1≤x2≤…≤xp∣yt≥…y1。如果 x p ≤ y t x_p\leq y_t xp≤yt,左侧首先耗尽,处理右侧,当右侧处理完成时,有条件 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,…,xp−1依次输出,直到满足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
26∣410∣814∣1216∣1511∣137∣93∣51
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
1256781314∣1615121110943
4、链表归并排序
上面讨论的归并排序都需要 2 N 2N 2N的空间,下面介绍链表排序,虽然它也需要额外的链间空间,但省去了移动数据操作。这个算法需要 N + 2 N+2 N+2的额外空间, N N N用来存放链接地址,2个空间用来作为两个链接的表头。
首先让两个表头分别指向 L 0 ← 1 L_0\gets1 L0←1和 L N + 1 ← 2 L_{N+1}\gets2 LN+1←2,链接地址 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 L0←1, L N + 1 ← 2 L_{N+1}\gets2 LN+1←2, L i ← − ( i + 2 ) L_i\gets -(i+2) Li←−(i+2)for 1 ≤ i ≤ N − 2 1\leq i\leq N-2 1≤i≤N−2,并置 L N − 1 ← L N ← 0 L_{N-1}\gets L_N\gets0 LN−1←LN←0。
- L2.[准备遍历] 置 s ← 0 s\gets0 s←0, t ← N + 1 t\gets N+1 t←N+1, p ← L s p\gets L_s p←Ls, q ← L t q\gets L_t q←Lt。如果 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 ∣Ls∣←p, s ← p s\gets p s←p, p ← L p p\gets L_p p←Lp。如果 p > 0 p>0 p>0,返回L3。
- L5.[完成剩余链表] 置 L s ← p L_s\gets p Ls←p, s ← t s\gets t s←t。然后置 t ← q t\gets q t←q, q ← L q q\gets L_q q←Lq重复直到 q ≤ 0 q\leq0 q≤0,转至L8。
- L6.[前进q] 置 ∣ L s ∣ ← q |L_s|\gets q ∣Ls∣←q, s ← q s\gets q s←q, q ← L q q\gets L_q q←Lq。如果 q > 0 q>0 q>0,返回L3。
- L7.[完成生于链表] 置 L s ← p L_s\gets p Ls←p, s ← t s\gets t s←t。然后 t ← p t\gets p t←p, p ← L p p\gets L_p p←Lp重复直到 p ≤ 0 p\leq0 p≤0。
- L8.[遍历是否结束?] 置 p ← − p p\gets-p p←−p, q ← − q q\gets-q q←−q。如果 q = 0 q=0 q=0,置 ∣ L s ∣ ← p |L_s|\gets p ∣Ls∣←p, ∣ L t ∣ ← 0 |L_t|\gets0 ∣Lt∣←0,并返回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
HEAD→R3→R1→R2
可见链接地址即为
(
2
,
3
,
1
)
(2,3,1)
(2,3,1)的逆
(
3
,
1
,
2
)
(3,1,2)
(3,1,2)。