外部排序

一.外部排序定義

外部排序指的是大文件的排序,即待排序的記錄存儲在外存儲器上,待排序的文件無法一次裝入內存,需要在內存和外部存儲器之間進行多次數據交換,以達到排序整個文件的目的。外部排序最常用的算法是多路歸併排序,即將原文件分解成多個能夠一次性裝入內存的部分,分別把每一部分調入內存完成排序。然後,對已經排序的子文件進行多路歸併排序。

二.外部排序的基本思路

1)按可用內存的大小,把外存上含有n個記錄的文件分成若干個長度爲L的子文件,把這些子文件依次讀入內存,並利用有效的內部排序方法對它們進行排序,再將排序後得到的有序子文件重新寫入外存;

2)對這些有序子文件逐趟歸併,使其逐漸由小到大,直至得到整個有序文件爲止。

下面舉個栗子。
假設有一個72KB的文件,其中存儲了18K個整數,磁盤中物理塊的大小爲4KB,將文件分成18組,每組剛好4KB。

首先通過18次內部排序,把18組數據排好序,得到初始的18個歸併段R1~R18,每個歸併段有1024個整數。

然後對這18個歸併段使用4路平衡歸併排序:

第1次歸併:產生5個歸併段

R11 R12 R13 R14 R15

其中

R11是由{R1,R2,R3,R4}中的數據合併而來

R12是由{R5,R6,R7,R8}中的數據合併而來

R13是由{R9,R10,R11,R12}中的數據合併而來

R14是由{R13,R14,R15,R16}中的數據合併而來

R15是由{R17,R18}中的數據合併而來

把這5個歸併段的數據寫入5個文件:

foo_1.dat foo_2.dat foo_3.dat foo_4.dat foo_5.dat

第2次歸併:從第1次歸併產生的5個文件中讀取數據,合併,產生2個歸併段

R21 R22

其中R21是由{R11,R12,R13,R14}中的數據合併而來

其中R22是由{R15}中的數據合併而來

把這2個歸併段寫入2個文件

bar_1.dat bar_2.dat

第3次歸併:從第2次歸併產生的2個文件中讀取數據,合併,產生1個歸併段

R31

R31是由{R21,R22}中的數據合併而來

把這個文件寫入1個文件

foo_1.dat

此即爲最終排序好的文件。

三.多路歸併排序算法以及敗者樹

多路歸併排序算法在常見數據結構書中都有涉及。從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]]) { //與父結點指示的數據進行比較
			temp=s;        //ls[t]記錄敗者所在的段號,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爲調整敗者樹後,選擇新的最小關鍵字*/
	}
}

》使用敗者樹加快合併排序

關於勝者樹與敗者樹詳解傳送門

外部排序最耗時間的操作時磁盤讀寫,對於有m個初始歸併段,k路平衡的歸併排序,磁盤讀寫次數爲

|logkm|,可見增大k的值可以減少磁盤讀寫的次數,但增大k的值也會帶來負面效應,即進行k路合併

的時候會增加算法複雜度,來看一個例子。

把n個整數分成k組,每組整數都已排序好,現在要把k組數據合併成1組排好序的整數,求算法複雜度

u1: xxxxxxxx

u2: xxxxxxxx

u3: xxxxxxxx

uk: xxxxxxxx

算法的步驟是:每次從k個組中的首元素中選一個最小的數,加入到新組,這樣每次都要比較k-1次,故

算法複雜度爲O((n-1)*(k-1)),而如果使用敗者樹,可以在O(logk)的複雜度下得到最小的數,算法複雜

度將爲O((n-1)*logk), 對於外部排序這種數據量超大的排序來說,這是一個不小的提高。

關於敗者樹的創建和調整,可以參考清華大學《數據結構-C語言版》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章