個人認為,外部排序是我們在學習過程中接觸到的乙個比較重要的演算法,它既包含了基本的排序演算法,又考察了對檔案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]),
如下圖所示
關於敗者樹的構建和每次讀入新值後的調整步驟見下面**。
14. **解釋附件中包含兩個程式:void createlosertree(run **runs, intn)2
7for(int i = n-1; i >= 0; i--)811
}12void adjust(run **runs, int n, int
s)13
29 t /= 2;30
}31 ls[0] =s;
32 }
generate_data.cpp
用於生成10000000
個不重複的隨即數字
external_sort.cpp
用於完成外部排序
在外部排序的程式中,限制只能使用乙個大小為1000000
的陣列作資料儲存,用於生成順串和多路歸併的輸入緩衝區。
程式執行結果如下圖
選擇置換 敗者樹搞定外部排序
個人認為,外部排序是我們在學習過程中接觸到的乙個比較重要的演算法,它既包含了基本的排序演算法,又考察了對檔案io 以及記憶體的理解,還展示了最基本的程式優化思想,可以說能夠寫好乙個外部排序,就說明基本的程式設計能力已經過關了。本文將對整個外部排序的過程進行詳細的分析,並介紹兩個經典演算法,最後附上完...
外部排序 選擇置換 敗者樹
1.問題描述 由於在現實環境中,有時候需要對乙個非常大的檔案進行排序,而計算機記憶體是有限的,當資料無法完全存入記憶體時,則無法使用正常的排序演算法一次完成排序,而必須利用磁碟空間的輔助進行外部排序,即利用有限的記憶體每次讀入部分資料排序後得到乙個順串後暫時放到磁碟,最後將多個順串進行歸併直到最終完...
C 外部排序(選擇置換 敗者樹)
參考部落格 單個順串的生成採用選擇置換演算法。多路歸併採用敗者樹。include include include include includeusing namespace std define max int 0x7fffffff define min int 1 const int kmaxs...