選擇置換+敗者樹搞定外部排序

我的認爲,外部排序是咱們在學習過程當中接觸到的一個比較重要的算法,它既包含了基本的排序算法,又考察了對文件IO以及內存的理解,還展現了最基本的程序優化思想,能夠說可以寫好一個外部排序,就說明基本的編程能力已通過關了。本文將對整個外部排序的過程進行詳細的分析,並介紹兩個經典算法,最後附上完整的程序代碼。算法

1. 問題描述編程

  因爲在現實環境中,有時候須要對一個很是大的文件進行排序,而計算機內存是有限的,當數據沒法徹底存入內存時,則沒法使用正常的排序算法一次完成排序,而必須利用磁盤空間的輔助進行外部排序,即利用有限的內存每次讀入部分數據排序後獲得一個順串後暫時放到磁盤,最後將多個順串進行歸併直到最終完成排序,由於在歸併過程當中,只需從每一個順串中取出最小的一個數據進行比較便可,而不須要整個順串都在內存中,因此解決了內存空間不足的問題。那麼,原問題就能夠分解成兩個子問題,一個是如何生成順串,另外一個是如何將順串進行歸併。數組

  首先,從性能上考慮,因爲磁盤IO的速度要比內存讀取的速度慢上幾十萬倍,因此必須儘可能減小磁盤IO次數。再考慮歸併過程當中,假設有8個順串,每次歸併兩個,則第一輪歸併後變成4個,第二輪變成2個,直到第三輪完成歸併,在這個過程當中對每一個數據進行了3IO,而若是一次能夠歸併8個順串,則只需一輪便可完成,即對每一個數據只進行了1IO。因此,爲了提升程序效率,則須要儘可能減小歸併過程當中的輪數,要實現這點,能夠從兩個角度入手,一是減小順串數量(即令每一個順串的長度儘量長),二是使用多路歸併,針對這兩點,本文將經過選擇置換算法和敗者樹來實現。性能

 2. 選擇置換學習

  選擇置換算法用於生成順串,在有限的內存限制下,它能夠生成大概兩倍於內存大小的順串,其算法步驟以下:優化

  假設內存中只有一個能容納N個整型的數組spa

  (1)首先從輸入文件中讀取N個數字將數組填滿  code

  (2)使用數組中現有數據構建一個最小堆blog

  (3)重複如下步驟直到堆的大小變爲0排序

    a. 把根結點的數字A(即當前數組中的最小值)輸出

    b. 從輸入文件中再讀出一個數字B,若R比剛輸出的數字A 大,則將B放到堆的根節點處,若B不比A大,則將堆的最後一個元素移到根結點,將B放到堆的最後一個位置,並把堆的大小縮減1(即新讀入的數據沒有進入堆中)

    c. 在根結點處調用Siftdown從新維護堆

  (4)換一個輸出文件,從新回到步驟(2

  解釋:在以上算法運行過程當中,步驟(3)每從最小堆中輸出一個最小值,就從輸入文件中再讀入一個數據,若新讀入的數比剛輸出的數大,則能夠屬於當前的順串,將其放入堆中便可,不然只能屬於下一個順串,需將其放在堆外,在運行過程當中,堆的大小逐漸縮減直到0,此時就輸出了一個順串,而數組中新的數則能夠用於構造一個新的堆,如此循環便可將原先的一個大文件轉化成一個大概2N的順串。至於爲何是2N,有一個比較抽象的類比證實:

  假設在一條環形跑道上有一輛鏟雪車在剷雪,且空中還在均勻地下着雪,那麼當鏟雪車已經沿着跑道開過一圈後,只要車速和降雪速度恆定,則跑道上的積雪量S也恆定,且車後積雪量最少,車前積雪量最多,以下圖a。在這種狀況下,設鏟雪車每開一圈的時間,降雪量爲X,車剷雪量爲Y,則XY知足S+X - Y =S,即X = Y,又由於在鏟雪車開一圈的過程當中,剷掉的雪爲原有的積雪加上降雪的一半,因此Y = S + X/2, 因此Y = 2S,即鏟雪車剷掉了2S的雪量。而在選擇置換中,數組的大小就至關於S,剷雪量就至關於輸出順串的大小,即2倍數組大。這個證實雖然有點抽象,但實際中只要輸入文件中的數字是隨機分佈的,獲得的順串大小的確大概是所用數組大小的兩倍。

3. 敗者樹

在多路歸併的過程當中,若是有K個順串,每次有K個候選值,要找出其中的最小值,普通的作法須要進行K-1次比較,而使用敗者樹,則只須要OlogK)次比較,其原理就像咱們日常的分組比賽,一個參賽者在小組出線以後,只須要與其餘小組出線的參賽者比賽便可決出最後的冠軍(最值),而不須要和其餘全部參賽者都比一遍。

下圖爲一個5路歸併過程當中構建的敗者樹,由於要按從小到大排序,因此在每次比較中,小的爲勝,大的爲敗。數組B[0..4]存儲從順串中讀入的數,L[0]存儲最終的勝者(最小值)的位置,L[1..3]存儲中間各比賽敗者的位置。

當前最小值爲5B[4]), 將5輸出後,若新讀入的數據爲11,則先與該組以前的敗者B[3]比較,勝後再與B[0]比較,結果爲敗,則將下標4記錄於L[2]處,令勝者B[0]繼續向上與B[2]比較,勝出後將將下標記錄到L[0],通過3次比較後得出新的最小值爲10(B[0]),以下圖所示

 

  關於敗者樹的構建和每次讀入新值後的調整步驟見下面代碼。

 1 void CreateLoserTree(Run **runs, int n)
 2 {
 3     for(int i = 0; i < n; i++)
 4     {
 5         ls[i] = -1;
 6     }
 7     for(int i = n-1; i >= 0; i--)
 8     {
 9         Adjust(runs, n, i);
10     }
11 }
12 void Adjust(Run **runs, int n, int s)
13 {
14     //首先根據s計算出對應的ls中哪個下標
15     int t = (s + n) / 2;    
16     int tmp;
17 
18     while(t != 0)
19     {
20         if(s == -1)
21             break;
22         if(ls[t] == -1 || runs[s]->buffer[runs[s]->idx] > 
23                 runs[ls[t]]->buffer[runs[ls[t]]->idx])
24         {
25             tmp = s;
26             s = ls[t];
27             ls[t] = tmp;
28         }
29         t /= 2;
30     }
31     ls[0] = s;
32 }

 

4. 代碼解釋

附件中包含兩個程序:

generate_data.cpp 用於生成10000000個不重複的隨即數字

external_sort.cpp 用於完成外部排序

在外部排序的程序中,限制只能使用一個大小爲1000000的數組做數據存儲,用於生成順串和多路歸併的輸入緩衝區。

程序運行結果以下圖

程序源代碼下載

external_sort.zip

相關文章
相關標籤/搜索