天天看點

常用的外部排序方法定義問題 處理過程 多路歸并排序算法以及敗者樹 勝者樹與敗者樹   敗者樹 多路平衡歸并外部排序

版權聲明:本文為部落客原創文章,未經部落客允許不得轉載。 https://blog.csdn.net/qq_34173549/article/details/81158566

定義問題

      外部排序指的是大檔案的排序,即待排序的記錄存儲在外存儲器上,待排序的檔案無法一次裝入記憶體,需要在記憶體和外部存儲器之間進行多次資料交換,以達到排序整個檔案的目的。外部排序最常用的算法是多路歸并排序,即将原檔案分解成多個能夠一次性裝入記憶體的部分,分别把每一部分調入記憶體完成排序。然後,對已經排序的子檔案進行多路歸并排序。

處理過程

  (1)按可用記憶體的大小,把外存上含有n個記錄的檔案分成若幹個長度為L的子檔案,把這些子檔案依次讀入記憶體,并利用有效的内部排序方法對它們進行排序,再将排序後得到的有序子檔案重新寫入外存;

  (2)對這些有序子檔案逐趟歸并,使其逐漸由小到大,直至得到整個有序檔案為止。

   先從一個例子來看外排序中的歸并是如何進行的?

  假設有一個含10000 個記錄的檔案,首先通過10 次内部排序得到10 個初始歸并段R1~R10 ,其中每一段都含1000 個記錄。然後對它們作如圖10.11 所示的兩兩歸并,直至得到一個有序檔案為止 如下圖

多路歸并排序算法以及敗者樹

    多路歸并排序算法在常見資料結構書中都有涉及。從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]])        /*與父結點訓示的資料進行比較*/

               {  /*ls[t]記錄敗者所在的段号,s訓示新的勝者,勝者将去參加更上一層的比較*/

                  temp=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為調整敗者樹後,選擇新的最小關鍵字*/

     }

如下圖,一個詳細的過程。2個子結點比較後的敗者放入它們的父結點,而勝者送到它們父結點的父節點去再作比較,這才是敗者樹。b[0]放的是最終的勝者。

勝者樹與敗者樹  

       勝者樹和敗者樹都是完全二叉樹,是樹形選擇排序的一種變型。每個葉子結點相當于一個選手,每個中間結點相當于一場比賽,每一層相當于一輪比賽。

      不同的是,勝者樹的中間結點記錄的是勝者的标号;而敗者樹的中間結點記錄的敗者的标号。

       勝者樹與敗者樹可以在log(n)的時間内找到最值。任何一個葉子結點的值改變後,利用中間結點的資訊,還是能夠快速地找到最值。在k路歸并排序中經常用到。

勝者樹

       勝者樹的一個優點是,如果一個選手的值改變了,可以很容易地修改這棵勝者樹。隻需要沿着從該結點到根結點的路徑修改這棵二叉樹,而不必改變其他比賽的結果。

Fig. 1

Fig.1是一個勝者樹的示例。規定數值小者勝。

1.         b3 PK b4,b3勝b4負,内部結點ls[4]的值為3;

2.         b3 PK b0,b3勝b0負,内部結點ls[2]的值為3;

3.         b1 PK b2,b1勝b2負,内部結點ls[3]的值為1;

4.         b3 PK b1,b3勝b1負,内部結點ls[1]的值為3。.

當Fig. 1中葉子結點b3的值變為11時,重構的勝者樹如Fig. 2所示。

2.         b3 PK b0,b0勝b3負,内部結點ls[2]的值為0;

4.         b0 PK b1,b1勝b0負,内部結點ls[1]的值為1。.

Fig. 2

敗者樹

       敗者樹是勝者樹的一種變體。在敗者樹中,用父結點記錄其左右子結點進行比賽的敗者,而讓勝者參加下一輪的比賽。敗者樹的根結點記錄的是敗者,需要加一個結點來記錄整個比賽的勝利者。采用敗者樹可以簡化重構的過程。

Fig. 3

Fig. 3是一棵敗者樹。規定數大者敗。

1.         b3 PK b4,b3勝b4負,内部結點ls[4]的值為4;

2.         b3 PK b0,b3勝b0負,内部結點ls[2]的值為0;

3.         b1 PK b2,b1勝b2負,内部結點ls[3]的值為2;

4.         b3 PK b1,b3勝b1負,内部結點ls[1]的值為1;

5.         在根結點ls[1]上又加了一個結點ls[0]=3,記錄的最後的勝者。

敗者樹重構過程如下:

·            将新進入選擇樹的結點與其父結點進行比賽:将敗者存放在父結點中;而勝者再與上一級的父結點比較。

·            比賽沿着到根結點的路徑不斷進行,直到ls[1]處。把敗者存放在結點ls[1]中,勝者存放在ls[0]中。

Fig. 4

       Fig. 4是當b3變為13時,敗者樹的重構圖。

       注意,敗者樹的重構跟勝者樹是不一樣的,敗者樹的重構隻需要與其父結點比較。對照Fig. 3來看,b3與結點ls[4]的原值比較,ls[4]中存放的原值是結點4,即b3與b4比較,b3負b4勝,則修改ls[4]的值為結點3。同理,以此類推,沿着根結點不斷比賽,直至結束。

        由上可知,敗者樹簡化了重構。敗者樹的重構隻是與該結點的父結點的記錄有關,而勝者樹的重構還與該結點的兄弟結點有關。

敗者樹 多路平衡歸并外部排序

外部排序的基本思路

假設有一個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

此即為最終排序好的檔案。

使用敗者樹加快合并排序

外部排序最耗時間的操作時磁盤讀寫,對于有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語言版》

産生二進制測試資料

打開Linux終端,輸入指令

dd if=/dev/urandom of=random.dat bs=1M count=512

 這樣在目前目錄下産生一個512M大的二進制檔案,檔案内的資料是随機的,讀取檔案,每4個位元組

看成1個整數,相當于得到128M個随機整數。

程式實作

[cpp] 

view plain copy
  1. #include <assert.h>  
  2. #include <fcntl.h>  
  3. #include <stdio.h>  
  4. #include <stdlib.h>  
  5. #include <string.h>  
  6. #include <unistd.h>  
  7. #include <sys/time.h>  
  8. #include <sys/types.h>  
  9. #include <sys/stat.h>  
  10. #define MAX_INT ~(1<<31)  
  11. #define MIN_INT 1<<31  
  12. //#define DEBUG  
  13. #ifdef DEBUG  
  14. #define debug(...) debug( __VA_ARGS__)   
  15. #else  
  16. #define debug(...)  
  17. #endif  
  18. #define MAX_WAYS 100  
  19. typedef struct run_t {  
  20.     int *buf;       /* 輸入緩沖區 */  
  21.     int length;     /* 緩沖區目前有多少個數 */  
  22.     int offset;     /* 緩沖區讀到了檔案的哪個位置 */  
  23.     int idx;        /* 緩沖區的指針 */  
  24. } run_t;  
  25. static unsigned int K;              /* K路合并 */  
  26. static unsigned int BUF_PAGES;      /* 緩沖區有多少個page */  
  27. static unsigned int PAGE_SIZE;      /* page的大小 */  
  28. static unsigned int BUF_SIZE;       /* 緩沖區的大小, BUF_SIZE = BUF_PAGES*PAGE_SIZE */  
  29. static int *buffer;                 /* 輸出緩沖區 */  
  30. static char input_prefix[] = "foo_";  
  31. static char output_prefix[] = "bar_";  
  32. static int ls[MAX_WAYS];            /* loser tree */  
  33. void swap(int *p, int *q);  
  34. int partition(int *a, int s, int t);  
  35. void quick_sort(int *a, int s, int t);  
  36. void adjust(run_t ** runs, int n, int s);  
  37. void create_loser_tree(run_t **runs, int n);  
  38. long get_time_usecs();  
  39. void k_merge(run_t** runs, char* input_prefix, int num_runs, int base, int n_merge);  
  40. void usage();  
  41. int main(int argc, char **argv)  
  42. {  
  43.     char                filename[100];  
  44.     unsigned int    data_size;  
  45.     unsigned int    num_runs;               /* 這輪疊代時有多少個歸并段 */  
  46.     unsigned int    num_merges;             /* 這輪疊代後産生多少個歸并段 num_merges = num_runs/K */  
  47.     unsigned int    run_length;             /* 歸并段的長度,指數級增長 */  
  48.     unsigned int    num_runs_in_merge;      /* 一般每個merge由K個runs合并而來,但最後一個merge可能少于K個runs */  
  49.     int                 fd, rv, i, j, bytes;  
  50.     struct stat         sbuf;  
  51.     if (argc != 3) {  
  52.         usage();  
  53.         return 0;  
  54.     }  
  55.     long start_usecs = get_time_usecs();  
  56.     strcpy(filename, argv[1]);  
  57.     fd = open(filename, O_RDONLY);  
  58.     if (fd < 0) {  
  59.         printf("can't open file %s\n", filename);  
  60.         exit(0);  
  61.     rv = fstat(fd, &sbuf);  
  62.     data_size = sbuf.st_size;  
  63.     K = atoi(argv[2]);  
  64.     PAGE_SIZE = 4096;                           /* page = 4KB */  
  65.     BUF_PAGES = 32;  
  66.     BUF_SIZE = PAGE_SIZE*BUF_PAGES;  
  67.     num_runs = data_size / PAGE_SIZE;           /* 初始時的歸并段數量,每個歸并段有4096 byte, 即1024個整數 */  
  68.     buffer = (int *)malloc(BUF_SIZE);  
  69.     run_length = 1;  
  70.     run_t **runs = (run_t **)malloc(sizeof(run_t *)*(K+1));  
  71.     for (i = 0; i < K; i++) {  
  72.         runs[i] = (run_t *)malloc(sizeof(run_t));  
  73.         runs[i]->buf = (int *)calloc(1, BUF_SIZE+4);  
  74.     while (num_runs > 1) {  
  75.         num_merges = num_runs / K;  
  76.         int left_runs = num_runs % K;  
  77.         if(left_runs > 0) num_merges++;  
  78.         for (i = 0; i < num_merges; i++) {  
  79.             num_runs_in_merge = K;  
  80.             if ((i+1) == num_merges && left_runs > 0) {  
  81.                 num_runs_in_merge = left_runs;  
  82.             }  
  83.             int base = 0;  
  84.             printf("Merge %d of %d,%d ways\n", i, num_merges, num_runs_in_merge);  
  85.             for (j = 0; j < num_runs_in_merge; j++) {  
  86.                 if (run_length == 1) {  
  87.                     base = 1;  
  88.                     bytes = read(fd, runs[j]->buf, PAGE_SIZE);  
  89.                     runs[j]->length = bytes/sizeof(int);  
  90.                     quick_sort(runs[j]->buf, 0, runs[j]->length-1);  
  91.                 } else {  
  92.                     snprintf(filename, 20, "%s%d.dat", input_prefix, i*K+j);  
  93.                     int infd = open(filename, O_RDONLY);  
  94.                     bytes = read(infd, runs[j]->buf, BUF_SIZE);  
  95.                     close(infd);      
  96.                 }  
  97.                 runs[j]->idx = 0;  
  98.                 runs[j]->offset = bytes;  
  99.             k_merge(runs, input_prefix, num_runs_in_merge, base, i);  
  100.         }  
  101.         strcpy(filename, output_prefix);  
  102.         strcpy(output_prefix, input_prefix);  
  103.         strcpy(input_prefix, filename);  
  104.         run_length *= K;  
  105.         num_runs = num_merges;  
  106.         free(runs[i]->buf);  
  107.         free(runs[i]);  
  108.     free(runs);  
  109.     free(buffer);  
  110.     close(fd);  
  111.     long end_usecs = get_time_usecs();  
  112.     double secs = (double)(end_usecs - start_usecs) / (double)1000000;  
  113.     printf("Sorting took %.02f seconds.\n", secs);  
  114.     printf("sorting result saved in %s%d.dat.\n", input_prefix, 0);  
  115.     return 0;  
  116. }  
  117. void k_merge(run_t** runs, char* input_prefix, int num_runs, int base, int n_merge)  
  118.     int bp, bytes, output_fd;  
  119.     int live_runs = num_runs;  
  120.     run_t *mr;  
  121.     char filename[20];  
  122.     bp = 0;  
  123.     create_loser_tree(runs, num_runs);  
  124.     snprintf(filename, 100, "%s%d.dat", output_prefix, n_merge);  
  125.     output_fd = open(filename, O_CREAT|O_WRONLY|O_TRUNC,   
  126.             S_IRWXU|S_IRWXG);  
  127.     if (output_fd < 0) {  
  128.         printf("create file %s fail\n", filename);  
  129.     while (live_runs > 0) {  
  130.         mr = runs[ls[0]];  
  131.         buffer[bp++] = mr->buf[mr->idx++];  
  132.         // 輸出緩沖區已滿  
  133.         if (bp*4 == BUF_SIZE) {  
  134.             bytes = write(output_fd, buffer, BUF_SIZE);  
  135.             bp = 0;  
  136.         // mr的輸入緩沖區用完  
  137.         if (mr->idx == mr->length) {  
  138.             snprintf(filename, 20, "%s%d.dat", input_prefix, ls[0]+n_merge*K);  
  139.             if (base) {  
  140.                 mr->buf[mr->idx] = MAX_INT;  
  141.                 live_runs--;  
  142.             } else {  
  143.                 int fd = open(filename, O_RDONLY);  
  144.                 lseek(fd, mr->offset, SEEK_SET);  
  145.                 bytes = read(fd, mr->buf, BUF_SIZE);  
  146.                 close(fd);  
  147.                 if (bytes == 0) {  
  148.                     mr->buf[mr->idx] = MAX_INT;  
  149.                     live_runs--;  
  150.                 else {  
  151.                     mr->length = bytes/sizeof(int);  
  152.                     mr->offset += bytes;  
  153.                     mr->idx = 0;  
  154.         adjust(runs, num_runs, ls[0]);  
  155.     bytes = write(output_fd, buffer, bp*4);  
  156.     if (bytes != bp*4) {  
  157.         printf("!!!!!! Write Error !!!!!!!!!\n");  
  158.     close(output_fd);  
  159. long get_time_usecs()  
  160.     struct timeval time;  
  161.     struct timezone tz;  
  162.     memset(&tz, '\0', sizeof(struct timezone));  
  163.     gettimeofday(&time, &tz);  
  164.     long usecs = time.tv_sec*1000000 + time.tv_usec;  
  165.     return usecs;  
  166. void swap(int *p, int *q)  
  167.     int     tmp;  
  168.     tmp = *p;  
  169.     *p = *q;  
  170.     *q = tmp;  
  171. int partition(int *a, int s, int t)  
  172.     int     i, j;   /* i用來周遊a[s]...a[t-1], j指向大于x部分的第一個元素 */  
  173.     for (i = j = s; i < t; i++) {  
  174.         if (a[i] < a[t]) {  
  175.             swap(a+i, a+j);  
  176.             j++;  
  177.     swap(a+j, a+t);  
  178.     return j;  
  179. void quick_sort(int *a, int s, int t)  
  180.     int     p;  
  181.     if (s < t) {  
  182.         p = partition(a, s, t);  
  183.         quick_sort(a, s, p-1);  
  184.         quick_sort(a, p+1, t);  
  185. void adjust(run_t ** runs, int n, int s)  
  186.     int t, tmp;  
  187.     t = (s+n)/2;  
  188.     while (t > 0) {  
  189.         if (s == -1) {  
  190.             break;  
  191.         if (ls[t] == -1 || runs[s]->buf[runs[s]->idx] > runs[ls[t]]->buf[runs[ls[t]]->idx]) {  
  192.             tmp = s;  
  193.             s = ls[t];  
  194.             ls[t] = tmp;  
  195.         t >>= 1;  
  196.     ls[0] = s;  
  197. void create_loser_tree(run_t **runs, int n)  
  198.     int     i;  
  199.     for (i = 0; i < n; i++) {  
  200.         ls[i] = -1;  
  201.     for (i = n-1; i >= 0; i--) {  
  202.         adjust(runs, n, i);  
  203. void usage()  
  204.     printf("sort <filename> <K-ways>\n");  
  205.     printf("\tfilename: filename of file to be sorted\n");  
  206.     printf("\tK-ways: how many ways to merge\n");  
  207.     exit(1);  

編譯運作

gcc sort.c -o sort -g

./sort random.dat 64

以64路平衡歸并對random.dat内的資料進行外部排序。在I5處理器,4G記憶體的硬體環境下,實驗結果如下

檔案大小    耗時

128M        14.72 秒

256M        30.89 秒

512M        71.65 秒

1G             169.18秒

讀取二進制檔案,檢視排序結

  1.     char *filename = argv[1];  
  2.     int *buffer = (int *)malloc(1<<20);  
  3.     struct stat     sbuf;  
  4.     int rv, data_size, i, bytes, fd;  
  5.         printf("%s not found!\n", filename);  
  6.     bytes = read(fd, buffer, data_size);  
  7.     for (i = 0; i < bytes/4; i++) {  
  8.         printf("%d ", buffer[i]);  
  9.         if ((i+1) % 10 == 0) {  
  10.             printf("\n");  
  11.     printf("\n");  
  12. }  

繼續閱讀