双调排序(Bitonic Sort)
前言
双调排序(
于
时间复杂度为
虽然其时间复杂度乍一看不如快排等著名排序快,但当双调排序应用于并行处理时,其时间复杂度就变成了
就算法竞赛而言,双调排序用处不大。
但双调排序真的是个很优美的算法,君且听我娓娓道来。
前置知识和约定
关于本博客的图片
都是我自己用“画图”画出来的。。。
画质粗糙还请体谅一下。。。
关于本博客的证明
证明都是我自己想的,如果您发现我的逻辑上有
比较器
本篇博客中的图片均用“
如:
表示将
再如:
是一个可以对三个元素排序的冒泡排序。
可以看出,比较器是一个很简单的计算结构。
双调序列(\text{Bitonic Sequence} )
若一个序列满足以下两个条件之一:
-
-
序列能够通过循环位移满足 1 。
循环位移就是每次将序列最后一个元素放到最前面去,这样执行若干次。
需要注意的是,单调不降和单调不升的序列都属于双调序列。
\text{Batcher} 定理
定理内容
将一个长度为
即:
为了方便叙述,我将这个操作称为“双调分裂(
定理证明
因为循环位移并不会改变这
而对于第二种情况的双调序列,只需要先循环位移,使之变为第一种,完成双调分裂后,再将
-
先证明“
MAX 中所有元素均大于等于MIN 中任意元素”,该命题等价于“原序列中前
n 大的数都进入了MAX 序列中” 。设序列中最大的元素为
a_k ,即a_1\leqslant\cdots\leqslant a_{k-1}\leqslant a_k\geqslant a_{k+1}\geqslant\cdots\geqslant a_{2n}\ ,\ k\in[1,n] 。对于
\forall\ i\in[1,n] ,考虑a_i 与a_{n+i} 的比较。-
若
n+i\leqslant k ,则a_{n+i} 必然大于等于a_i ,故a_{n+i} 进入MAX 序列。考虑到a_{n+i}\geqslant a_{n+i-1}\geqslant\cdots\geqslant a_1 ,可知a_{n+i} 至少大于等于n 个数,故a_{n+i} 是原序列前n 大的数中的一员。 -
若
k<n+i ,则a_{n+i} 与a_i 的大小关系不确定,需要进一步讨论。
注意到,此时我们有
a_1\leqslant\cdots\leqslant a_{i-1}\leqslant a_i ,a_{n+i}\geqslant a_{n+i+1}\geqslant\cdots\geqslant a_{2n} ,所以
a_i 至少大于等于i-1 个数,a_{n+i} 至少大于n-i 个数,且这i-1 个数不同于这n-i 个数。-
若
a_i\leqslant a_{n+i} ,则a_{n+i} 进入MAX 。而
a_{n+i} 此时必然大于等于原序列中(n-i)+(i-1)+1=n 个数,所以a_{n+i} 是原序列前n 大的数。 -
若
a_i\geqslant a_{n+i} ,则a_i 进入MAX 。一样的,
a_{i} 此时必然大于等于原序列中(n-i)+(i-1)+1=n 个数,所以a_{i} 是原序列前n 大的数。
由此我们证明了双调分裂后
MAX 序列中全部的数均为原序列中前n 大的数,而MAX 序列中总共有n 个数。故MAX 序列包含了原序列全部的前n 大的数。故命题得证。
-
-
再证明“
MAX,MIN 两个序列均仍为双调序列”。为了方便表示,我们记
b_1,\cdots,b_{2n} 为a_1,\cdots,a_{2n} 经过双调分裂后得到的序列。再考虑
a_{k+1},\cdots,a_{2n} 与a_{k-n+1},\cdots,a_n 这两段的比较。由于a_{k+1},\cdots,a_{2n} 单调不增,a_{k-n+1},\cdots,a_{n} 单调不降,故分裂后所得的b_{k+1},\cdots,b_{2n} 必然为单调不增或先单调不增后单调不降。若
b_{k+1},\cdots,b_{2n} 单调不增,则b_{n+1},\cdots,b_{2n} 为第一种情况的双调序列;若b_{k+1},\cdots,b_{2n} 先单调不增后单调不降,则必有b_{2n}=a_n ,则b_{2n}\leqslant a_{n+1} ,于是b_{n+1},\cdots,b_{2n} 为第二种情况的双调序列。而
b_1,\cdots,b_n 单调不降(对应b_{k+1},\cdots,b_{2n} 单调不增)或先单调不降后单调不增(对应b_{k+1},\cdots,b_{2n} 先单调不增后单调不降),故b_1,\cdots,b_n 为第一种情况的双调序列。也许它能有助于理解吧:
由于
a_1,\cdots,a_{k-1} 单调不降,a_{n+1},\cdots,a_{n+k-1} 单调不增,因为
b_{k-1}=max\{a_{k-1},a_{n+k-1}\}\geqslant b_{k}=a_{n+k} ,故b_1,\cdots,b_n 为第一种双调序列。而
b_{n+1},\cdots,b_{n+k-1} 为单调不增或先单调不增后单调不降。对于后者,因b_{n+1}=a_{n+1}\leqslant a_{n}=b_{2n} ,所以此时b_{n+1},\cdots,b_{2n} 为第二种双调序列。再来一张:
-
于是
双调排序的实现
双调合并(\text{Bitonit Merge} )
有了
只需将双调序列分裂,对得到的两个双调序列继续双调分裂。直到序列长度为
由于每次操作都会使双调序列中较大的那一半的数放到一起,较小那一半放到另一边。故不断分裂后总会得到一个单调序列。
即:
像这样将一个双调序列转化为单调序列的操作被称为“双调合并(
双调排序
有了双调合并,我们就可以构造双调排序的算法。
上述双调合并可将双调序列转化为单调不降序列。
我们把双调合并的
所以我们得到了将两个双调序列合并为一个双调序列的操作。
这样,我们就可以实现分治了。
具体而言:
-
对于数据总数
2^n 的序列,先将之分为(1,2)(3,4)\cdots(2^n-1,2^n) 这2^{n-1} 组,则每一组都是双调序列。 -
对这些双调序列进行双调合并,使之变成
2^{n-2} 个单调不增序列和2^{n-2} 个单调不降序列。且两种序列交错排列。 -
重新分组,
(1,2,3,4)(5,6,7,8)\cdots(2^n-3,2^n-2,2^n-1,2^n) ,易知每组仍然是双调序列。 -
\cdots\cdots -
只剩一组了
(1,2,\cdots,2^n) ,最后使用一次双调合并,将之化为单调不降序列,即得排好序后的序列。
像这样:
橙色、绿色框均能够将双调序列分裂。
根据上文论述,如图这样比较、交换,最终会得到一个单调不降的序列。
为了便于
(看我手工画图厉害吧)
图中所有的比较节点均为原先的
这样比较器的方向就全部一致了。
把这张图翻译成代码,即为:
void Bitonic_Sort(int a[],const int N) { // N 为数据总数,须为 2的幂次。
int t;
for(int i=1,i2=2;i2<=N;++i,i2<<=1) {
for(int j=1,j2=i2-1;j2>=1;++j,j2-=2) {
for(int k=j;k<=N;k+=i2) {
if(a[k]<a[k+j2]) t=a[k],a[k]=a[k+j2],a[k+j2]=t;
}
}
for(int j=(i2>>2);j>=1;j>>=1) {
for(int k=1;k<=j;++k) {
for(int l=k;l<=N;l+=(j<<1)) {
if(a[l]<a[l+j]) t=a[l],a[l]=a[l+j],a[l+j]=t;
}
}
}
}
}
双调排序的优缺点
优点
-
时间复杂度较低。
设其计算次数为
T(n) ,观察得递推式T(n)=2T(\dfrac{n}{2})+\dfrac{n}{2}\log_2n ,由主定理可知
T(n)=\Theta(n(\log_2n)^{1+1})=\Theta(n\log_2^2n) ,这个复杂度还是很优秀的。 -
便于并行处理,并行处理时,时间复杂度为
\Theta(\dfrac{\log_2n(\log_2n+1)}{2})=O(\log_2^2n) 。
缺点
-
时间复杂度还是有些高。
比起快排、归并还是很有差距的。所以竞赛中并不常用。
-
只能处理
n=2^k 个数据。对于不是
n\neq2^k 个数据的情况,我们将之补全为2^k 个数据。用正无穷或者负无穷补全都可以。最终排序相应取前
n 或后n 个数即可。不过,我们可以通过控制循环条件来减少计算的次数。具体而言,我们选择将正无穷填充到原数组的后面,而在比较时删去那些与正无穷作比较的比较器。
比如对
11 个数排序时,我们的双调排序会变成这样:因为原数组中的数与正无穷的比较肯定不会发生交换,故我们就算不作这样的比较也仍然能保证排序是对的。
这就看出我们把比较器方向改为一致的优势了。
而且这样做似乎能够提高运行效率。
当然上面代码就得改一改啦。
后记
-
其实写本篇博客是为了填坑,填我旷野大计算的坑。
于
\text{2020.01.14} 正式填坑完毕。 -
这种底层的排序还是很有用的,可以多做些了解。
-
从图中我们可以看出,不论输入数据是什么,程序所做的比较都是不变的,在任意一个步骤中,谁和谁比较都是预先确定好的。
这为其能够并行计算提供了基础。