個人認為,外部排序是我們在學習過程中接觸到的乙個比較重要的演算法,它既包含了基本的排序演算法,又考察了對檔案io
以及記憶體的理解,還展示了最基本的程式優化思想,可以說能夠寫好乙個外部排序,就說明基本的程式設計能力已經過關了。本文將對整個外部排序的過程進行詳細的分析,並介紹兩個經典演算法,最後附上完整的程式**。
1. 問題描述
由於在現實環境中,有時候需要對乙個非常大的檔案進行排序,而計算機記憶體是有限的,當資料無法完全存入記憶體時,則無法使用正常的排序演算法一次完成排序,而必須利用磁碟空間的輔助進行外部排序,即利用有限的記憶體每次讀入部分資料排序後得到乙個順串後暫時放到磁碟,最後將多個順串進行歸併直到最終完成排序,因為在歸併過程中,只需從每個順串中取出最小的乙個資料進行比較即可,而不需要整個順串都在記憶體中,所以解決了記憶體空間不足的問題。那麼,原問題就可以分解成兩個子問題,乙個是如何生成順串,另乙個是如何將順串進行歸併。
首先,從效能上考慮,由於磁碟io
的速度要比記憶體讀取的速度慢上幾十萬倍,所以必須儘量減少磁碟io
次數。再考慮歸併過程中,假設有8
個順串,每次歸併兩個,則第一輪歸併後變成4
個,第二輪變成2
個,直到第三輪完成歸併,在這個過程中對每個資料進行了3
次io,而如果一次可以歸併8
個順串,則只需一輪即可完成,即對每個資料只進行了1
次io。所以,為了提高程式效率,則需要儘量減少歸併過程中的輪數,要實現這點,可以從兩個角度入手,一是減少順串數量(即令每個順串的長度盡可能長),二是使用多路歸併,針對這兩點,本文將通過選擇置換演算法和敗者樹來實現。
2. 選擇置換
選擇置換演算法用於生成順串,在有限的記憶體限制下,它可以生成大概兩倍於記憶體大小的順串,其演算法步驟如下:
假設記憶體中只有乙個能容納n
個整型的陣列
(1)首先從輸入檔案中讀取n
個數字將陣列填滿
(2)使用陣列中現有資料構建乙個最小堆
(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
,則x,y
滿足s+x - y =s
,即x = y
,又因為在鏟雪車開一圈的過程中,鏟掉的雪為原有的積雪加上降雪的一半,所以y = s + x/2,
所以y = 2s
,即鏟雪車鏟掉了2s
的雪量。而在選擇置換中,陣列的大小就相當於s
,剷雪量就相當於輸出順串的大小,即2
倍陣列大。這個證明雖然有點抽象,但實際中只要輸入檔案中的數字是隨機分布的,得到的順串大小的確大概是所用陣列大小的兩倍。
3. 敗者樹
在多路歸併的過程中,如果有k
個順串,每次有k
個候選值,要找出其中的最小值,普通的做法需要進行k-1
次比較,而使用敗者樹,則只需要o
(logk)
次比較,其原理就像我們平常的分組比賽,乙個參賽者在小組出線之後,只需要與其他小組出線的參賽者比賽即可決出最後的冠軍(最值),而不需要和其他所有參賽者都比一遍。
下圖為乙個5
路歸併過程中構建的敗者樹,因為要按從小到大排序,所以在每次比較中,小的為勝,大的為敗。陣列b[0..4]
儲存從順串中讀入的數,l[0]
儲存最終的勝者(最小值)的位置,l[1..3]
儲存中間各比賽敗者的位置。
當前最小值為5
(b[4]
), 將5
輸出後,若新讀入的資料為11
,則先與該組之前的敗者b[3]
比較,勝後再與b[0]
比較,結果為敗,則將下標4
記錄於l[2]
處,令勝者b[0]
繼續向上與b[2]
比較,勝出後將將下標記錄到l[0],
經過3次比較後得出新的最小值為10(b[0]),
如下圖所示
關於敗者樹的構建和每次讀入新值後的調整步驟見下面**。
void createlosertree(run **runs, int n)
for(int i = n-1; i >= 0; i--)
}void adjust(run **runs, int n, int s)
t /= 2;
}ls[0] = s;
}
首先,勝者樹會是乙個這樣的形式:
和我們前面討論的有點不同,這裡幾乎堆的每個葉節點對應乙個輸入的序列。我們讓他們構成乙個完全二叉樹。以上圖為例,我們進行一輪勝者的選擇之後,堆結構則如下:
我們可以看到,最終在堆頂的那個元素是最小的,而且有一條路徑從葉節點到堆的根。如果我們把最小的這個元素處理完之後該怎麼調整呢?下圖可以說明這個問題:
我們發現這個問題是通過在原來序列裡取後續的元素,然後像勝者樹調整一樣向上,符合條件的元素放上面,然後一直比較到根。這樣就找到了下乙個最小的元素。這樣一直迴圈下去。如果乙個序列處理完了我們可以採用在末尾增加乙個無窮大的值。
總的來說,這個方法和普通的最小堆調整差不多,就是調整的方式不一樣而已。我們也可以很容易得出物件的實現,這裡就不再貼詳細的實現**了。
選擇置換 敗者樹搞定外部排序
個人認為,外部排序是我們在學習過程中接觸到的乙個比較重要的演算法,它既包含了基本的排序演算法,又考察了對檔案io 以及記憶體的理解,還展示了最基本的程式優化思想,可以說能夠寫好乙個外部排序,就說明基本的程式設計能力已經過關了。本文將對整個外部排序的過程進行詳細的分析,並介紹兩個經典演算法,最後附上完...
外部排序 選擇置換 敗者樹
1.問題描述 由於在現實環境中,有時候需要對乙個非常大的檔案進行排序,而計算機記憶體是有限的,當資料無法完全存入記憶體時,則無法使用正常的排序演算法一次完成排序,而必須利用磁碟空間的輔助進行外部排序,即利用有限的記憶體每次讀入部分資料排序後得到乙個順串後暫時放到磁碟,最後將多個順串進行歸併直到最終完...
C 外部排序(選擇置換 敗者樹)
參考部落格 單個順串的生成採用選擇置換演算法。多路歸併採用敗者樹。include include include include includeusing namespace std define max int 0x7fffffff define min int 1 const int kmaxs...