在網上找了不少的資料,不夠全面也不夠清楚,這裏組合和修改一下兩份資料,將外部排序中過程詳細的介紹下
參考網址(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空間不足以提高讀寫速率,則需要程序員編寫程序將大塊數據讀入寫出。