并行与分布式系统中的并行双调归并网络:双调排序器(Bitonic Sorter)算法
1. 问题描述
我们面临一个经典问题:如何高效地对一组元素进行排序。在并行计算环境中,我们希望充分利用多个处理器(或核心)来加速排序过程。一种非常著名的并行排序网络是双调排序器(Bitonic Sorter)。它是由Ken Batcher在1968年提出的一种基于比较和交换的排序网络,特别适合在SIMD(单指令多数据)架构或专用硬件(如FPGA)上实现。其核心思想是通过构建“双调序列”并递归地进行合并操作,最终得到一个全局有序的序列。
问题具体描述如下:
- 输入:一个长度为 \(n\)(假设 \(n\) 是2的幂,例如 \(n = 2^k\))的无序数组。
- 输出:一个按升序(或降序)排列的有序数组。
- 目标:设计一个并行算法,利用 \(O(n \log^2 n)\) 次比较操作,但通过并行化,可以在 \(O(\log^2 n)\) 的时间步(time steps)内完成排序。每个时间步内,可以并行执行多个独立的比较-交换操作。
2. 基本概念与定义
在深入算法之前,我们需要理解几个关键概念:
双调序列
一个序列 \(a_0, a_1, ..., a_{n-1}\) 被称为双调序列,如果存在一个索引 \(i\)(\(0 \le i < n\)),使得:
- \(a_0 \le a_1 \le ... \le a_i\)(非递减)
- \(a_i \ge a_{i+1} \ge ... \ge a_{n-1}\)(非递增)
或者整个序列是先递减后递增(即循环移位后满足上述条件)。
简单来说,双调序列是一个先上升后下降(或先下降后上升)的序列,形成一个“山峰”形状。
例如:
- [3, 5, 8, 10, 7, 4, 2] 是双调序列(先上升到10,再下降)。
- [1, 2, 3, 4] 也是双调序列(可以看作先上升到4,然后“下降”部分长度为0)。
比较器
一个比较器是一个简单的硬件单元或操作,它接受两个输入 \((a, b)\),并输出 \((\min(a, b), \max(a, b))\)。在双调排序网络中,比较器有两种模式:
- 上升比较器:输出 \((\min(a, b), \max(a, b))\),即较小的值放在上面(或左边)。
- 下降比较器:输出 \((\max(a, b), \min(a, b))\),即较大的值放在上面(或左边)。
双调合并定理
关键定理:给定一个双调序列 \(S\) 和一个比较器网络(称为双调合并器),我们可以将该序列分成两个等长的子序列 \(S_1\) 和 \(S_2\),其中 \(S_1\) 由每个比较器的较小输出组成(上半部分),\(S_2\) 由每个比较器的较大输出组成(下半部分)。然后,定理指出:
- \(S_1\) 和 \(S_2\) 各自仍然是双调序列。
- 并且,\(S_1\) 中的每一个元素都小于等于 \(S_2\) 中的每一个元素。
这意味着,我们只需要递归地对 \(S_1\) 和 \(S_2\) 进行排序(同样是双调合并),最终就可以得到完全有序的序列。
3. 算法步骤详解
双调排序器的构建分为两个主要阶段:构建双调序列和合并双调序列。整个算法是递归的。
步骤1:构建双调序列
我们从一个完全无序的序列开始,需要首先将其构造成一个双调序列。这里采用分治法:
- 将输入序列分成两半。
- 递归地对左半部分进行升序排序(构建一个向上的双调序列),对右半部分进行降序排序(构建一个向下的双调序列)。
- 将这两部分连接起来,就得到一个完整的双调序列(左半部分升序,右半部分降序)。
递归基:当序列长度为1时,它本身就是一个双调序列(长度为1,自然满足条件)。
步骤2:双调合并(Bitonic Merge)
这是算法的核心操作。给定一个长度为 \(n\) 的双调序列,我们通过一个比较器网络(称为BM(n))将其合并成一个有序序列。
- 比较-交换操作:
- 将序列对半分成两个子序列:上半部分和下半部分。
- 对于 \(i = 0\) 到 \(n/2 - 1\),将位置 \(i\) 的元素与位置 \(i + n/2\) 的元素进行比较。
- 如果当前合并的目标是升序,则使用上升比较器(即输出较小值在上,较大值在下)。
- 比较后,上半部分序列(较小值部分)和下半部分序列(较大值部分)各自形成一个双调序列(根据双调合并定理)。
- 递归合并:
- 递归地对上半部分(较小值部分)进行双调合并(升序)。
- 递归地对下半部分(较大值部分)进行双调合并(升序)。
- 递归基:当序列长度为1时,它已经有序。
注意:如果目标排序是降序,则比较器模式相反(使用下降比较器)。
步骤3:完整的双调排序
完整的排序算法 BitonicSort(A, n, ascending) 如下:
- 输入:数组
A(长度 \(n = 2^k\)),布尔值ascending表示排序方向。 - 过程:
- 如果
n == 1,直接返回。 - 否则:
a. 将数组对半分成left和right。
b. 递归调用BitonicSort(left, n/2, true)对左半部分进行升序双调排序。
c. 递归调用BitonicSort(right, n/2, false)对右半部分进行降序双调排序。
d. 现在,整个数组A是一个双调序列(左升右降)。
e. 调用BitonicMerge(A, n, ascending)将整个双调序列合并成指定方向的有序序列。
- 如果
其中 BitonicMerge(A, n, ascending) 的实现:
- 如果
n == 1,返回。 - 执行比较-交换步骤:对于
i = 0到n/2 - 1,比较A[i]和A[i + n/2],如果ascending为真,则将较小值放到A[i],较大值放到A[i + n/2];否则交换规则相反。 - 递归调用
BitonicMerge(A[0:n/2], n/2, ascending)处理上半部分。 - 递归调用
BitonicMerge(A[n/2:n], n/2, ascending)处理下半部分。
4. 并行化分析
双调排序器的天然结构非常适合并行化:
- 比较-交换操作的并行性:在每一层的合并操作中,所有 \(n/2\) 个比较器对
(A[i], A[i+n/2])都是完全独立的,可以同时执行。这提供了很高的并行度。 - 递归的并行性:在构建双调序列和合并过程中,左右两半的递归调用也是相互独立的,可以并行执行。
时间复杂度(并行版本):
- 设我们有 \(p\) 个处理器,且 \(n\) 是2的幂。
- 双调排序的深度(即时间步数)是 \(\log^2 n\)(更精确地是 \(\frac{(\log n)(\log n + 1)}{2}\))。
- 构建双调序列的递归深度为 \(\log n\)。
- 在每个深度级别上,合并操作的深度也是 \(\log n\)(因为合并过程本身也是一个深度为 \(\log n\) 的递归树)。
- 实际上,总深度 = \(1 + 2 + 3 + ... + \log n = \frac{(\log n)(\log n + 1)}{2} \in O(\log^2 n)\)。
- 在每个时间步内,最多有 \(n/2\) 个比较器可以并行工作。如果我们有足够多的处理器(例如 \(p = n\)),则每个时间步都是常数时间。
- 因此,并行时间复杂度为 \(O(\log^2 n)\),这比最好的比较排序下界 \(O(\log n)\) 稍差,但对于基于比较器的排序网络来说,这是非常高效的。
空间复杂度:算法是就地排序的,只需要 \(O(1)\) 的额外空间(用于交换的临时变量)。
5. 示例演示
让我们用一个简单例子:对数组 [3, 7, 4, 8, 6, 2, 1, 5](n=8)进行升序排序。
步骤(简化描述,展现关键合并阶段):
-
递归构建双调序列:
- 将数组分成两半:左=[3,7,4,8],右=[6,2,1,5]。
- 对左半递归升序排序(最终得到双调序列 [3,4,7,8])。
- 对右半递归降序排序(最终得到双调序列 [6,5,2,1])。
- 合并后整个序列为
[3,4,7,8,6,5,2,1],这是一个双调序列(先升后降)。
-
第一次合并(对整个序列进行BitonicMerge):
- 比较-交换对:(3,6), (4,5), (7,2), (8,1)。使用上升比较器(因为目标升序):
- (3,6) → (3,6)
- (4,5) → (4,5)
- (7,2) → (2,7)
- (8,1) → (1,8)
- 得到序列:
[3,4,2,1,6,5,7,8]。 - 递归合并上半部分 [3,4,2,1] 和下半部分 [6,5,7,8]。
- 比较-交换对:(3,6), (4,5), (7,2), (8,1)。使用上升比较器(因为目标升序):
-
递归合并上半部分 [3,4,2,1]:
- 它是一个双调序列(3,4升,2,1降)。
- 比较-交换对:(3,2), (4,1) → (2,3), (1,4) → 得到
[2,1,3,4]。 - 递归合并 [2,1] 和 [3,4]。
- 合并 [2,1]:(2,1) → (1,2) → 得到
[1,2]。 - 合并 [3,4]:(3,4) → (3,4)。
- 合并 [2,1]:(2,1) → (1,2) → 得到
- 上半部分最终为
[1,2,3,4]。
-
递归合并下半部分 [6,5,7,8]:
- 类似过程,最终得到
[5,6,7,8]。
- 类似过程,最终得到
-
最终序列:
[1,2,3,4,5,6,7,8],排序完成。
整个过程中,每个阶段的比较-交换对都可以并行执行。
6. 算法特性与应用
- 确定性:双调排序器是一个固定的比较网络,与输入数据无关。
- 稳定性:传统双调排序器不是稳定排序(即相等元素的相对顺序可能改变)。
- 适用场景:
- SIMD/GPU计算:由于规则的数据访问模式和高度并行性,双调排序常用于GPU编程(例如CUDA和OpenCL中的排序实现)。
- 硬件排序网络:在FPGA或ASIC中实现高速排序。
- 并行算法教学:作为并行分治算法的经典案例。
- 局限性:
- 要求输入长度 \(n\) 是2的幂(可以通过填充满足)。
- 比较次数 \(O(n \log^2 n)\) 比最优的 \(O(n \log n)\) 多一个对数因子,但在并行环境下,时间步的减少更为关键。
总结,双调排序器展示了如何通过巧妙的递归结构和并行比较-交换操作,在 \(O(\log^2 n)\) 并行时间内完成排序,是并行算法中一个优雅而强大的典范。