Bitonic Sort 介绍

9 min

Bitonic Sort

双调排序(Bitonic Sort)是一种支持并行运算的排序方式,全程只依赖于简单的比较和交换,很适合在具有大量并行执行单元如 GPU, NPU 等架构上对大量元素进行排序。

排序的流程大致分为两步。第一步从上到下将数组拆分成双调序列,第二步从下到上逐级合并成一个单调序列。下面会分别介绍什么是双调序列以及拆分-合并的流程。

双调序列(Bitonic Sequence)

双调序列指的是由一系列的数字,先单调递增后单调递减(或者先单调递减再单调递增)组成的序列。例如下列三组数据都可以称作双调序列。

  1. 递增递减数据量相同 [1, 4, 5, 6, 3, 2] :↗↘ (1, 4, 5) 递增, (6, 3, 2) 递减 [8, 2, 1, 3]:↘↗↘↗ (8, 2) 递减,(1, 3) 递增
  2. 递增递减数据量不同 [1, 2, 4, 7, 6, 3] :↗↘ (1, 2, 4, 7) 递增, (6, 3) 递减
  3. 递增递减数据为 0 [1, 2, 3]:↗(1, 2, 3) 递增,()递减 [6]:↗(6) 递增,()递减 [或者 ↘() 递增,(6)递减]

通过上面的三组双调序列可以总结出双调序列的定义(以先递增后递减为例):一个序列 nums,存在一个 index ii ,对于所有 index 小于等于 ii 的所有元素nums[:i]是单调递增的,所有 index 大于等于 ii 的所有元素nums[i:]单调递减的,则称该序列为双调序列。

比较和交换(compare & swap)

比较和交换贯穿双调排序的流程,全程需要进行多次该操作。对于 nums[i]nums[j] 这两个需要比较的数,在递增序列和递减序列的处理逻辑定义为:

  1. nums[i]nums[j] 处于递增序列
    • nums[i] > nums[j] -> swap(nums[i], nums[j])
    • nums[i] <= nums[j] -> do nothing
  2. nums[i]nums[j] 处于递减序列
    • nums[i] < nums[j] -> swap(nums[i], nums[j])
    • nums[i] >= nums[j] -> do nothing

创建一系列双调序列

根据双调序列的定义,我们可以将任意一组数据拆分成一系列双调序列的组合。为了方便展示,先不考虑奇数的场景。

[3, 6, 5, 7, 4, 1, 8, 2] 作为例子,先将其对半拆分:

[3, 6, 5, 7][4, 1, 8, 2],再将每一部分对半拆分:

[3, 6][5, 7][4, 1][8, 2],再将每一部分对半拆分:

[3][6][5][7][4][1][8][2]

依据双调序列中第三类例子(递增递减数据为 0)可以将这一组数据看成是一系列递增、递减的序列组合。

双调组合(Bitonic Merge)

双调组合是双调排序中的第二步,它的作用是将双调序列组(Bitonic Sequences)组合成一个新的单调序列组,最终不断组合成为一个单调的序列(单调的序列也是一个双调序列)。需要注意的是,每次 Merge 的序列必须是双调序列,单次 Merge 完之后的结果一定是严格单调。还是以 [3, 6, 5, 7, 4, 1, 8, 2] 作为例子,由于上述已经将其拆分成 8 个单调序列组。所以

  • 第一步:两两组合成 4 个双调序列,Merge 变成 4 个单调序列,即 8 个 1 组合成 4 个 2;
  • 第二步:将 4 个单调序列组合成 2 个双调序列,Merge 变成 2 个单调序列,即 4 个 2 组合成 2 个 4;
  • 第三步:将 2 个单调序列组合成 1 个双调序列,Merge 变成 1 个单调序列,即 2 个 4 组合成 1 个 8。

组合的规则是在第 n 次 Merge 时,规定结果升序(ascend),在第 n+1 次 Merge 时,规定结果降序(descend)。将双调序列一分为二,逐位比较,按照 compare & swap 中提到的规则按照升(或者降)序进行比较和交换。再对交换完成的每一半再次一分为二,仍然按照升(或者降)序进行比较交换,再对剩余的进行一分为二,直至切分到最小比较单元为止。最终组合起来即为结果。

最小比较单元:指的是只有两个数进行比较的情况,因为再往下拆分成一个数没有比较的必要。

Merge 1 to 2

对于输入的数据,从左到右两两组合,[3] [6][5] [7][4] [1][8] [2],按照组合的规则 「在第 n 次 Merge 时,规定结果升序(ascend),在第 n+1 次 Merge 时,规定结果降序(descend)」。

第 1 次、第 3 次 Merge 保持升序

根据比较和交换的规则,在递增序列中,如果 a > b 则交换 a, b, 如果 a <= b 则保持不变。所以在 3 和 6 比较时,保持不变。 4 和 1 比较时进行交换。由于此时已经时最小比较单元,则不必再向下拆分。

第 2 次,第 4 次 Merge 保持降序

根据比较和交换的规则,在递减序列中,如果 a < b 则交换 a, b, 如果 a >= b 则保持不变。所以在 8 和 2 比较时,保持不变。 5 和 7 比较时进行交换。由于此时已经时最小比较单元,则不必再向下拆分。

合并结果

此时 Merge 后的结果是 [3, 6] [7, 5] [1, 4] [8, 2]

Merge 2 to 4

依旧对于输入的数据 [3, 6] [7, 5] [1, 4] [8, 2] 从左到右两两组合,按照组合的规则 「在第 n 次 Merge 时,规定结果升序(ascend),在第 n+1 次 Merge 时,规定结果降序(descend)」。

第 1 次 Merge 保持升序

在递增序列中,3 和 7 保持不变,6 和 5 进行交换,得到[3, 5, 7, 6],当前的结果不是最小比较单元,则进行 2 次 SubMerge,将序列切一半后继续 Merge。在 SubMerge 中,3 和 5 进行比较,保持不变;7 和 6 进行交换。最终得到结果[3, 5, 6, 7]

第 2 次 Merge 保持降序

在递减序列中,4 和 2 保持不变,1 和 8 进行交换,得到 [8, 4, 1, 2],当前的结果不是最小比较单元,则进行 2 次 SubMerge,将序列切一半后继续 Merge。在 SubMerge 中,8 和 4 进行比较,保持不变;1 和 2 进行交换。最终得到结果 [8, 4, 2, 1]

合并结果

此时 Merge 后的结果是 [3, 5, 6, 7] [8, 4, 2, 1]

Merge 4 to 8

对于 [3, 5, 6, 7] [8, 4, 2, 1] 这个双调序列,逐位比较。由于只剩一次 Merge 了,那么这一次 Merge 保持升序。

升序 Merge

按照递增的交换规则,第一次 Merge 的时候,3 和 8 保持不变,5 和 4,6 和 2,7 和 1 需要进行交换。当前的结果不是最小比较单元,则进行 2 次 SubMerge,对于 [3, 4, 2, 1],3 和 2,4 和 1 需要交换。对于 [8, 5, 6, 7],8 和 6 需要交换。SubMerge 完成后,仍然不是最小比较单元,则对于每个 SubMerge,再次进行 2 次 SubMerge,2 和 1,6 和 5,8 和 7 需要交换。

合并结果

此时 Merge 后的结果是 [1, 2, 3, 4, 5, 6, 7, 8]

这样一来,经过 3 次的 Merge 和多次 SubMerge,可以得到一个单调有序的序列。

优缺点

  • 优点: 支持并行;排序效率高(时间复杂度低);支持分块(tile)计算这种场景。
  • 缺点:在非并行的场景执行速度会变低;对于输入数据量有要求(n = 2^k);算法实现起来较为复杂。