1. 程式人生 > >並行排序演算法——時間複雜度O(n)的排序法

並行排序演算法——時間複雜度O(n)的排序法

最近老師講了並行的排序演算法,讓我對這個原來不是很瞭解的排序演算法產生了濃厚的興趣。並行排序方法,是指採用平行計算的方法對一組資料進行排序,理論上是在類似內排序的環境下,採用多核並行的方法讓時間降低,排序的複雜度最好的情況下能降低至O(n)左右。

排序的實質

排序的實質是什麼?這是一個不是問題的問題。我們可以說是讓所有的數都按照一定的規則被放置,但這種說法實際上是解釋了排序的漢字含義。換句話不如說排序是:從序列中任選一對數都是有序的,那麼此序列就是已排序的

普通(序列)氣泡排序

為了滿足上述的概念,我們發現了氣泡排序法雙層迴圈,比較陣列中可取到的任意一對數字,如果不滿足要求則交換。這是一種最簡單的排序方法,理解起來很簡單,也與上述的排序實質含義很符合。但是我們在平常的程式中完全不會採用氣泡排序,這是因為氣泡排序有很多缺點:

  1. 比較次數是所有排序中最多的,必須要進行(n-1)2/2次比較,按照氣泡排序的定義,很難有優化的方法。
  2. 交換次數不固定而且很多,相比較氣泡排序的次數不固定,同樣是複雜度為O(n2)的選擇排序雖然比較次數也較多,但是可以把交換次數穩定在n次。
  3. 不能利用序列一些隱含的資訊。氣泡排序只能不斷的比較比較比較,對序列沒有記憶性,相比較氣泡排序同樣是複雜度為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時:

  1. 先進行偶數比較並交換 arr[0],arr[1]
  2. 再進行奇數比較並交換 arr[1],arr[2]
  3. 再進行偶數比較並交換 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次遍歷;而快速排序和歸併排序將資料分組遞迴計算這不適合用平行計算;堆排序先進行線性時間的建堆,再不斷調整樹結構進行排序時間較大的部分在調整樹結構,但這部分根本沒辦法用並行去優化。