如果數據量過大,超過最大的內存容量,那么一次性將所有數據讀入內存進行排序是不可行的。
例如,一個文件每一行存了一個整數,該文件大小為10GB,而內存大小只有512M,如何對這10GB的數據進行排序呢?
外部排序就是為了解決這種問題的。
思路:
外部排序的思路是,將超大文件分成若干部分,每一部分是可以讀入內存的,例如,將10GB的文件分為40份,則每一份就只有256M。將每一份讀入內存,用已知的方法進行排序(快拍,堆排等方式),再寫到一個文件中。這樣,我們得到了40個已序的小文件。
再采用歸並(Merge)的方式,將40個文件合並為一個大文件,則這個大文件就是我們要的結果。
難點:
歸並的方式是一個難點。最直觀地思路是兩路歸並。
將40個文件編號1-40,1和2歸並,3和4歸並...39和40歸並,生成了20個文件,再將這20個文件繼續兩路歸並。
從40個文件變成20個文件,相當於把所有10G的數據從磁盤讀出,再寫到磁盤上,從20個文件到10個文件,也相當於把10G的數據從磁盤讀出,再寫到磁盤上。這種磁盤IO操作一共要執行6次。(2^6=64>40)
再來考慮K路歸並。所謂K路歸並,就是一次比較K路數據,選出最小的。例如當K=10,則是將40個文件分成1-10,11-20,21-30,31-40。對1-10,由於已序,故只要比較出這10個文件的第一個數,看哪個最小,哪個就寫到新文件,再進行下一輪的比較。這樣,只要2次磁盤IO就可以了。
假設我們將文件分為m份,使用K路歸並,則磁盤IO的次數就是log K底m。我們當然是希望這個值越小越好。但是是不是K越大就越好呢?我們來看看算法的時間復雜度。
對於總共s個數據的K路歸並,每次需比較K-1次,才能得出結果中的一個數據,s個數據就需要比較(s-1)(K-1)次
對於總共n個數據,分為m份,使用K路歸並,總共需要比較 (log K底m) * (n-1)(K-1)= (logm/logK)*(n-1)(K-1) = logm*(n-1)*(K-1)/logK,當K增大時,(K-1)/logK是增大的,也即時間復雜度是上升的。因此要么K不能太大,要么找出一個新的方法,使得每次不用比較K-1次才得出結果中的一個數據。我們選擇后者,這種方法就是失敗樹。
失敗樹:
圓形的是失敗樹的節點,方形的是K路數據中每路數據的最小值,圖上K=4。
失敗樹用數組ls[]存儲,K路數據中沒路數據的最小值用b[]存儲。
失敗樹的性質:
失敗樹里的每個節點中所存的值是失敗者的下標,即b[]中的一個下標。例如,b[0]和b[1]比較的失敗者(我們要求出最小值,因此大的數是失敗者)是b[1],ls[2]中存的就是1。
值得注意的是,我們並不是使用失敗者去進行下一輪比較,而是用成功者進行下一輪比較。例如,ls[2]中雖然存的是1,但是參與下一輪比較的是b[0],同理,ls[3]中存的是下標3,但是參加下一輪比較的是b[2]。ls[1]中存放的是b[0]和b[2]比較所產生的失敗者下標。獲勝者的下標則存在ls[0]中,因此,b[ls[0]]是所有比較的獲勝者,即這四個數中最小的。
因此,我們將b[ls[0]]寫入新的文件,然后b[ls[0]]讀入本路的下一個數據。
讀入下一個數據之后,失敗樹的性質可能發生變化,我們要對數進行維護。
維護的過程
將新的b[ls[0]] 與失敗樹中的父節點進行比較。一直比到b[ls[1]],然后產生新的ls[0]
具體到本處,就是b[0]與b[ls[2]]比較,失敗者為b[ls[2]],因此ls[2]不變,再將b[0]與b[ls[1]]比較,失敗者為b[0],因此ls[1]更新為0,ls[0]更新為之前的ls[1]。現在新的最小值就是b[ls[0]]=b[2]=7
#include <iostream> using namespace std; #define K 4 //表示4路歸並 #define MIN INT_MIN; int b[K+1] = {5,13,7,9}; int ls[K] = {0};//記錄敗者的下標 void Adjust(int s) { int t = (s+K)/2;//t=(s+k),得到與之相連ls數組的索引 while(t>0) { if(b[s] > b[ls[t]])//父親節點 { int temp = s; //s保存獲勝者的下標 s = ls[t]; ls[t]=temp; } t=t/2; //父節點 } ls[0] = s;//將最小節點的索引存儲在ls[0] } void CreateLoser() { b[K] = MIN; int i; for(i=0;i<K;i++)ls[i]=K; for(i=K-1;i>=0;i--)Adjust(i); //加入一個基點,要進行調整 } int main() { CreateLoser(); system("pause"); return 0; }
建立失敗樹的過程用到了一個小技巧。首先假設各路的最小值都是MIN,MIN小於各路的最小值,失敗數節點里的值可以為任意值,這里統一為K。然后從i=K-1 to o,一個一個讀入b[i],並對失敗樹進行維護,當所有的b[i]都讀入后,失敗樹也就建立好了。
用失敗樹找最小值,時間復雜度為logK,不再是K-1,因此對於總共n個數據,分為m份,使用K路歸並,時間復雜度為logm * (n-1)