在網上找了不少的資料,不夠全面也不夠清楚,這里組合和修改一下兩份資料,將外部排序中過程詳細的介紹下
參考網址(http://www.cnblogs.com/songQQ/archive/2011/02/22/1961071.html and http://chenkegarfield.blog.163.com/blog/static/62330008200910249526638/)
一、定義問題
外部排序指的是大文件的排序,即待排序的記錄存儲在外存儲器上,待排序的文件無法一次裝入內存,需要在內存和外部存儲器之間進行多次數據交換,以達到排序整個文件的目的。外部排序最常用的算法是多路歸並排序,即將原文件分解成多個能夠一次性裝入內存的部分,分別把每一部分調入內存完成排序。然后,對已經排序的子文件進行多路歸並排序。
二、處理過程
(1)按可用內存的大小,把外存上含有n個記錄的文件分成若干個長度為L的子文件,把這些子文件依次讀入內存,並利用有效的內部排序方法對它們進行排序,再將排序后得到的有序子文件重新寫入外存;
(2)對這些有序子文件逐趟歸並,使其逐漸由小到大,直至得到整個有序文件為止。
先從一個例子來看外排序中的歸並是如何進行的?
假設有一個含10000 個記錄的文件,首先通過10 次內部排序得到10 個初始歸並段R1~R10 ,其中每一段都含1000 個記錄。然后對它們作如圖10.11 所示的兩兩歸並,直至得到一個有序文件為止 如下圖

三 、多路歸並排序算法以及敗者樹
多路歸並排序算法在常見數據結構書中都有涉及。從2路到多路(k路),增大k可以減少外存信息讀寫時間,但k個歸並段中選取最小的記錄需要比較k-1次,為得到u個記錄的一個有序段共需要(u-1)(k-1)次,若歸並趟數為s次,那么對n個記錄的文件進行外排時,內部歸並過程中進行的總的比較次數為s(n-1)(k-1),也即(向上取整)(logkm)(k-1)(n-1)=(向上取整)(log2m/log2k)(k-1)(n-1),而(k-1)/log2k隨k增而增因此內部歸並時間隨k增長而增長了,抵消了外存讀寫減少的時間,這樣做不行,由此引出了“敗者樹”tree of loser的使用。在內部歸並過程中利用敗者樹將k個歸並段中選取最小記錄比較的次數降為(向上取整)(log2k)次使總比較次數為(向上取整)(log2m)(n-1),與k無關。
敗者樹是完全二叉樹,因此數據結構可以采用一維數組。其元素個數為k個葉子結點、k-1個比較結點、1個冠軍結點共2k個。ls[0]為冠軍結點,ls[1]--ls[k-1]為比較結點,ls[k]--ls[2k-1]為葉子結點(同時用另外一個指針索引b[0]--b[k-1]指向)。另外bk為一個附加的輔助空間,不屬於敗者樹,初始化時存着MINKEY的值。
多路歸並排序算法的過程大致為:
1):首先將k個歸並段中的首元素關鍵字依次存入b[0]--b[k-1]的葉子結點空間里,然后調用CreateLoserTree創建敗者樹,創建完畢之后最小的關鍵字下標(即所在歸並段的序號)便被存入ls[0]中。然后不斷循環:
2)把ls[0]所存最小關鍵字來自於哪個歸並段的序號得到為q,將該歸並段的首元素輸出到有序歸並段里,然后把下一個元素關鍵字放入上一個元素本來所在的葉子結點b[q]中,調用Adjust順着b[q]這個葉子結點往上調整敗者樹直到新的最小的關鍵字被選出來,其下標同樣存在ls[0]中。循環這個操作過程直至所有元素被寫到有序歸並段里。
四、偽代碼:
void Adjust(LoserTree &ls, int s)
/*從葉子結點b[s]到根結點的父結點ls[0]調整敗者樹*/
{ int t, temp;
t=(s+K)/2; /*t為b[s]的父結點在敗者樹中的下標,K是歸並段的個數*/
while(t>0) /*若沒有到達樹根,則繼續*/
{ if(b[s]>b[ls[t]]) /*與父結點指示的數據進行比較*/
{ /*ls[t]記錄敗者所在的段號,s指示新的勝者,勝者將去參加更上一層的比較*/
temp=s;
s=ls[t];
ls[t]=temp;
}
t=t/2; /*向樹根退一層,找到父結點*/
}
ls[0]=s; /*ls[0]記錄本趟最小關鍵字所在的段號*/
}
void K_merge( int ls[K])
/*ls[0]~ls[k-1]是敗者樹的內部比較結點。b[0]~b[k-1]分別存儲k個初始歸並段的當前記錄*/
/*函數Get_next(i)用於從第i個歸並段讀取並返回當前記錄*/
{ int b[K+1),i,q;
for(i=0; i<K;i++)
{ b[i]=Get_next(i); /*分別讀取K個歸並段的第一個關鍵字*/ }
b[K]=MINKEY; /*創建敗者樹*/
for(i=0; i<K ; i++) /*設置ls中的敗者初值*/
ls[i]=K;
for(i=K-1 ; i>=0 ; i--) /*依次從b[K-1]……b[0]出發調整敗者*/
Adjust(ls , i); /*敗者樹創建完畢,最小關鍵字序號存入ls[0]
while(b[ls[0]] !=MAXKEY )
{ q=ls[0]; /*q為當前最小關鍵字所在的歸並段*/
prinftf("%d",b[q]);
b[q]=Get_next(q);
Adjust(ls,q); /*q為調整敗者樹后,選擇新的最小關鍵字*/
}
}
如下圖,一個詳細的過程。2個子結點比較后的敗者放入它們的父結點,而勝者送到它們父結點的父節點去再作比較,這才是敗者樹。b[0]放的是最終的勝者。

五、 小結
最后,對使用多路歸並排序來進行外部排序的過程大致描述一下:根據有限的內存資源將大文件分為L個段,然后依次將這L個段讀入內存並利用高效的內部排序算法對每個段進行排序,排序后的結果即為初始有序歸並段直接寫入外存文件。內部排序時要選擇合適的排序算法,並且要考慮到內部排序需要的輔助空間以及有限的內存空間來決定究竟要把大文件分為幾個段。接下來選擇合適的路數k對這L個歸並段進行多路歸並排序,每一趟歸並使k個歸並段變為1個較大歸並段寫入文件,反復幾趟歸並后得到整個有序的文件。在多路歸並過程中,內存空間只需要維護一個大小為2k的敗者樹,數據取、放都是對應外存的讀寫,這樣的話一次把一大塊數據讀入內存、把內存中排好的一大塊數據寫入文件比較省時,不知這個需要程序員編程安排還是OS能通過虛擬頁面文件直接幫忙做到。找出計算機組成原理的課本回顧下發現,自認為用虛擬頁面文件管理解決這個問題完全是風馬牛不相及的。段頁式虛擬存儲是將程序的邏輯空間以段頁式來管理,而要排序的文件不屬於程序本身的邏輯空間。實際上,這個問題應該從磁盤本身提供的高速緩存方面來考慮。現在磁盤一般都有幾M到十幾M的高速緩存,利用數據訪問的空間局部性和時間局部性規則,使用預讀策略,一次性將一塊數據讀入高速緩存,再次讀寫時則先檢查cache中是否能夠命中,如能命中則不需去盤片上讀。若cache空間不足以提高讀寫速率,則需要程序員編寫程序將大塊數據讀入寫出。
