1. 程式人生 > >二路和多路歸併排序

二路和多路歸併排序

先通俗地說一下歸併排序。打個比方,桌子上有兩堆撲克牌,每堆撲克牌都是已經排好序的(不妨假設是升序)。我們從兩堆撲克牌中各取最上面的一張(取的兩張牌顯然分別是兩堆牌中最小的 ),比較大小,把最小的一張拿在手裡,然後從“勝出”的牌所在的牌堆中再取一張。重複上面的過程。。。如果有一個牌堆被取完了,那麼就把另一堆一股腦搬過來。這時候,你手中的牌就是升序的。換句話說,通過上面的演算法,你成功把兩個各為升序的牌堆合成了一個升序的牌堆——二合一。可以證明,該演算法的複雜度是O(nlgn)。

上面所說的其實就是一個二路歸併排序。顯然,如果把k堆牌合成一堆,那就是k路歸併排序。

考慮這樣一個問題,我這裡有k個檔案,裡面各自儲存著已經排好序(不妨假設是升序)的,並且序相同的(要麼都是升序要麼都是降序)的整數。檔案裡的資料量很大,全部一股腦的裝到記憶體裡然後排序在輸出是不現實的。這時候,就需要進行外排序,這時資料不會一次性全部進入記憶體。

在看接下來的內容之前,強烈建議讀者在紙上演示一下,更能加深理解。

我們可以先建立一個敗者樹。所謂敗者,就是在兩兩比較中不被選中的數,比方說我想要排成升序,那麼兩數之間較大的數就不會被選中,那它就是敗者。b[i]表示從第i個檔案中按順序取出的數(類比一下牌堆。。),這作為敗者樹的葉節點。然後,兩子節點的父節點儲存的是敗者的所在檔案編號,勝者就晉級去更高一層比賽,那麼冠軍就應該是我們要的數(通過b[LoserTree[0]]得到)。接下來,每個節點都記錄了每場比賽的敗者,這些資訊將成為優化的關鍵。我們從勝者所在的檔案裡再取一個數,儲存在b[s]中,然後我們只需要更新從葉節點s一路更新到根節點,不需要理會其他節點。因為其他節點所代表的比賽結果和決定接下來的勝者沒有關係。

這樣一來,我們在O(k)的時間內決定第一個勝者,然後不斷地用O(lgk)的時間決定下一個勝者。

/*
以下程式執行k路歸併排序,最終結果為升序
*/
#include<cstdio>
#include<cstring>
#include<cstdlib>
using namespace std;
#define k 3  //歸併排序的路數
#define FLAG 100
/*
表示檔案結束的“哨兵值”,每個輸入檔案的末尾都有它,對於升序排序,他應該是比
所有待排的數都大,這樣只要還有數沒排,它總是個敗者。那如果他晉級成了冠軍呢?
*/
#define KEY -1
/*
KEY用於初始化敗者樹中的敗者,這應該是一個比待排序檔案中任何數都
小的數,保證第一輪調整能順利進行(使每個與他比較的數都成為敗者)。
由於不將其輸出,這個冠軍被“忽略”。
*/
FILE *fp[k+1];//fp[0]到fp[k-1]為輸入檔案(小牌堆),fp[k]為輸出檔案(最終的總牌堆)。
int LoserTree[k];  //敗者樹,在這裡用順序儲存結構。儲存的是每個敗者所在的檔案編號
int b[k+1];  //我們會將KEY放在b[k]中,b陣列的含義同前文
int input(int i)  //從第i個檔案中取數
{
    int val;
    fscanf(fp[i],"%d",&val);
    return val;
}
void output(int val)  //將val輸出到輸出檔案中,fp[k]是指向輸出檔案的指標
{
    fprintf(fp[k],"%d ",val);
}
void Adjust(int s) //從葉節點s向根節點一路更新,t為父節點,LoserTree[t]儲存的是敗者
{
    int i,t;
    t=(s+k)/2;  //這樣可以得到父節點
    while(t>0)
    {  /*s指示新的勝者,他也許不會是一成不變的*/
        if(b[s]>b[LoserTree[t]])
/*與父節點的比較實際上就是在與“擂主”比較(這個節點對應比賽的勝者被選走,敗者就成了擂主)
這一比有兩種情況:挑戰者大於擂主,按升序挑戰者就成了敗者,也就是這個if語句對應的情況,
s與LoserTree[t]交換,挑戰者在這個節點“待著”,原擂主成為勝者晉級(對應s),否則挑戰者晉級*/
        {
            i=s;s=LoserTree[t];LoserTree[t]=i;
        }
        t=t/2;  //這樣可以得到父節點
    }
    LoserTree[0]=s;  //這一趟比賽最後的冠軍!!
}
void CreateLoserTree()  //建立敗者樹
{
    b[k]=KEY;//不懂這一步可以翻看前文預編譯KEY的那一句
    for(int i=0;i<k;i++)
        LoserTree[i]=k;  //b[k]稱為每場比賽的擂主
    for(int i=0;i<k;i++)
        Adjust(i);   //各路英雄前來挑戰,然而勝負早已決定(不懂看前面KEY)
}
void K_Merge()  //開始k路歸併排序
{
    int p;
    for(int i=0;i<k;i++)  //從各個檔案裡取數
        b[i]=input(i);
    CreateLoserTree();  //建立敗者樹
    while(b[LoserTree[0]]!=FLAG)
//哨兵成了冠軍的話。。沒有數需要排了,只要還有數就肯定比他強
    {
        p=LoserTree[0];
        output(b[p]);   //把冠軍輸出到檔案
        b[p]=input(p);  //取出下一個數
        Adjust(p);   //一路往根節點更新
    }
    output(FLAG);   //關於這句我在文末會解釋
}
int main()
{
    char fname[k][15],fout[15];  //儲存輸入檔名和輸出檔名
    for(int i=0;i<k;i++)
    {
        printf("請輸入第%d個輸入檔名:\n",i+1);
        gets(fname[i]);
        fp[i]=fopen(fname[i],"r");  //以只讀形式開啟檔案
    }
    printf("請輸入輸出檔名:\n");
    gets(fout);
    fp[k]=fopen(fout,"w");   //以只寫方式開啟檔案
    K_Merge();         //k路歸併排序
    for(int i=0;i<=k;i++)
        fclose(fp[i]);   //別忘了關閉檔案
    return 0;
}
/*
在這裡設輸入檔名為"in1.txt","in2.txt","in3.txt",
輸出檔名為"out.txt"(當然這些都有你隨意制定)
其中的資料為:in1.txt:10 15 16 100
              in2.txt:9 18 20 100
              in3.txt:20 22 40 100
執行之後,輸出檔案中為:9 10 15 16 18 20 20 22 40 100 
*/
為什麼我們最後要把FLAG對應的哨兵值也輸出呢?考慮這一種情況:待排的檔案數目也相當多,也許有上百萬,那麼我們不能這些檔案一次排完,要分多次執行程式。那麼這次的輸出可能成為下次的輸入,輸入檔案最後當然要有FLAG啦。。

如果是降序呢?首先FLAG的值要調整為一個足夠小的數,然後KEY(第一輪比賽中註定的勝者)要調整為一個足夠大的數。比賽規則要改:b[s]>b[LowerTree[0]]改為b[s]<b[LowerTree[0]]。子檔案的順序也要是降序。這樣就可以進行輸出為降序的k路歸併排序了。