二路和多路歸併排序
先通俗地說一下歸併排序。打個比方,桌子上有兩堆撲克牌,每堆撲克牌都是已經排好序的(不妨假設是升序)。我們從兩堆撲克牌中各取最上面的一張(取的兩張牌顯然分別是兩堆牌中最小的 ),比較大小,把最小的一張拿在手裡,然後從“勝出”的牌所在的牌堆中再取一張。重複上面的過程。。。如果有一個牌堆被取完了,那麼就把另一堆一股腦搬過來。這時候,你手中的牌就是升序的。換句話說,通過上面的演算法,你成功把兩個各為升序的牌堆合成了一個升序的牌堆——二合一。可以證明,該演算法的複雜度是O(nlgn)。
上面所說的其實就是一個二路歸併排序。顯然,如果把k堆牌合成一堆,那就是k路歸併排序。
考慮這樣一個問題,我這裡有k個檔案,裡面各自儲存著已經排好序(不妨假設是升序)的,並且序相同的(要麼都是升序要麼都是降序)的整數。檔案裡的資料量很大,全部一股腦的裝到記憶體裡然後排序在輸出是不現實的。這時候,就需要進行外排序,這時資料不會一次性全部進入記憶體。
在看接下來的內容之前,強烈建議讀者在紙上演示一下,更能加深理解。
我們可以先建立一個敗者樹。所謂敗者,就是在兩兩比較中不被選中的數,比方說我想要排成升序,那麼兩數之間較大的數就不會被選中,那它就是敗者。b[i]表示從第i個檔案中按順序取出的數(類比一下牌堆。。),這作為敗者樹的葉節點。然後,兩子節點的父節點儲存的是敗者的所在檔案編號,勝者就晉級去更高一層比賽,那麼冠軍就應該是我們要的數(通過b[LoserTree[0]]得到)。接下來,每個節點都記錄了每場比賽的敗者,這些資訊將成為優化的關鍵。我們從勝者所在的檔案裡再取一個數,儲存在b[s]中,然後我們只需要更新從葉節點s一路更新到根節點,不需要理會其他節點。因為其他節點所代表的比賽結果和決定接下來的勝者沒有關係。
這樣一來,我們在O(k)的時間內決定第一個勝者,然後不斷地用O(lgk)的時間決定下一個勝者。
為什麼我們最後要把FLAG對應的哨兵值也輸出呢?考慮這一種情況:待排的檔案數目也相當多,也許有上百萬,那麼我們不能這些檔案一次排完,要分多次執行程式。那麼這次的輸出可能成為下次的輸入,輸入檔案最後當然要有FLAG啦。。/* 以下程式執行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的值要調整為一個足夠小的數,然後KEY(第一輪比賽中註定的勝者)要調整為一個足夠大的數。比賽規則要改:b[s]>b[LowerTree[0]]改為b[s]<b[LowerTree[0]]。子檔案的順序也要是降序。這樣就可以進行輸出為降序的k路歸併排序了。