外部排序總結


如果數據量過大,超過最大的內存容量,那么一次性將所有數據讀入內存進行排序是不可行的。

例如,一個文件每一行存了一個整數,該文件大小為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)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM