[Alg]排序算法之交换排序
作者:屎壳郎 miaosg01@163.com
日期:July 2021
版次:初版
简介: 交换排序是排序算法中的一大类,通过构成反序数据对的两两交换来消除反序,最终得到有秩序的排列。那么排列交换的本质是什么呢?说来话长,让我们回到远古时代,重温数据结构,从环形链表开始说起。
0、 交换排序的本质
环形链表的合并:
假设现在我们有两个指针分别指向两个环形链表,要想把两个环形链表合并成一个的操作为:
L
I
N
K
(
p
t
r
1
)
↔
L
I
N
K
(
p
t
r
2
)
LINK(ptr1)\leftrightarrow LINK(ptr2)
LINK(ptr1)↔LINK(ptr2),即交换这两个指针的链接地址。见下图:

环形链表的拆分:
那么反过来,我们想把一个环形链表拆分成两个环形链表,如何操作呢?同样的操作,只是两个指针要指向同一个环形链表内:
L
I
N
D
(
p
t
r
1
)
↔
L
I
N
K
(
p
t
r
2
)
LIND(ptr1)\leftrightarrow LINK(ptr2)
LIND(ptr1)↔LINK(ptr2),即交换这两个指针的链接地址。区别就在于合并两个链表时,两个指针指向不同的环形链表;而拆分环形链表时,两个指针指向同一个链表。 见下图:

讲了这么多,环形链表的合并拆分跟交换排序又有什么关系?我要说数组就是一个或多个环形链表组成,你信吗?我们举个例子:
(
3
7
6
9
8
1
4
5
2
)
(3\,7\,6\,9\,8\,1\,4\,5\,2)
(376981452),
R
1
R_1
R1的值为3,表示
R
1
R_1
R1存储的指针指向
R
3
R_3
R3,即
R
1
→
R
3
R_1\rightarrow R_3
R1→R3,依此推算
R
1
→
R
3
→
R
6
→
R
1
R_1\rightarrow R_3\rightarrow R_6\rightarrow R_1
R1→R3→R6→R1构成了一个环形链表(红色表示);同理
(
R
5
,
R
8
)
(R_5,R_8)
(R5,R8)(蓝色表示)和
(
R
2
,
R
7
,
R
4
,
R
9
)
(R_2,R_7,R_4,R_9)
(R2,R7,R4,R9)(黑色表示)分别构成了两个环形链表。所以这个排列可表示为:
(
(
3
6
1
)
(
2
7
4
9
)
(
5
8
)
)
((3\,6\,1)(2\,7\,4\,9)(5\,8))
((361)(2749)(58))共三个环形链表构成的数组排列(在《排列与反序》中,应用这个思想解决了由反序表生成对应的排列问题,同时提供了数组转链表、链表转数组的算法)。见下图:

我们在不同的环中交换一次数据,环数减少1;在同环中交换一次数据环数增加1。我们交换排序的目标是把它排列为 ( 1 2 3 4 5 6 7 8 9 ) (1\,2\,3\,4\,5\,6\,7\,8\,9) (123456789),只包含一个元素、指向自身的环。还是以上面的例子为例,有3个环,我们的排序目标是9个元素要形成9个环,那么最少的交换次序 9 − 3 = 6 9-3=6 9−3=6,即最少要交换(同环交换)6次才能完成排序。理解了排列是由不同的环构成的,也就明白为什么第一类斯特林数满足 ∑ k [ n k ] = n ! \sum_k{n\brack k}=n! ∑k[kn]=n!性质—— n n n个元素形成不同环的数量总和等于 n n n的排列数,其有一一的对应关系。(参考《排列与反序》,里面有详细的数组转链表、链表转数组的讲解,可加深对这方面知识的理解。)
1、 最短的排序程序,没有之一
最基本的算法就是与相邻的数据两两比较、交换,这是冒泡排序的基本思路。与直接插入排序类似,两两的比较交换数据是非常低效的,因其数据需要的平均净移动量为 1 3 N {1\over3}N 31N,见《[Alg]排序算法之插入排序》。当然,万事万物都有其两面性,牺牲了性能,得到的好处是最简洁的代码,下面是最短的排序程序描述:
int i;
begin: i = N; //设置从头开始遍历
for( ; i > 0; --i){
if(R[i] < R[i-1]){
R[i] <-> R[i-1];//交换数据
goto begin;
}
}
这个算法原理非常之简单,从头开始遍历数据记录并与其相邻的记录比较,发现一个反序后即对进行数据交换,然后从头开始重新遍历,直到消除所有的反序,至此if分支不成立,不执行条件语句中的goto跳转,然后从循环退出。这是一个比冒泡排序还苯的算法,大约平均执行 1 4 N 2 {1\over4}N^2 41N2(见拆分数,反序的均值)次循环(一个反序对应一次循环)时间复杂度 O ( N 3 ) O(N^3) O(N3)。这个算法除了能增加一点我们的见识,貌似没有其它作用了(在快排中,选头、中、尾三个数据时用到了,这个排序很简洁,比写一大堆的if语句要好)。
回过头来接着聊冒泡排序,冒泡排序相邻的数据两两比较,成反序则交换。一趟遍历后,大数据上浮;我们可以在下一趟遍历时,让小数据下沉。这点改进稍稍能提高点性能,但与直接插入比起来,虽然都很垃圾,但冒泡排序更垃圾,其代码量相当于直接插入排序的两倍。好像除了冒泡排序妇孺皆知、广为传诵的名字,也没有其它让人欣赏的东西了。
2、Batcher’s 并行排序算法
我们从《[Alg]排序算法之插入排序》的分析中得知,要提高算法的效率,就要增大比较和交换的跨度。下面提到的算法和shell排序算法类似,增加比较交换跨度。虽然其原理相似,但其具体的实现手段却相去甚远。这种增大跨度的方式很新奇,亦非常的错综复杂,不容易理解。我们先列出算法的实现,再配以例子逐条解释。可能也只有这个方法能把算法过程说的明白一点,我实在想不出其它更好的方式了。
算法M:(Batcher’s 并行算法)
设记录 R 0 , R 1 , … , R N − 1 R_0,R_1,\ldots, R_{N-1} R0,R1,…,RN−1,其关键词为: K 0 , K 1 , … , K N − 1 K_0,K_1,\ldots,K_{N-1} K0,K1,…,KN−1,假设 N − 1 > 2 N-1>2 N−1>2。
- M1.[初始化 p p p.] 置 p ← 2 t − 1 p\gets2^{t-1} p←2t−1,在这里 t = ⌈ lg N ⌉ t=\lceil\lg N\rceil t=⌈lgN⌉,即 t t t取满足条件 2 t ≥ N 2^t\geq N 2t≥N的最小值。
- M2.[初始化 q , r , d q,r,d q,r,d.] 置 q ← 2 t − 1 q\gets2^{t-1} q←2t−1, r ← 0 r\gets0 r←0, d ← p d\gets p d←p。
- M3.[循环 i i i.] For 0 ≤ i < N − d 0\leq i<N-d 0≤i<N−d and i ∧ p = r i\wedge p=r i∧p=r,执行M4,然后转到M5。
- M4.[比较交换] 如果 K i + 1 > K i + d + 1 K_{i+1}>K_{i+d+1} Ki+1>Ki+d+1,交换 R i + 1 ↔ R i + d + 1 R_{i+1}\leftrightarrow R_{i+d+1} Ri+1↔Ri+d+1。
- M5.[循环 q q q.] 如果 q ≠ p q\neq p q=p,置 d ← q − p d\gets q-p d←q−p, q ← q / 2 q\gets q/2 q←q/2, r ← p r\gets p r←p,并返回M3。
- M6.[循环 p p p.] 置 p ← p / 2 p\gets p/2 p←p/2,如果 p > 0 p>0 p>0,返回M2;否则结束。
先设置一个例子,设记录 R R R其值为 ( R 0 , R 1 , R 2 , R 3 , R 4 , R 5 , R 6 , R 7 ) (R_0,R_1,R_2,R_3,R_4,R_5,R_6,R_7) (R0,R1,R2,R3,R4,R5,R6,R7)。
- 1、 p p p在算法中的作用是对数据分段,例子 R R R中共有8个数据项, t = 3 , p = 2 t − 1 = 4 t=3, p=2^{t-1}=4 t=3,p=2t−1=4。M1和M6构成了算法的整个外循环,执行 p = 4 , p = 2 , p = 1 p=4,p=2,p=1 p=4,p=2,p=1不同分段,然后结束。
- 2、 r r r控制数据段的选取,在M3中条件 0 ≤ i < N − d 0\leq i<N-d 0≤i<N−d是防止数据越界,和段选择条件 i ∧ p = r i\wedge p=r i∧p=r(逻辑位and操作,相当于c语言的 i & p = = r i\&p==r i&p==r)一起,控制着整个比较交换过程数据段选择。
- 3、
q
q
q和
d
d
d一起控制着比较、交换过程的跨度。
以上述例子,运行一遍会更清晰:
第一趟: p = 4 p=4 p=4
| p p p | q q q | r r r | d d d |
|---|---|---|---|
| 4 | 4 | 0 | 4 |
M3中条件
0
≤
i
<
4
0\leq i<4
0≤i<4选出大范围保证不越界,然后条件进一步精确到数据段
i
∧
p
=
0
i\wedge p=0
i∧p=0,
p
=
4
(
0100
)
2
p=4(0100)_2
p=4(0100)2选取的为
0
(
0000
)
2
∧
4
(
0100
)
2
=
0
0(0000)_2\wedge4(0100)_2=0
0(0000)2∧4(0100)2=0
1
(
0001
)
2
∧
4
(
0100
)
2
=
0
1(0001)_2\wedge4(0100)_2=0
1(0001)2∧4(0100)2=0
2
(
0010
)
2
∧
4
(
0100
)
2
=
0
2(0010)_2\wedge4(0100)_2=0
2(0010)2∧4(0100)2=0
3
(
0011
)
2
∧
4
(
0100
)
2
=
0
3(0011)_2\wedge4(0100)_2=0
3(0011)2∧4(0100)2=0
即为选中 ( R 0 , R 1 , R 2 , R 3 ) (R_0,R_1,R_2,R_3) (R0,R1,R2,R3)(在第一步中两条件重合,后续会看的更清楚)在跨度 d = 4 d=4 d=4的情况下比较交换,即: ( R 0 : R 4 ) , ( R 1 : R 5 ) , ( R 2 : R 6 ) , ( R 3 : R 7 ) (R_0:R_4),(R_1:R_5),(R_2:R_6),(R_3:R_7) (R0:R4),(R1:R5),(R2:R6),(R3:R7)。
p = 2 p=2 p=2时:
| p p p | q q q | r r r | d d d |
|---|---|---|---|
| 2 | 4 | 0 | 2 |
| 2 | 2 | 2 | 2 |
首先选出大范围 0 ≤ i < 6 ( N − d ) 0\leq i<6(N-d) 0≤i<6(N−d)以双竖线标出,再在这个大范围内选出长度2的数据段 ( R 0 , R 1 ‾ , R 2 , R 3 , R 4 , R 5 ‾ ∣ ∣ R 6 , R 7 ) (\overline{R_0,R_1},R_2,R_3,\overline{R_4,R_5}||R_6,R_7) (R0,R1,R2,R3,R4,R5∣∣R6,R7),上划线的项与随后的不带上划线的项比较交换。
在M5处,改变跨度 d ← q − p d\gets q-p d←q−p, q ← q / 2 q\gets q/2 q←q/2;反转数据段选择 r ← p r\gets p r←p,即改为上次没选中的 ( R 0 , R 1 , R 2 , R 3 ‾ , R 4 , R 5 ‾ ∣ ∣ R 6 , R 7 ) (R_0,R_1,\overline{R_2,R_3},\underline{R_4,R_5}||R_6,R_7) (R0,R1,R2,R3,R4,R5∣∣R6,R7)以跨度 d = 2 d=2 d=2与后面的项比较交换,即上划线为选中数据与下划线部分比较交换。
p = 1 p=1 p=1时
| p p p | q q q | r r r | d d d | 比较次序(上划线与下划线比较交换) |
|---|---|---|---|---|
| 1 | 4 | 0 | 1 | ( R 0 ‾ , R 1 ‾ , R 2 ‾ , R 3 ‾ , R 4 ‾ , R 5 ‾ , R 6 ‾ ∥ R 7 ‾ ) (\overline{R_0},\underline{R_1},\overline{R_2},\underline{R_3},\overline{R_4},\underline{R_5},\overline{R_6}\|\underline{R_7}) (R0,R1,R2,R3,R4,R5,R6∥R7) |
| 1 | 2 | 1 | 3 | ( R 0 , R 1 ‾ , R 2 , R 3 ‾ , R 4 ‾ , R 5 , R 6 ‾ ∥ R 7 ) (R_0,\overline{R_1},{R_2},\overline{R_3},\underline{R_4},{R_5},\underline{R_6}\|{R_7}) (R0,R1,R2,R3,R4,R5,R6∥R7) |
| 1 | 1 | 1 | 1 | ( R 0 , R 1 ‾ , R 2 ‾ , R 3 ‾ , R 4 ‾ , R 5 ‾ , R 6 ‾ ∥ R 7 ) ({R_0},\overline{R_1},\underline{R_2},\overline{R_3},\underline{R_4},\overline{R_5},\underline{R_6}\|{R_7}) (R0,R1,R2,R3,R4,R5,R6∥R7) |
从上面的详解中可以看出,这个算法是一个非常机械的过程,只要确定了 N N N,后续所有的比较、交换、跨度等等要素全部确定,它不依赖于任何前序的执行结果,这点与后面要介绍的快速排序完全不同,快排依赖于上次的划分。万事万物都有其两面性,这个特点使得该算法笨重,但是它这个特征使得它也特别适合于并行计算,在M3步骤中,满足条件多个执行过程数据的对比交换过程没有交叉重叠,完全可同步执行。整个执行过程可在 O ( 1 2 ⌈ lg N ⌉ ) ( ⌈ lg N ⌉ + 1 ) O({1\over2}{\lceil\lg N\rceil})(\lceil\lg N\rceil+1) O(21⌈lgN⌉)(⌈lgN⌉+1)完成,这是串行算法所不能达到的。
3、快速排序
快排应该是应用范围最广、广为人知的一种算法,它是典型的分治思想的应用。其基本原理是选出一项,如 R 1 R_1 R1为基准,与其它项逐个比较,比它大的项放置在右侧,比它小的项放置左侧,这样一个大问题就划分成了两个同样的子问题。依此递归下去,便得到结果。
算法Q:(快速排序)
设记录 ( 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)。 同时需要辅助stack,容量为 ⌊ lg N ⌋ \lfloor\lg N\rfloor ⌊lgN⌋。我们对算法实现在了如下处理:
-
1、 在头尾插入 K 0 = − ∞ K_0=-\infty K0=−∞和 K N + 1 = ∞ K_{N+1}=\infty KN+1=∞,以免除麻烦的边界条件检查;
-
2、 为避免子划分很小时,快排反复划分和stack操作的低效问题,设置了一个阀值, M = 9 M=9 M=9,当子划分小于等于该值时,转为直接插入排序。( M = 9 M=9 M=9为最优值,TAOCP 5.2.2 P120给出了详细的数学分析。)
-
3、为了保持内循环的高效和尽量的划分均等,相等的键值也进行交换。
-
Q1.[初始化] 如果 N ≤ M N\leq M N≤M,转到Q9。否则,stack置空;并置 l ← 1 l\gets 1 l←1, r ← N r\gets N r←N。
-
Q2.[准备递归步骤] 置 i ← l i\gets l i←l, j ← r + 1 j\gets r+1 j←r+1;并设 K ← K l K\gets K_l K←Kl。
-
Q3.[比较 K i : K K_i:K Ki:K] i ← i + 1 i\gets i+1 i←i+1,如果 K i < K K_i<K Ki<K,重复。
-
Q4.[比较 K : K j K:K_j K:Kj] j ← j − 1 j\gets j-1 j←j−1,如果 K < K j K<K_j K<Kj,重复。
-
Q5.[比较 i : j i:j i:j] 如果 j ≤ i j\leq i j≤i,交换 R l ↔ R j R_l\leftrightarrow R_j Rl↔Rj并跳转至Q7。
-
Q6.[交换] 交换 R i ↔ R j R_i\leftrightarrow R_j Ri↔Rj,并返回Q3。
-
Q7.[压栈] 如果 r − j ≥ j − l > M r-j\geq j-l>M r−j≥j−l>M, ( j + 1 , r ) (j+1,r) (j+1,r)压栈,置 r ← j − 1 r\gets j-1 r←j−1并返回Q2;如果 j − l > r − j > M j-l>r-j>M j−l>r−j>M, ( l , j − 1 ) (l,j-1) (l,j−1)压栈,置 l ← j + 1 l\gets j+1 l←j+1并返回Q2;如果 r − j > M > j − l r-j>M>j-l r−j>M>j−l,置 l ← j + 1 l\gets j+1 l←j+1并返回Q2;如果 j − l > M ≥ r − j j-l>M\geq r-j j−l>M≥r−j,置 r ← j − 1 r\gets j-1 r←j−1并返回Q2.
-
Q8.[弹栈] 如果stack非空, ( l ′ , r ′ ) (l',r') (l′,r′)出栈,并置 l ← l ′ , r ← r ′ l\gets l',r\gets r' l←l′,r←r′并返回Q2。
-
Q9.[直接插入排序]参见《[Alg]排序算法之插入排序》
快排退化问题
不同于其它排序算法,快排喜欢乱序的输入,对接近排好序(秩序越好)是非常不友好的,最坏的情况出现在完全排好序的排列,快排只能每次分离出一个数据,且时间复杂度退化至 O ( N 2 ) O(N^2) O(N2)。为避免这种窘境,可以采取以以下措施来改善。
- 1、采用随机化的方法选择基准(随机选择算法中提到的 R l R_l Rl),这样就避免了从头开始选择最大值或最小值,致使划分出一个元素的尴尬。
- 2、从头、尾及中间分别选取三个值,取中间值,这个方法相对简单,且避免了生成随机数的开销。
我们现在考虑一种更极端的情况,假设所有的排序关键词 K K K都相等,会出现什么情况? i i i从头到尾与 j j j逐个交换,在中点结束,形成完美的二等分;假设我们把 Q 3 Q3 Q3、 Q 4 Q4 Q4的 < < <改为 ≤ \leq ≤呢? i i i j j j不交换任何值,只是在 Q 5 Q5 Q5步交换 R l ↔ R j R_l\leftrightarrow R_j Rl↔Rj,即 R 0 R_0 R0与 R 1 R_1 R1,随后 R 0 R_0 R0与 R 2 R_2 R2交换,依次的 R 0 R_0 R0与 R 3 , R 4 , … , R N R_3,R_4,\ldots,R_N R3,R4,…,RN交换,哇——进入灾难模式。
消除边界条件检查
在算法前置条件中提到,在头尾分别插入 K 0 = − ∞ K_0=-\infty K0=−∞和 K N + 1 = ∞ K_{N+1}=\infty KN+1=∞,它们的作用分别是 K 0 K_0 K0消除最后直接插入排序算法的边界条件检查,后面的 K N + 1 K_{N+1} KN+1的作用是当选择 K l K_l Kl为最大值时,防止寻找 K i > K l K_i>K_l Ki>Kl值时发生越界。
我们在讨论插入排序时,已经采用了第二种消除边界检查的方法,故可取消 K 0 = − ∞ K_0=-\infty K0=−∞;对于 K N + 1 = ∞ K_{N+1}=\infty KN+1=∞,我们采用如下方法,针对上面提到的快排退化的改进措施,我们稍加改造,可达到一箭双雕的目的,即可预防快排退化,又可消除右侧边界条件检查,具体如下:
从头、尾、中分别取三个值,中间值交换到最左侧( R l R_l Rl),最大值交换到最右侧( R r R_r Rr)。
中位数问题
快速排序算法的这种大事化小、分而治之(divide and conquer)的思想有着广泛的应用。下面这个问题的解决方案就是一个典型的应用范例:给出一个乱序的排列,寻找排序后位于中间位置的值。通常第一个想到的就是排序,然后就容易确定中间值,为了确定一个值而排序是很浪费的,尤其是数据很多的时候。如果我们不采用排序的手段,借鉴快排的处理思路,选取第一个 K l K_l Kl为基准,进行交换,运行后的结果是,小于 K l K_l Kl的项位于左侧,大于 K l K_l Kl的位于右侧,设 K l K_l Kl的位置为 P O S ( K l ) POS(K_l) POS(Kl)。如果 P O S ( K l ) > m i d POS(K_l)>mid POS(Kl)>mid(设 m i d mid mid为中间位置),表明中位数位于左侧数据部分,则继续重复该步骤在左侧寻找;如果 P O S ( K l ) < m i d POS(K_l)<mid POS(Kl)<mid,则在右侧数据中寻找;直到找到 P O S ( K l ) = m i d POS(K_l)=mid POS(Kl)=mid的值为止。也就是运行快排前几步确定中点的值,因舍弃了不需要的另一部分,故不需要stack存储。
我们以此为例说明了寻找中位数的方法,当然,这个方法可以寻找任意一个位置的数,应该称为K位数问题。
荷兰国旗问题
红色、白色、蓝色的标识随机的排成一列,如何通过两两交换使得红色表示位于顶部,蓝色的位于底部(见荷兰国旗图)。我们要把快排的两分改进为三分天下的问题(是不是叫三国问题或隆中对问题更符合国情)。

算法D:(三分法快速排序——Tripartitioning Quicksort)

b
b
b相当于快排的
i
i
i,向右搜索时,如果
=
k
=k
=k,则与
a
a
a交换,直到
K
b
>
K
K_b>K
Kb>K;
c
c
c相当于快排的
j
j
j向左搜索,遇到
=
K
=K
=K,则与
d
d
d交换,直到
K
c
<
K
K_c<K
Kc<K后
K
b
↔
K
c
K_b\leftrightarrow K_c
Kb↔Kc交换。依此处理,直到耗尽图中
?
?
?
???
???部分。然后做整理工作,把
=
K
=K
=K的部分交换到中央位置。
- D1.[初始化] 置 a ← b ← l a\gets b\gets l a←b←l, c ← d ← r c\gets d\gets r c←d←r。
- D2.[增加 b b b,直到 K b > K K_b>K Kb>K] 如果 b ≤ c b\leq c b≤c and K b < K K_b<K Kb<K,b加1,重复该步; 如果 b ≤ c b\leq c b≤c and K b = K K_b=K Kb=K,交换 R a ↔ R b R_a\leftrightarrow R_b Ra↔Rb,a、b、都加1,重复该步。
- D3.[减小 c c c,直到 K c < K K_c<K Kc<K] 如果 b ≤ c b\leq c b≤c and K c > K K_c>K Kc>K,c减1,重复该步; 如果 b ≤ c b\leq c b≤c and K c = K K_c=K Kc=K,交换 R c ↔ R d R_c\leftrightarrow R_d Rc↔Rd,c、d都减1,重复该步。
- D4.[交换] 如果 b < c b<c b<c,交换 R b ↔ R c R_b\leftrightarrow R_c Rb↔Rc,b加1,c减1,返回D2。
- D5.[整理] For 0 ≤ k < m i n ( a − l , b − a ) 0\leq k < min(a-l,b-a) 0≤k<min(a−l,b−a),交换 R l + k ↔ R c − k R_{l+k}\leftrightarrow R_{c-k} Rl+k↔Rc−k;For 0 ≤ k < m i n ( d − c , r − d ) 0\leq k<min(d-c,r-d) 0≤k<min(d−c,r−d),交换 R b + k ↔ R r − k R_{b+k}\leftrightarrow R_{r-k} Rb+k↔Rr−k。最后设 i ← l + b − a i\gets l+b-a i←l+b−a, j ← r − d + c j\gets r-d+c j←r−d+c(把相等值移至中间,具体实现数组旋转请参考《[Alg]排序算法之插入排序》旋转数组部分。
上面的算法D2、D3设置了条件检测 b ≤ c b\leq c b≤c。如果进入D2之前确保 a < b a<b a<b和 c < d c<d c<d,上述条件检测完全可以取消。
当键值大量重复或在接近排序尾声时键值接近,这时要花大量精力的检查键值以确定后续操作,这严重降低了算法的效率。可以转为三分快排,因为 = K =K =K的部分为最终确定的位置,后续排序不须再移动,这极大的加快了排序速度。
4、基数交换排序
基数交换排序与快速排序没有什么本质不同,只是把比较 K K K改为了按位比较0和1。下面列出算法:
算法R:(基数排序)
设关键词
K
K
K的最高有效位为
m
m
m位,则需要容量
m
−
1
m-1
m−1的辅助stack。
b
b
b对位计数。
- R1.[初始化] 清空stack,并置 l ← 1 l\gets 1 l←1, r ← N r\gets N r←N, b ← 1 b\gets 1 b←1。
- R2.[开始排序] 如果 l = r l=r l=r转到R10。否则置 i ← l i\gets l i←l, j ← r j\gets r j←r。
- R3.[检查 K i K_i Ki的 b b b位是否为1] 如果为1,转到R6。
- R4.[增加 i i i] i ← i + 1 i\gets i+1 i←i+1。如果 i ≤ j i\leq j i≤j,转至R3;否则转至R8.
- R5.[检查 K j K_j Kj的 b b b位是否为0] 如果为0,转至R7.
- R6.[减小 j j j] j ← j − 1 j\gets j-1 j←j−1。如果 i ≤ j i\leq j i≤j,转至R5;否则转至R8.
- R7.[交换] 交换 R i ↔ R j + 1 R_i\leftrightarrow R_{j+1} Ri↔Rj+1,然后跳转至R4.
- R8.[检查特殊情形]如果 b > m b>m b>m,说明对所有的位都处理完,转至R10。否则:如果 j < l j<l j<l或 j = r j=r j=r(所有位为1或0),转至R2;否则,如果 j = l j=l j=l(只有一个0 bit), l ← l + 1 l\gets l+1 l←l+1 并转至R2。
- R9.[压栈] ( r , b ) (r,b) (r,b)压栈,并设 r ← j r\gets j r←j,返回R2。
- R10.[弹栈] 如果stack空,结束;否则 l ← r + 1 l\gets r+1 l←r+1,弹栈 ( r ′ , b ′ ) (r',b') (r′,b′)并置 r ← r ′ r\gets r' r←r′, b ← b ′ b\gets b' b←b′,并返回R2.
特别注意b处理,b应该取最大值的最高有效位,保证键值中最少有一个b位为1。如果取值比所有键值的最高有效位还高,那第一趟遍历,所有都为0, i i i从左向右搜索时遇不到1,这会导致越界;程序会报段错误。只要第一趟发生一次交换,就能保证边界。后续没有必要边界检查。
基数交换排序也有上面提到的键值接近时的退化问题,例如好多键值 k k k位以前都相等,必须检查 k + 1 k+1 k+1甚至 k + 2 k+2 k+2, k + 3 k+3 k+3甚至更多位才能确定后续执行的指令。我们也可以采用三分快排的思想来解决这个问题,就是把记录三分为 ( l , i − 1 , k ) (l,i-1,k) (l,i−1,k), ( i , j , k + 1 ) (i,j,k+1) (i,j,k+1), ( j + 1 , r , k ) (j+1,r,k) (j+1,r,k)三个部分,采用三分快排的思路来处理。
好漫长的一章,我觉得我把交换排序的方方面面都介绍清除了,但是要完全消化吸收,里面还是有好多东西要好好咀嚼的。
本文详细介绍了交换排序,包括其本质、最短的排序程序、Batcher's并行排序算法以及快速排序的优化。通过对交换排序的深入解析,阐述了不同排序算法的原理和应用场景,旨在帮助读者理解排序算法的内在逻辑和性能特点。

2685

被折叠的 条评论
为什么被折叠?



