最近老师讲了并行的排序算法,让我对这个原来不是很了解的排序算法产生了浓厚的兴趣。并行排序方法,是指采用并行计算的方法对一组数据进行排序,理论上是在类似内排序的环境下,采用多核并行的方法让时间降低,排序的复杂度最好的情况下能降低至O(n)左右。
排序的实质
排序的实质是什么?这是一个不是问题的问题。我们可以说是让所有的数都按照一定的规则被放置,但这种说法实际上是解释了排序的汉字含义。换句话不如说排序是:从序列中任选一对数都是有序的,那么此序列就是已排序的。
普通(串行)冒泡排序
为了满足上述的概念,我们发现了冒泡排序法:双层循环,比较数组中可取到的任意一对数字,如果不满足要求则交换。这是一种最简单的排序方法,理解起来很简单,也与上述的排序实质含义很符合。但是我们在平常的程序中完全不会采用冒泡排序,这是因为冒泡排序有很多缺点:
- 比较次数是所有排序中最多的,必须要进行(n-1)2/2次比较,按照冒泡排序的定义,很难有优化的方法。
- 交换次数不固定而且很多,相比较冒泡排序的次数不固定,同样是复杂度为O(n2)的选择排序虽然比较次数也较多,但是可以把交换次数稳定在n次。
- 不能利用序列一些隐含的信息。冒泡排序只能不断的比较比较比较,对序列没有记忆性,相比较冒泡排序同样是复杂度为O(n2)的插入排序和复杂度约为O(n1.3)的希尔排序却能在序列为几乎完成排序的状态下用相当好的效果完成排序。
冒泡排序、选择排序和插入排序同为O(n2)简单排序,但是相比较另外两种还有一些特点,冒泡排序基本没什么用算是废了。
package Main;
/** * Title: BubbleSort * Description: BubbleSort Test * Company: www.QuinnNorris.com * * @date: 2017/11/30 上午12:31 星期四 * @author: quinn_norris * @version: 1.0 */
public class BubbleSort {
public static void main(String[] args) {
int[] arr = {3, 2, 1, 4, 5};
bubbleSort(arr);
}
public static int[] bubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++)
for (int j = 0; j < n - 1 - i; j++)
if (arr[j] > arr[j + 1]) {
arr[j] = arr[j] ^ arr[j + 1];
arr[j + 1] = arr[j] ^ arr[j + 1];
arr[j] = arr[j] ^ arr[j + 1];
}
return arr;
}
}
冒泡排序推广——奇偶交换排序
就像是红黑树的结构推广自比较简单的2-3树一样,这里用刚才的冒泡排序推广出另外一种较为复杂的排序:奇偶交换排序。
在冒泡排序中,我们采用的思想是不断的比较临近的两个数字,如果位置不对就交换。在每轮中一个数字可能会与身边的另一个发生交换,理论上至多需要n轮才能把一个数字确保交换到正确的位置上。第一次循环会讲最大的数字移动到数组最右边,下一次循环把第二大的数移动到右侧第二个位置,以此类推。而奇偶交换排序是将每一轮每对数字比较与交换操作分为奇偶两类分别执行。
比如一种可行的情况,当n=3时:
- 先进行偶数比较并交换 arr[0],arr[1]
- 再进行奇数比较并交换 arr[1],arr[2]
- 再进行偶数比较并交换 arr[0],arr[1]
定义:奇偶交换排序进行n轮比较与交换,第一轮进行所有索引为偶数的元素与后一位进行比较(如果没有后一位则不进行比较),如果顺序有误则交换;下一轮进行所有索引为奇数的元素与后一位比较(如果没有后一位则不进行比较),如果顺序有误则交换;如此反复交替进行n轮,则此时序列为已排序序列。
奇偶交换排序正确性验证
一般的,有如下定理:
设A是一个拥有n个键值的列表,作为奇偶交换排序算法的输入,那么经过n个阶段后,A能够排好序。
尝试了一下,当n为3的时候将{3,2,1}序列转化为{1,2,3}序列需要3步,当n为5的时候将{5,4,3,2,1}序列转化为{1,2,3,4,5}序列需要5步都满足上述定理。但是这只是特例尝试,能不能用一般性证明上述定理的正确性呢?个人觉得大概可以采用如下思路证明:
当n=5时,
第一次: 先进行偶数比较并交换 (arr[0],arr[1]) (arr[2],arr[3])
第二次: 再进行奇数比较并交换 (arr[1],arr[2]) (arr[3],arr[4])
第三次: 再进行偶数比较并交换 (arr[0],arr[1]) (arr[2],arr[3])
第四次: 再进行奇数比较并交换 (arr[1],arr[2]) (arr[3],arr[4])
第五次: 再进行偶数比较并交换 (arr[0],arr[1]) (arr[2],arr[3])
直接看很难看出这种方法的正确性,我们稍微做一些变换,将一次偶数的交换和奇数的交换的效果联合起来形成一条交换链。比如上例n=5时,从n=0开始或n=1开始向下取四次交换都可以形成两条完成的交换链,总计能够形成n/2条交换链,每条交换链相当于冒泡排序中一次完整的比较,在一条交换链中一个元素可以移动0个位置、1个位置或2个位置,在n/2条交换链中一个元素最多可移动n/2*2=n个位置,通过交换链这种说法我们得出结论,任何一个元素可以移动到序列中任何想要的位置上。
回忆未优化的冒泡排序,冒泡排序的正确性是显而易见的,在冒泡排序中一个元素被比较的次数最多为n次,而在奇偶交换排序中每个中间元素被比较的次数也为n次,在奇数次与一侧数据比较,偶数次与另侧数据比较,不断交替这两种情况。由此我们可以将奇偶交换排序转化为元素比较方向不断变化的冒泡排序。只要证明比较方向不断变化的冒泡排序是正确的即可,它的正确性是显然的。
运用奇偶交换排序算法进行并行排序
在不使用并行的时候,奇偶交换排序的复杂度为每轮的比较次数乘以比较轮数O(n2)。但是如果我们用多核去并行计算那么它的时间复杂度就能降到约O(n)左右。
n个数据使用n核计算
如果我们一共有n个数据需要进行排序,那么我们使用n个核,每个核存储一个数据。当进行外循环时,如果外轮的索引是偶数,那么就控制内层的偶数索引的核和它左侧的奇数索引的核的数据进行比较如果顺序有误则交换,当外循环索引是奇数,那么相反和右侧的进行比较交换。这样,内层循环采用并行的方法,时间复杂度为O(1),整体复杂度为O(n),在较短的时间内仍能保证算法的正确性。
n个数据使用p核计算
上面的例子是在n个数据使用n核计算的情况下,但是实际上根本不可能出现这种情况,一般的我们的数据都非常大,而我们能够使用的核数最多在两位数,那么在这种情况下,我们为每个核分配n/p个数据,首先将每个核内进行快速的内排序,然后在每个核之间进行类似奇偶交换排序的算法进行排序,只不过这时两个核中的数据不能简单比较,我们在这里采用归并排序的方法对两个核中的数据进行排序,然后把小的那一半放在左边,大的另外一半数据放在右边。用这种方法模拟出奇偶交换排序的思路。当p越大时、当p和n越接近时这种算法效率最好,这种情况下时间复杂度与p有关,但因为采用的这种方法本身原因,算法复杂度必定要小过O(nlogn),是一种不错的并行排序方法。
为什么用冒泡进行并行排序而不用其他排序算法?
为什么采用冒泡算法的变种奇偶排序算法进行并行排序,而不采用其他的选择、插入、快速、堆、归并等等…其他的算法进行并行排序呢?
原因在于奇偶交换排序有个优点,在每一轮内部,他的比较和交换是同时发生的可以同时处理。就是说在每一轮内部,两个数字间如果比较后发现顺序错误那么这两个数据交换即可,这两个数字的状态和本轮内其他任何数字都没有关系,是独立的。在多核中,每一组配对的核只需要判断对方的情况并选择是否进行交换即可,这些组可以同时进行没有依赖关系。
观察其他的排序算法,选择排序是在一轮中挑选出最大的数字交换位置,无论是否用多核计算,必定有n次的遍历;插入排序要一直判断与前一个数字相比的大小也是必定有n次遍历;而快速排序和归并排序将数据分组递归计算这不适合用并行计算;堆排序先进行线性时间的建堆,再不断调整树结构进行排序时间较大的部分在调整树结构,但这部分根本没办法用并行去优化。