双调排序(Bitonic sort)学习

之前的时候参加浙大的考核,学习了一下双调排序,正好现在在学习CUDA编程,因此把之前的学习成果整理一下。


问题说明: 
*********************** 
给出分成m段 的n个浮点数,输入数据已按段号有序,但每段内部 无序。用C/C++ 编写一个分段双调排序(Bitonic sort)函数,对每一段内部的浮点数进行排序,但 不要改变段间的位置。 


接口方式: 
*********************** 
void segmentedBitonicSort(float* data, int* seg_id, int* seg_start, int n, int m); 
输入数据中,data包含需要分段排序的n个float值,seg_id给出data中n个元素各 自所在的 段编号。seg_start共有m+1个元素,前m个分别给 出0..m-1共m个段的起 始位置,seg_start[m]保证等于n。 
seg_id中的元素保证单调不下降,即对任意的i<j,seg_id[i]<=seg_id[j]。 seg_id所有元 素均在0到m-1范围内。 
输出结果覆盖data,保证每一段内排序,但不改变段间元素的顺序。 

注意: 
*********************** 
1、必须使用双调排序算法进行排序。 
2、可以直接使用从网上下载的双调排序代码,但须注明出处。 


样例输入: 
*********************** 
float data[5]={0.8, 0.2, 0.4, 0.6, 0.5}; 
int seg_id[5]={0,   0,   1,   1,   1} 
int seg_start[3]={0,2,5}; 
int n=5; 
int m=2; 

样例输出: 
*********************** 
float data[5]={0.2, 0.8, 0.4, 0.5, 0.6}; 

加分挑战(非必需): 
*********************** 
1、不递归:segmentedBitonicSort函数及其所调用的任何其他函数都不得直接或 间接地进行递归。 
2、不调用函数:segmentedBitonicSort不调用除标准库函数外的任何其他函数。 
3、内存高效:segmentedBitonicSort及其所调用的任何其他函数都不得进行动态 内存分配,包括malloc、new和静态定义的STL容器。 
4、可并行:segmentedBitonicSort涉及到的所有时间复杂度O(n)以上的代码都写 在for循 环中,而且每个这样的for循环内部的循环顺序可 以任意改变,不影响程 序结果。注:自己测试时可以用rand()决定循环顺序。 
5、不需内存:segmentedBitonicSort不调用任何函数(包括C/C++标准库函数), 不使用全局变量,所有局部变量都是int、float或指针类 型,C++程序不使用new 关键字。 
6、绝对鲁棒:在输入数据中包含NaN时(例如sqrt(-1.f)),保证除NaN以外 的数 据正确排序,NaN的个数保持不变。 

 

算法描述

双调序列

在了解双调排序算法之前,我们先来看看什么是双调序列。 双调序列是一个先单调递增后单调递减(或者先单调递减后单调递增)的序列。

Batcher定理

将任意一个长为2n的双调序列A分为等长的两半X和Y,将X中的元素与Y中的元素一一按原序比较,即a[i]与a[i+n] (i < n)比较,将较大者放入MAX序列,较小者放入MIN序列。则得到的MAX和MIN序列仍然是双调序列,并且MAX序列中的任意一个元素不小于MIN序列中的任意一个元素[2]。

双调排序

假设我们有一个双调序列,则我们根据Batcher定理,将该序列划分成2个双调序列,然后继续对每个双调序列递归划分,得到更短的双调序列,直到得到的子序列长度为1为止。这时的输出序列按单调递增顺序排列。

见下图:升序排序,具体方法是,把一个序列(1…n)对半分,假设n=2^k,然后1和n/2+1比较,小的放上,接下来2和n/2+2比较,小的放上,以此类推;然后看成两个(n/2)长度的序列,因为他们都是双调序列,所以可以重复上面的过程;总共重复k轮,即最后一轮已经是长度是2的序列比较了,就可得到最终的排序结果。

任意序列生成双调序列

前面讲了一个双调序列如何排序,那么任意序列如何变成一个双调序列呢?

这个过程叫Bitonic merge, 实际上也是divide and conquer的思路。 和前面sort的思路正相反, 是一个bottom up的过程——将两个相邻的,单调性相反的单调序列看作一个双调序列, 每次将这两个相邻的,单调性相反的单调序列merge生成一个新的双调序列, 然后排序(同3、双调排序)。 这样只要每次两个相邻长度为n的序列的单调性相反, 就可以通过连接得到一个长度为2n的双调序列,然后对这个2n的序列进行一次双调排序变成有序,然后在把两个相邻的2n序列合并(在排序的时候第一个升序,第二个降序)。 n开始为1,每次翻倍,直到等于数组长度,最后就只需要再一遍单方向(单调性)排序了。

以16个元素的array为例,

1. 相邻两个元素合并形成8个单调性相反的单调序列,

2. 两两序列合并,形成4个双调序列,分别按相反单调性排序

3. 4个长度为4的相反单调性单调序列,相邻两个合并,生成两个长度为8的双调序列,分别排序

4. 2个长度为8的相反单调性单调序列,相邻两个合并,生成1个长度为16的双调序列,排序双调排序示意图

 

算法实现

我首先实现了双调排序的基本功能,递归无疑是最容易思考的方法,具体思想如下

其中双调序列排序是核心部分,其具体思想又可由下图表示:

其中MAX和MIN为两个小的双调序列,MAX的所有元素大于MIN的所有元素(Batcher定理),进而可以将其进一步拆分成更小的双调序列。

由于双调排序对于2的幂次长的序列有最为简单的形式,因此在算法实现中对序列进行的补长,使得补长后的序列长度为2的幂次。最后再将填充的元素从序列中剔除即可。

代码中当出现除以2或者乘以2的时候一律使用右移或左移,可以使得效率更高。

我重点对网上的双调排序的代码进行了精简,主要是针对Sort函数和Merge函数,增加了代码的重用性。

 

拓展部分

不递归

为了保证不递归,我们需要采用迭代的方法进行排序,代码以及具体思路如下:

以序列总长度为8为例:

其中利用箭头代表每次比较后箭头连接的两个元素的大小关系

step表示每过step的步长以后是一个新的双调序列

step0表示将当前双调序列拆分成两个子双调序列需要比较的总次数

i表示以两个双调序列为一组,i为每个组的起始序号(由于两个双调序列经过排序以后可以合成一个双调序列,所以用i来表示每个组的起始序号)

j表示在对双调序列进行双调排序时,每过多少间隔可以划分成一个新的子序列

k表示将当前双调序列拆分成两个子双调序列时正在第k次比较

 

所以对于需要升序的双调排序而言,当前比较的序号为第i个双调序列中第j个子序列的第k次比较,即i+j+k;对于需要降序的双调排序而言还需要加上step表示与升序的双调序列相邻的双调序列,即i+j+k+step。通过这种迭代的算法,可以免去原算法中的递归部分

不调用函数

由于没有了递归,自然就免去了递归函数;同时也可以将交换函数swap放在segmentedBitonicSort中使得segmentedBitonicSort不调用任何的函数

内存高效

在原来的实现中要通过new来开辟一片新的数据区来存放增长后的数据,但在改进后直接在segmentedBitonicSort中声明一个新的数组来存放数据即可。

可并行

segmentedBitonicSort涉及到的所有时间复杂度O(n)以上的代码都写在for循环中,而且for循环内部除了step与step0的循环外循环顺序均可以任意改变,验证之后发现其余三个循环的顺序确实不影响程序结果。

不需内存

原实现需要通过ERROR这个全局变量来通知主程序输入是否出错,现在讲输出也放在segmentedBitonicSort内,使得出错后可以直接return,开始读入下一组数据

绝对鲁棒

利用NaN!=NaN的特点来对其进行识别,使得只要满足这个条件时,它总会排在较后的位置,因此当双调排序结束后,NaN总会排在序列最后面且保证NaN的个数不变

 

源码

/***************************************************
* 文件名:Version2.cpp
* 文件描述:给出分成m段的n个浮点数,输入数据已按段号有序,但每段内部无序。用C/C++ 编写一个分段双调排序(Bitonic sort)函数,对每一段内部的浮点数进行排序,但不改变段间的位置。本版本用迭代的方法
* 作者:
* 身份:
* E-mail   . 
* Mobile   . 
* QQ/Wechat. 
***************************************************/

#include<cstring>
#include<stdio.h>
#include<vector>
using namespace std;

#define MAX 99999//MAX为将数据补长至2的幂的填充数
#define MIN 0.00000000001//浮点数无法直接比较,因此设定一个最小值,当两者之差小于MIN时认为二者相等


void segmentedBitonicSort(float* data, int* seg_id, int* seg_start, int n, int m)
{
	//输入出错
	if (seg_start[m] != n || seg_id[n - 1] != (m - 1))
	{
		printf("Input Error!\n");
		return;
	}

	int seg = 0;
	for (int i = 0; i < n; i++)
	{
		if (seg < m && i == seg_start[seg])
		{
			float* num = data + i;//num为每段开头的元素的地址
			int len = 1;
			int seg_len = seg_start[seg + 1] - seg_start[seg];

			//计算不小于数据长度的最小的2的幂
			while (len < seg_len)
			{
				len = len << 1;
			}

			float Nnum[65536] = { 0 };

			for (int i = 0; i<len; ++i)  //将数据补长,使数据的长度为2的幂
			{
				Nnum[i] = (i < seg_len) ? num[i] : MAX;

			}

			//通过迭代实现双调排序
			for (int step = 2; step <= len; step <<= 1)
			{
				for (int i = 0; i < len; i += step << 1)
				{
					for (int step0 = step >> 1; step0 >0; step0 >>= 1)
					{
						for (int j = 0; j < step; j += step0 << 1)
						{
							for (int k = 0; k < step0; ++k)
							{
								if (Nnum[i + j + k] > Nnum[i + j + k + step0] || Nnum[i + j + k] != Nnum[i + j + k])
								{
									float temp = Nnum[i + j + k];
									Nnum[i + j + k] = Nnum[i + j + k + step0];
									Nnum[i + j + k + step0] = temp;
								}
								if (i + step < len)
								{
									if (Nnum[i + j + k + step] < Nnum[i + j + k + step + step0] || Nnum[i + j + k + step + step0] != Nnum[i + j + k + step + step0])
									{
										float temp = Nnum[i + j + k + step];
										Nnum[i + j + k + step] = Nnum[i + j + k + step + step0];
										Nnum[i + j + k + step + step0] = temp;
									}
								}
							}
						}
					}
				}
			}

			int j = 0;
			//删除补长的填充码
			for (int i = 0; i<len; ++i)
			{
//将排序后的除了填充元素的所有元素(包括NaN)放到原数组中
				if (MAX - Nnum[i] > MIN || Nnum[i] != Nnum[i])
				{
					num[j++] = Nnum[i];
				}
			}
			seg++;
		}
	}

	//输出结果
	for (int i = 1; i<m + 1; i++)
	{
		printf("seg_id %d : ", i - 1);
		for (int j = seg_start[i - 1]; j < seg_start[i]; j++)
		{
			if (data[j] == data[j])
				printf("%g ", data[j]);
			else
				printf("nan ");
		}
		printf("\n");
	}
	return;
}


int main()
{
	int n;
	int m;
	float data[1000] = { 0 };
	int seg_id[1000] = { 0 };
	int seg_start[1000] = { 0 };

	/*输入格式:n, m, data, seg_id, seg_start
	其中n代表n个浮点数
	m代表将浮点数分为m段
	data为具体的数值
	seg_id表示data中n个元素各自所在的 段编号
	seg_start表示0..m-1共m个段的起始位置*/

	printf("Please input in following order: \nn, m, data, seg_id, seg_start\n");
	while (scanf("%d%d", &n, &m) != EOF)
	{
		if (n <= 0 || m <= 0 || m>n)
		{
			printf("Input Error!\n");
			continue;
		}
		for (int i = 0; i<n; i++)
		{
			scanf("%f", &data[i]);
		}
		for (int i = 0; i<n; i++)
		{
			scanf("%d", &seg_id[i]);
		}
		for (int i = 0; i<m + 1; i++)
		{
			scanf("%d", &seg_start[i]);
		}
		segmentedBitonicSort(data, seg_id, seg_start, n, m);
		memset(data, 0, sizeof(data));
		memset(seg_id, 0, sizeof(seg_id));
		memset(seg_start, 0, sizeof(seg_start));
	}
	return 0;
}

 

性能分析 

双调排序的核心思路是先将普通的序列装化为双调序列,并对双调序列进行双调排序后合成大的双调序列,而双调排序又是将大的双调序列拆分成小的双调序列后进行比较。这么做看起来好像是绕了一个大弯,本来一个序列直接对他进行排序就好了,为何又要把他先合起来再拆分呢?在时间复杂度的维度上来看,我们根据前面比较次数的图片可以很容易求出双调排序的时间复杂度(具体推导如下)

可见其时间复杂度为O(n*logn*logn),虽然比冒泡排序O(n^2)好一点,但是却不如常用的快速排序与归并排序的O(n*logn)。那么我们又为什么要采用这个排序方法呢?原因是双调排序是排序网络方法中的一种。而排序网络指的就是序列是数据独立的,即网络比较顺序与数据无关的排序方法。它将原本只能一次性处理的数据转换成了多次处理,所以特别适合硬件做并行化。这也与通信中的FFT(快速傅里叶变换)思想非常相像:为了求出离散信号的频谱,我们往往不是用DFT来求,而是采用更快的FFT来求,具体思路也是讲长序列拆分为短序列,再通过蝶形图(如下图)合成长序列的频谱。

 

综上所述:双调排序经过优化后具有以下优点:

  1. 不递归,降低空间复杂度
  2. 不调用其他库函数
  3. 保证内存的高效性,不适用动态内存分配
  4. 可以并行运算从而使当有多处理器时可以降低总的处理时间
  5. 不需要内存,不存在全局变量,不使用new
  6. 绝对鲁棒,在输入数据中包含NaN时(例如sqrt(-1.f)),把NaN当作比任意数大的数进行排序,保证除NaN以外的数据正确排序,NaN的个数保持不变。

缺点有:

  1. 时间复杂度不如归排、快排
  2. 当数据量大时,难以用数组存放数据,但是如果进行优化可以通过setvbuf函数设置I/O缓存,从而保证程序的正确运行
  3. 虽然在segmentedBitonicSort开辟的数组是局部变量,但是由于它需要容纳不定长的数据,所以我给他开了65536的长度,虽然函数调用结束后会释放,但是在函数执行的时候还是造成了很大一部分的内存浪费 

参考资料

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

https://blog.csdn.net/abcjennifer/article/details/47110991

三十分钟理解:双调排序Bitonic Sort,适合并行计算的排序算法

https://blog.csdn.net/xbinworld/article/details/76408595

分段双调排序实现

https://blog.csdn.net/hanshuning/article/details/49132089


 老师给出的回复:

我们对你提交的代码进行了测试,发现有如下问题:

1. 引入了接口定义之外的依赖;

2. 栈上直接声明静态数组的方法支持的数据规模有限;

3. 不需内存的挑战也不能算完成;

 

 

  • 4
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值