并行排序
排序是一項非常常用的操作,你的應用程序在運行時,可能無時無刻不在進行排序操作。排序的算法有很多,但是對于大部分的算法都是串行執行的。當排序的元素很多時,若使用并行算法代替串行,顯然可以更加有效的利用CPU,提高排序效率。但將串行算法修改為并行算法并非易事,甚至會極大的增加原有算法的復雜度。下面介紹幾種相對簡單的算法。
奇偶交換排序:分離數據相關性
在介紹奇偶交換排序前,首先來看一下冒泡排序。在這里,我們將數組從小到大排序:
1 public static int[] bubbleSort(int[] arr){ 2 for (int i = 0;i < arr.length - 1;i++){ 3 for (int j = 0;j < arr.length - 1 - i;j++){ 4 if (j+1 == arr.length-i){ 5 break; 6 } 7 if (arr[j] > arr[j+1]){ 8 int temp = arr[j]; 9 arr[j] = arr[j+1]; 10 arr[j+1] = temp; 11 } 12 } 13 } 14 return arr; 15 }在冒泡排序的交換過程中,由于每次交換的兩個元素存在數據沖突,也就是對于每個元素,它既可能與前面的元素交換,也可能與后面的元素交換,因此很難直接改造成并行算法。如果能夠解開這種數據的相關性,就可以比較容易的使用并行算法來實現類似的排序。奇偶交換排序就是基于這種思想的對于奇偶交換來說,它將排序過程分為兩個階段,奇交換和偶交換。對于奇交換來說,它總是比較奇數索引以及相鄰的后續元素;偶交換總是比較偶數索引和其相鄰的后續元素,并且奇交換和偶交換會成對出現,這樣才能保證比較和交換涉及到數組中的每一個元素。
?從上圖可以看出,由于將整個比較交換獨立分割為奇階段和偶階段。這就使得在每一個階段內,所有的比較和交換都是沒有數據相關性的。因此,每一次比較和交換都可以獨立執行,也就可以并行化了。
下面是奇偶交換的串行實現:
1 public static int[] oddEvevSort(int[] arr){ 2 int exchFlag = 1,start = 0; 3 while (exchFlag == 1 || start == 1){ 4 exchFlag = 0; 5 for (int i = start;i < arr.length - 1;i += 2){ 6 if (arr[i] > arr[i+1]){ 7 int temp = arr[i]; 8 arr[i] = arr[i+1]; 9 arr[i+1] = temp; 10 exchFlag = 1; 11 } 12 } 13 if (start == 0){ 14 start = 1; 15 }else { 16 start = 0; 17 } 18 } 19 return arr; 20 }上述代碼中,exchFlag用來記錄當前迭代是否發生了數據交換,而start變量用來表示是奇交換還是偶交換。初始時,start為0,表示進行偶交換,每次迭代結束后,切換start狀態。如果上一次發生了數據交換,或者當前進行的是奇交換,循環就不會停止,直到程序不再發生交換,并且當前進行的是偶交換為止(表示奇偶交換已經成對出現)。
下面是奇偶交換的并行排序:
1 public class CurrentOddEven { 2 static int[] arr = {23,3,23,4,5,5,6,6,6453,678,68,9,9,79878,97,897897,98,78}; 3 static int exchFlag = 1; 4 static synchronized void setExchFlag(int v){ 5 exchFlag = v; 6 } 7 8 static synchronized int getExchFlag(){ 9 return exchFlag; 10 } 11 public static class OddEvenSortTask implements Runnable{ 12 int i; 13 CountDownLatch latch; 14 15 public OddEvenSortTask(int i,CountDownLatch latch){ 16 this.i = i; 17 this.latch = latch; 18 } 19 20 @Override 21 public void run() { 22 if (arr[i] > arr[i+1]){ 23 int temp = arr[i]; 24 arr[i] = arr[i+1]; 25 arr[i+1] = temp; 26 setExchFlag(1); 27 } 28 latch.countDown(); 29 } 30 } 31 32 public static void main(String[] args) throws InterruptedException { 33 int start = 0; 34 ExecutorService es = Executors.newCachedThreadPool(); 35 while (getExchFlag() == 1 || start == 1){ 36 setExchFlag(0); 37 //偶數的數組長度,當start=1時,只有length/2-1 個線程 38 CountDownLatch latch = new CountDownLatch(arr.length/2-(arr.length%2 == 0?start:0)); 39 for (int i = start;i < arr.length - 1;i += 2){ 40 es.submit(new OddEvenSortTask(i,latch)); 41 } 42 //等待所有線程結束 43 latch.await(); 44 if (start == 0){ 45 start = 1; 46 }else { 47 start = 0; 48 } 49 } 50 for (int temp:arr){ 51 System.out.println(temp); 52 } 53 es.shutdown(); 54 } 55 }?上述代碼第11行,定義了奇偶排序的任務類。該任務的主要工作是進行數據的比較和交換(第22~26行)。并行排序的主體在main方法中,使用了countDownLatch來記錄線程數量,對于每一次的迭代,使用單獨的線程對每一次元素比較和交換操作,在下一次的迭代開始之前,必須等上一次的迭代必須完成。
?希爾排序:改進的插入排序
插入排序也是一種很常見的排序算法。它的基本思想:一個未排序的數組(當然也可以是鏈表)可以分為兩個部分,前半部分是已經排序的,后半部分是未排序的。在進行排序時,只需要在未排序的部分中選擇一個元素,將其插入前面有序的數組中即可。最終,未排序的部分會越來越少,直到為0,那么排序就完成了。初始時,可以假設已排序部分就是第一個元素。
插入排序串行排序如下:
1 public static int[] insertSort(int[] arr){ 2 int length = arr.length; 3 int i,j,key; 4 for (i = 1;i < length;i++){ 5 //key為要準備插入的元素 6 key = arr[i]; 7 j = i - 1; 8 while (j >= 0 && arr[j] > key){ 9 arr[j+1] = arr[j]; 10 j--; 11 } 12 //找到合適的位置插入key 13 arr[j+1] = key; 14 } 15 return arr; 16 }上述代碼第6行,提取準備插入的元素(也就是未排序序列中的第一個元素)。接著,在已排序的序列中找到這個元素的插入位置(第8~10行),并進行插入(第13行)即可。
簡單的插入排序是很難進行并行化的。因為這一次的數據插入依賴于上一次得到的有序序列,因此多個步驟之間無法并行。為此,我們可以對插入排序進行擴展,就是希爾排序。
希爾排序將整個數組根據間隔h分割為若干個數組。子數組相互穿插在一起,每一次排序時,分別對每一個子數組進行排序。如下圖所示(借大神的圖侵刪):
從上圖可以看出,每一組排序完成后,可以遞減h的值,進行下輪更精細的排序,知道h=1,此時,等價于一次插入排序。
希爾排序的一個主要優點是,即使最小的一個元素在數組的末尾,由于每次元素的移動都以h為間隔進行,因此數組末尾的小元素可以在很少的交換次數下,就被置換到最接近元素最終位置的地方。
下面是希爾排序的串行實現:
1 public static int[] shellSort(int[] arr){ 2 //計算出最大的h值 3 int h = 1; 4 while (h <= arr.length/3){ 5 h = h*3 + 1; 6 } 7 while (h > 0){ 8 for (int i = h;i < arr.length;i++){ 9 if (arr[i] < arr[i - h]){ 10 int temp = arr[i]; 11 int j = i - h; 12 while (j >= 0 && arr[j] > temp){ 13 arr[j + h] = arr[j]; 14 j -= h; 15 } 16 arr[j + h] = temp; 17 } 18 } 19 //計算出下一個h值 20 h = (h - 1)/3; 21 } 22 return arr; 23 }上述代碼的4~6行,計算出一個合適的h值,接著進行正式的希爾排序。第8行代碼的for循環進行間隔為h的插入排序,每次排序結束后,遞減h的值(第20行),直到h=1,退化為插入排序。
由于希爾排序每次都針對不同的子數組進行排序,各個子數組之間是完全獨立的,因此,是可以改寫為并行程序的:
1 public class CurrentShellSort { 2 static int[] a = {2,3,1,45,53,3,55,4,65,765,7,7,89687,6,89,69,4354,89,99}; 3 static ExecutorService pool = Executors.newCachedThreadPool(); 4 5 public static class ShellSortTask implements Runnable { 6 7 int x = 0; 8 int h = 0; 9 CountDownLatch l; 10 11 public ShellSortTask(int x, int h, CountDownLatch latch) { 12 this.x = x; 13 this.h = h; 14 this.l = latch; 15 } 16 17 public void run() { 18 int i, j, key; 19 for (i = x + h; i < a.length; i = i + h) { 20 if (a[i] < a[i - h]) { 21 j = i - h; 22 key = a[i]; 23 while (j >= 0 && a[j] > key) { 24 a[j + h] = a[j]; 25 j -= h; 26 } 27 a[j + h] = key; 28 } 29 } 30 l.countDown(); 31 } 32 } 33 34 public static void pShellSort(int[] arr) throws InterruptedException { 35 // 計算出最大的n值 36 int h = 1; 37 CountDownLatch lathc = null; 38 while (h <= arr.length / 3) { 39 h = h * 3 + 1; 40 } 41 while (h > 0) { 42 System.out.println("h=" + h); 43 lathc = new CountDownLatch(h); 44 for (int x = 0; x < h; x++) { 45 pool.submit(new ShellSortTask(x, h, lathc)); 46 } 47 lathc.await(); 48 System.out.println(Arrays.toString(arr)); 49 // 計算下一個h值 50 h = (h - 1) / 3; 51 } 52 pool.shutdown(); 53 } 54 //測試 55 public static void main(String[] args) throws InterruptedException { 56 pShellSort(a); 57 } 58 }?輸出結果:
h=13 [2, 3, 1, 45, 53, 3, 55, 4, 65, 765, 7, 7, 89687, 6, 89, 69, 4354, 89, 99] h=4 [2, 3, 1, 4, 53, 3, 7, 7, 65, 6, 55, 45, 4354, 89, 89, 69, 89687, 765, 99] h=1 [1, 2, 3, 3, 4, 6, 7, 7, 45, 53, 55, 65, 69, 89, 89, 99, 765, 4354, 89687]?
參考:《Java高并發程序設計》 葛一鳴 郭超 編著:
轉載于:https://www.cnblogs.com/Joe-Go/p/9828848.html
總結
- 上一篇: MVC架构中的Repository模式
- 下一篇: 错误集(二)