1. 程式人生 > >合併排序的非遞迴實現(自底向上設計)

合併排序的非遞迴實現(自底向上設計)

上一篇博文,討論了合併排序的遞迴實現。這篇文章,說說合並排序的非遞迴實現。

思路描述

假設一個數組,共有11個(0到10)元素。
首先,進行“1+1”合併:即第0個和第1個合併,第2個和第3個合併,……,第8個和第9個合併,第10個不用合併;
然後,進行“2+2”合併:0~3合併,4~7合併,此時剩下3個,雖然不足4,但是大於2,所以做“2+1”合併,即8~10合併;
再然後,進行“4+4”合併,即0~7合併,此時剩下3個,3小於等於4,所以不用合併;
最後,進行“8+8”合併,因為總數是11,雖然不足16,但是大於8,所以做“8+3”合併.

也許上面的描述把你搞暈了,沒關係,看看示意圖,秒懂。

下圖共11個元素,實線箭頭表示合併,虛線箭頭表示不做處理。

這裡寫圖片描述

C語言程式碼

#include <stdio.h>
#include <string.h>   //memcpy函式用這個標頭檔案
#include <stdlib.h>   //malloc和free函式用這個標頭檔案

/*
 *此函式為了測試用,功能是列印陣列,不想刷屏,所以沒有加換行
 *a是陣列首地址
 *len是陣列長度
 */
void print_array(int *a, int len)
{
    int i=0;
    for(i=0; i<len; ++i)
        printf
("%d ",a[i]); } /* * 將兩個有序陣列b和c合併為一個有序陣列a * b是第一個非降序陣列的首地址,長度是len_b * c是第二個非降序陣列的首地址,長度是len_c * a指向輸出陣列的首地址 * 注意:a陣列需要呼叫者分配空間,且a陣列不能與b或者c重疊 */ void __merge(int *a, int *b, int len_b, int *c, int len_c) { int i = 0; // b的下標 int j = 0; // c的下標 int k = 0; // a的下標 int len = len_b + len_c; //計算出a陣列的長度
/*迴圈執行條件是 i 和 j 都沒有越界*/ while(i < len_b && j < len_c) //&&的優先順序更低 { /*比較b[i]和c[j],把較小的複製到a[k],同時i(或j)和k指向下一個元素*/ if(b[i] <= c[j]) a[k++] = b[i++]; else a[k++] = c[j++]; } if(i == len_b) //說明b已經處理完 memcpy(a+k, c+j, (len-k)*sizeof(int)); else //說明c已經處理完 memcpy(a+k, b+i, (len-k)*sizeof(int)); } void __merge_two_part(int a[], int len_1, int len_2) { int *sub_1 = a; int *sub_2 = a + len_1; /* 這裡可以新增除錯資訊,展示排序過程,比如 printf("merge "); print_array(sub_1,len_1); printf(" and "); print_array(sub_2,len_2); */ int temp_len = sizeof(int) * (len_1 + len_2); //計算需要多少位元組 int *temp = malloc(temp_len); //分配臨時空間 if(temp == NULL) { printf("malloc failed\n"); return; } __merge(temp,sub_1,len_1,sub_2,len_2); memcpy(a,temp,temp_len); //此時陣列a已經有序 /* 這裡可以新增除錯資訊,展示排序過程,比如 printf("---> "); print_array(a,len_1 + len_2); printf("\n"); */ free(temp); } void __n_and_n_merge(int a[], int a_len, int n) { int left = a_len; //left用來計數,表示還剩多少個元素沒有參與合併 int join_len = n + n; // n並n,合併後的長度是2n,用join_len表示 while(left >= join_len) { __merge_two_part(a,n,n); a += join_len; //使a指向下一段 left -= join_len; } /* 當不夠n+n合併時,如果超過n個元素,仍然進行合併; 如果剩餘元素小於等於n個,則不做處理 */ if(left > n) { __merge_two_part(a,n,left-n); } } void merge_sort_down2up(int a[], int len) { int step = 1; //初始步長 while(step < len) { __n_and_n_merge(a,len,step); //step + step 合併 step *= 2; } } //測試函式 int main(void) { int a[11]={9,8,5,3,2,9,4,6,7,1,0}; //11個數 merge_sort_down2up(a,11); printf("最終結果:"); print_array(a,11); printf("\n"); return 0; }

程式碼講解

已經在程式碼中添加了詳細的註釋,這裡只說要點。
與排序相關的函式有4個。

void __merge(int *a, int *b, int len_b, int *c, int len_c);
void __merge_two_part(int a[], int len_1, int len_2);
void __n_and_n_merge(int a[], int a_len, int n);
void merge_sort_down2up(int a[], int len);

前三個屬於內部函式(我以雙下劃線開頭),第四個merge_sort_down2up屬於開放給使用者的函式,呼叫的時候傳入整形陣列名和長度即可。

void __merge(int *a, int *b, int len_b, int *c, int len_c);
這個函式在上一篇博文中出現過,屬於歸併過程中最基本的“磚頭”,此函式被第二個函式__merge_two_part呼叫。

void __merge_two_part(int a[], int len_1, int len_2);
為了增強整個程式碼的可讀性,我專門設計了這個介面。
這個函式的目的是把一個數組的前半部分(已經為升序)和後半部分(已經為升序)進行合併排序。int a[]也可以寫為int *a,用來接收陣列首地址;len_1表示前半部分的長度,len_2表示後半部分的長度。
假設陣列a包含7個元素,前4個和後3個元素已經分別有序,現在要做4+3合併,那麼應該這樣用
__merge_two_part(a, 4, 3);

示意圖如下:

這裡寫圖片描述

呼叫後,陣列a為升序。

void __n_and_n_merge(int a[], int a_len, int n);
此函式對整個陣列進行n+n合併。
注意:對於剩餘的元素,當不夠n+n合併時,如果大於n個元素,則進行n+x合併; 如果小於等於n個,則不做處理.

a_len表示陣列長度,每次呼叫時保持不變,n表示合併的步長,取值為1,2,4,8...;
n的初始值為1,反覆呼叫此函式,呼叫後n取值翻倍,直到n大於等於a_len為止。

void __merge_two_part(int a[], int len_1, int len_2)函式中新增一些列印資訊,可以看出整個排序過程。截圖如下:

這裡寫圖片描述