算法 - 并行排序算法之双调排序(Bitonic_Sort)

目录

引言:

了解:

什么是并行计算: 

并行计算分为时间上并行和空间上并行:

并行排序算法与双调排序之间的关系:

学习:

双调排序(Bitonic_Sort)的介绍及原理:

双调排序中的双调序列:

贝切尔比较器(Batcher comparator):

Batcher定理: 

Batcher归并网络:

 双调排序:

网上资料中的双调排序图:

在画板上进行Batcher定理的实际运用: 

任意序列构造双调序列(Bitonic_Merge):

网上资料中的构造双调序列图:

分析此过程:

类比归并排序的代码:

实现:

参考资料:


引言:

双调排序是我在网上看到的关于并行排序算法时偶然间了解到的一个算法,这个算法也拥有着优秀的排序效率和时间复杂度以及空间复杂度。个人认为理解和实现这个算法对于我们对排序算法的了解和提升是有一定的帮助的,但是在网上所查找的双调排序的资料和文章所用的图片几乎都是一样的,这篇文章我会独立的将双调排序的原理和过程通过画板画出来,并写下我自己对双调排序的认识,再用C语言进行实现,双调排序看着比较难懂,但是如果仔细拆分和研究,实际上还是比较容易懂的。

在写双调排序的代码之前,我们先对基于并行计算的并行排序算法以及双调排序做一个简单的了解。

了解:

什么是并行计算: 

并行计算是一个相对比较庞大的知识体系,他被应用在很多的地方,例如硬件和计算机结构的设计上,这里我只是把并行计算当作对学习双调排序的前提进行了解,不会用太多的笔墨。

并行计算是相对于串行计算来讲的,在知乎上看见了一个特别通俗易懂的例子:

我们上小学每次考完试的时候可能会遇到的两种情况:

情况一:老师说,来,这次考试的所有卷子我批改完了,班长拿下去给每个人一发吧。

情况二:老师说,来,这次考试的所有卷子我批改完了,第一排每个人都过来我给你们一叠卷子你们给咱班娃一发。

其实情况一和二就分别对应着串行计算和并行计算。

并行计算分为时间上并行和空间上并行:

那么何谓时间上并行和空间上并行呢?这里再举两个个我在网上看到的通俗易懂的例子:

一:如果我们此时生产绿箭口香糖,我们就需要机器来进行以下四个步骤:清洗、消毒、切割、包装。在我们有很多包原料的前提下,我们通常会采用的方法就是等待一包原料将上述的四个步骤全部执行完之后再进行下一包原料的加工,有了现代机械化加工之后,我们就可以将上述的四个步骤进行机械化的流水线操作,用四台机器同时负责这四个过程。

上述就是并行计算中的时间上并行,也就是在同一时间同时启动多个操作来提升算法的效率。

二:比方说小明要在植树节进行植树活动,全程操作都由小明一人完成的话需要3小时,但是如果小明叫上他的两个朋友小红和小黑,它们三个一块儿进行植树操作的话每个人只会有1小时的工作量。

上述就是并行计算中的空间上并行,也就是将一个庞大的任务分成多个子任务分别来进行进而提升算法的效率。

并行排序算法与双调排序之间的关系:

并行排序算法是基于计算机并行计算的,并行计算或称平行计算是相对于串行计算来说的。它是一种一次可执行多个指令的算法,目的是提高计算速度,及通过扩大问题求解规模,解决大型而复杂的计算问题。并行排序算法就是在计算机并行计算大力发展的今天为了提高效率而被提出的排序算法,常见的并行排序算法有冒泡排序、归并排序、双调排序,而我们今天着重要了解和实现的算法就是双调排序。

学习:

双调排序(Bitonic_Sort)的介绍及原理:

双调排序是排序网络方法中的一种排序方法,同时也是排序网络中最快的方法之一,排序网络的意思就是网络比较顺序与数据无关的排序方法。

双调排序中的双调序列:

由非严格增序列X和非严格将序列Y组成的序列称之为双调序列。

一个序列:a1,a2,a3,...,an是双调序列,必须满足以下条件:

(1)存在一个ak(1 <= k <= n),使得a1 >= ... >= ak <= ... <= an;

(2)序列能够满足循环移位条件

例如:1,2,3,4,9,8,7,6,5 

贝切尔比较器(Batcher comparator):

Batcher比较器是实现“比较和条件交换”操作的一种逻辑单元。往比较器里面输入两个值,比较器就会输出两个值对应的最大值和最小值。

  

Batcher定理: 

将任意一个长度为n的双调序列A分为等长的两半X、Y,将X中的元素与Y中的元素一一按照原序进行比较,也就是a [i] 与a [i + n/2] (i < n)进行比较,将较大者放入MAX序列中,较小者放入MIN序列,得到的MAX和MIN序列仍然是双调序列,并且MAX序列中任意一个元素是不小于MIN序列中任意一个元素的。我们可以把这个过程看作是一个逐步拆分序列的过程。

Batcher归并网络:

这个归并网络是Batcher这个人提出来的,就是由一系列的Batcher比较器组成的网络,在比较器的两个输入端输入两个值X和Y,然后比较器就会在两个输出端输出最大值和最小值,这是不是和双调序列有些相似?其实双调排序网络就是基于Batcher比较器实现的,Batcher归并网络采用的就是分治法的策略,就是把序列不断的对半分,把前半段进行分类也就是我们双调序列前半段的升序序列,后半段也进行分类也就是后半段的将序序列,然后再利用归并网络进行归并得到新序列。

注意:Batcher归并网络不仅有双调归并,还有奇偶归并。如果排序网络采用双调归并构造的话我们就称它为双调排序网络,如果采用奇偶排序网络构造我们就称它为奇偶排序网络。

 双调排序:

现在假设我们有一个双调序列,我们根据上面的Batcher定理对序列进行双调划分,也就是将这个双调序列分成两个双调序列,递归此过程,也就是一个双调序列划分为两个双调序列,两个双调序列划分为四个双调序列,直到得到的子序列长度为1。

这个过程类似于希尔排序过程中的缩小增量步骤,在之前我曾经在这篇文章:C语言实现希尔排序中使用画板分析过希尔排序的过程,我们定义了一个名为gap的变量作为希尔排序中的增量,gap的初始值就为序列的总长度len,随后每次进行排序的时候gap的值都会缩小一半,直到增量缩小为1,此时就是希尔排序的倒数第二趟(因为最后一趟是对序列的检查),这一趟也就相当于是插入排序,在这里我们直接给出我之前写过的希尔排序代码用于我们对双调排序的类比和理解:

//希尔排序
void Shell_sort(int *ar, int len) {
	assert(ar != nullptr && len >= 0);
	int i = 0,j = 0;
	int temp = 0;//定义中间值用于存储数据
	int gap = 0;//定义排序增量
	gap = len;//将排序增量的初始值直接设置为数组的长度
	while(gap > 1){
		gap /= 2;
		for(i = gap;i < len;i += gap){
			if(ar[i] < ar[i - gap]){
				temp = ar[i];
				for(j = i - gap;j >= 0 && ar[j] > temp;j -= gap){
					ar[j + gap] = ar[j];
				}
				ar[j + gap] = temp;
			}
		}
	}
}

好了,对比希尔排序,我们在这里直接给出双调排序的排序图,在这里我给出两张图片,一张是网上都能找到的双调排序图,一张是我自己画出来的以便于大家进行理解:

网上资料中的双调排序图:

在画板上进行Batcher定理的实际运用: 

这张图看着很抽象,实际上理解起来还行,接下来我在画板上进行讲述:

我们从上图得知,初始的双调序列为:3,5,8,9,10,12,14,20,95,90,60,40,35,23,18,0,一共是16个元素,也就是n的值此时为16,我们第一次进行双调划分的长度就是16 / 2 = 8,如图:

这时你应该能清楚的感觉到Batcher定理与希尔排序的相似之处,我画的这张图也就对应着Batcher定理中的将任意一个长度为n的双调序列A分为等长的两半X、Y,将X中的元素与Y中的元素一一按照原序进行比较,也就是a [i] 与a [i + n/2] (i < n)进行比较,序列总长度为n,左边长度为8的序列为X序列,右边为Y序列,用a[0](序列中的3)和a[0 + 8](序列中的95)进行比较,再用a[1]和a[1 + 8]进行比较,然后重复此过程。

接下来我们再将两个元素比较之后的较大者放入MAX序列,较小者放入MIN序列:

 

我交换了元素20和0 的位置,形成了两个序列注意此时MAX序列中任意一个值都是大于MIN序列中的数的,此时n的值更新为8,此时递归双调划分的过程,我们再次得到新的序列,并且再次将双调序列对半分,进行比较:

如图,在前面一整个双调序列的基础上又形成了两个双调序列,此时n的值更新为4,我们继续进行双调划分的过程,将序列对半分进行比较:

此时,序列已经被划分称为了4个双调序列,分别是3,0,8,5 | 10,9,14,12 | 18,20,35,23 | 60,40,95,90, 我们现在将n的值更新为2,n \ 2 = 1,现在子序列的长度为1,所以我们对相邻元素之间进行比较,并把相对较小的值放在前面,如图为比较后的序列:

此时我们经过四次依据Batcher定理的划分,成功将一个双调序列拆分成现在的升序序列,这也是整个双调排序中至关重要的一步。

现在我们可以将一个双调序列转化为升序序列,但是问题是在实际使用算法解决问题的过程中,开始不可能直接给我们提供双调序列而需要我们自己来构建双调序列,所以此时就有一个新的问题:如果把一个任意序列变成双调序列进而完成升序排序?

任意序列构造双调序列(Bitonic_Merge):

假设我们现在随意输入n个整形值,我们要对这n个元素进行双调排序形式的升序排序,我们必须要做的就是先将这n个元素构造成一个双调序列,然后再递归运用Batcher定理完成最终的升序排序。

那么如何将任意序列构造成双调序列呢?

构造双调序列的过程称为Bitonic_Merge,也许你也注意到了后面有一个Merge的单词,Merge在中文的意思就是合并、归并的意思,这时自然而然就想到了归并排序,其实这个过程是和归并排序(Merge_Sort)相类似的。

网上资料中的构造双调序列图:

这张图还是能轻易地读懂的,和上面的过程恰恰相反,我们首先设定子序列长度为1,将序列中的元素两两进行比较,第一趟构造一个长度为2的单调序列,序列的单调性依次为单调递减、单调递增这样往复下去,然后对长度为2的子序列进行两两归并,并再次对长度为4的序列中的元素进行排序,如此递归下去直到生成一个完整的双调序列。

注:相邻之间的线条我们可以理解为计算机中的比较器。

分析此过程:

我们从上图得知初始序列为:10,20,5,9,3,8,12,14,90,0,60,40,23,35,95,18。

Step 1 : 将子序列的长度设置为2,并按照单调递增、单调递减、单调递增、单调递减的顺序将子序列排序好,排序后的序列为:10,20,9,5,3,8,14,12,0,90,60,40,23,35,95,18 

Step 2:将长度为2的子序列进行两两合并,此时子序列的长度为原子序列长度的2倍,重新进行排序,子序列的单调性依次为递增递减,排序后的序列为:5,9,10,20,14,12,8,3,0,40,60,90,95,35,23,18

Step 3 : 将长度为4的子序列继续进行归并,此时子序列的长度为8,且此时序列中只会出现一个单调递增序列和一个单调递减序列,排序后的序列为:3,5,8,9,10,12,14,20,95,90,60,40,35,23,18,0

Step 4:双调序列构造完成。

注意:小的双调子序列在归并的过程中,还是需要继承归并前的比较方式的,我们可以发现,长度为2的子序列在归并成长度为4的子序列时,拿递增序列举例子,不仅要进行原来的两两比较,还要进行Batcher定理中a [i] 与a [i + n/2]这种比较方式,只不过此时n的值从原来的2更新到了现在的4,同样在子序列长度从4变为8的过程中,还要继承原来子序列长度为2和长度为4的比较方式,并在此基础上添加a [i] 与a [i + n/2]的比较方式,此时n的值更新为归并后的8,也就是a[1]与a[1 + 8 / 2]进行比较,如此往复下去。

类比归并排序的代码:

在我之前写过的文章:C语言实现归并排序(Merge_sort)中提到过,归并排序就是将子序列一层一层的进行合并,并且按照升序的顺序将元素依次放置最终的到我们想要的序列,在这里我直接给出归并排序的核心代码我们进行类比分析,以便于我们更好地理解构造双调序列的过程:

归并排序的核心代码:

void merge(int *ar,int low,int middle,int high,int *temp)//归并排序
{
	int i = low;//通过I遍历左半边数组
	int j = middle + 1;//通过j遍历右半边数组
	int k = low;//通过k遍历整个数组
	while(i <= middle && j <= high){//循环通过I和j分别遍历从中间划分开来的两个数组,并分开比较大小
		temp[k++] = ar[i] < ar[j] ? ar[i++] : ar[j++];//如果前者小于后者就将前者直接添加至新数组的后面,依此类推
	}
	while(i <= middle)//如果比较完了,但左半边数组还有剩余的元素,直接将这些元素添加至新数组的后方
	temp[k++] = ar[i++];
	while(j <= high)//同理,比较完了右半边还有剩余的,也直接添加至新数组的后方
	temp[k++] = ar[j++];
	for(i = low;i <= high;i++){//最后将新数组的所有元素赋值给ar数组
		ar[i] = temp[i];
	}
}
void merge_sort(int *ar,int low,int high,int *temp)
{
	if(low >= high){//如果左边界大于右边界,直接返回
		return;
	}
	int middle = low + (high - low) / 2;//相比较(low + high)/ 2;的优化写法,可以防止越界
	merge_sort(ar,low,middle,temp);递归调用,左半部分递归归并
	merge_sort(ar,middle + 1,high,temp);//右半部分递归归并
	merge(ar,low,middle,high,temp);//递归操作
}
void mergesort(int *ar,int len)//最终归并函数
{
	int *temp = (int *)malloc(sizeof(int)*len);//向堆区申请len个int类型大小的空间赋给temp新数组
	assert(temp != nullptr);//断言新数组不为空
	merge_sort(ar,0,len - 1,temp);//递归归并操作
	free(temp);//释放temp的内存空间
    temp = NULL;//将temp置空
}

在归并排序中,一共有三个函数,分别是排序函数,递归归并边界函数、最终归并函数,也就是说我们要归并子序列并且排序它,需要有三个功能,分别是按照排序规则将子序列中的元素进行排序 、递归将边界进行合并并再次进行排序、开辟空间并合并为最终序列。此时我们即将要进行的构造双调序列与归并排序类似,只不过我们需要将排序的规则替换为双调序列的规则也就是从小到大再从大到小,并继承原先子序列中的比较方式。

实现:

刚才在分析双调排序的过程中我们不难发现,实现这个排序首先要做的就是交换函数,其次为序列等分和升序降序的排序和归并,所以我们对这几个功能来分别实现。

#include <iostream>
#include <iomanip>

using namespace std;

void printArr(int *arr, int len) {
    for (int i = 0; i < len; ++i) {
        cout << setw(3) << arr[i];
    }
    cout << endl << endl;
}

// 由bitonicSort中的顺序可知,这里传入的arr已是双调序列
void bitonicMerge(int *arr, int len, bool asd) {
    if (len > 1) {
        int m = len / 2;
        for (int i = 0; i < m; ++i) {
            if (arr[i] > arr[i + m] == asd)
                swap(arr[i], arr[i + m]); // 根据asd判断是否交换
        }
        // for循环结束后又生成了2个双调序列,分别merge直到序列长度为1
        bitonicMerge(arr, m, asd); // 都是按照asd进行merge
        bitonicMerge(arr + m, m, asd);
    }
}

void bitonicSort(int *arr, int len, bool asd) { // asd 升序
    if (len > 1) {
        int m = len / 2;
        bitonicSort(arr, m, !asd); // 前半降序
        bitonicSort(arr + m, len - m, asd); // 后半升序
        // 前2个sort之后形成了1个双调序列,然后传入merge合并成asd规定的序列
        bitonicMerge(arr, len, asd); // 合并
    }
}


int main() {
    int len = 16;
    auto *arr = new int[len]; // 自动识别类型
    bool asd = true;

    // 1.随机生成无序序列
    srand((unsigned int) time(nullptr));

    for (int i = 0; i < len; ++i) {
        arr[i] = (int) random() % 100;
    }

    cout << "无序序列: ";
    printArr(arr, len);
    
    // 2.双调排序
    bitonicSort(arr, len, asd);

    cout << "双调排序: ";
    printArr(arr, len);
}

文章未完成,待补充。

参考资料:

CUDA(六). 从并行排序方法理解并行化思维——冒泡、归并、双调排序的GPU实现_Rachel-Zhang的博客-CSDN博客

双调排序Bitonic Sort,适合并行计算的排序算法_不动明王1984的博客-CSDN博客

谢小帅的博客-双调排序

双调排序_Icoding_F2014的博客-CSDN博客_双调排序时间复杂度

什么是双调排序?(双调归并/双调排序整理)_QuteMelon的博客-CSDN博客_双调排序

  • 3
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ritchie_Zeng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值