歸並排序(Merge Sort)與快速排序思想類似:將待排序數據分成兩部分,繼續將兩個子部分進行遞歸的歸並排序;然后將已經有序的兩個子部分進行合並,最終完成排序。其時間復雜度與快速排序均為O(nlogn),但是歸並排序除了遞歸調用間接使用了輔助空間棧,還需要額外的O(n)空間進行臨時存儲。從此角度歸並排序略遜於快速排序,但是歸並排序是一種穩定的排序算法,快速排序則不然。
所謂穩定排序,表示對於具有相同值的多個元素,其間的先后順序保持不變。對於基本數據類型而言,一個排序算法是否穩定,影響很小,但是對於結構體數組,穩定排序就十分重要。例如對於student結構體按照關鍵字score進行非降序排序:
// A structure data definition
typedef struct __Student
{
char name[16];
int score;
}Student;
// Array of students
name : A B C D
score: 80 70 75 70
Stable sort in ascending order:
name : B D C A
score: 70 70 75 80
Unstable sort in ascending order:
name : D B C A
score: 70 70 75 80
其中穩定排序可以保證B始終在D之前;而非穩定排序,則無法保證。
1)數組的歸並排序
歸並排序的思想實際上是一種分治法,即將待排序數據分成兩部分,分別對兩部分排序,然后將這兩部分合並。下面以非降序排序為例:
// Split arr[] into two parts [begin,mid), [mid,end)
// and using merge_core to merge this two parts
// Total Time O(nlogn)
void merge_sort(int arr[], int begin, int end)
{
if (end-begin < 2) return;
int mid = (begin+end)>>1;
merge_sort(arr,begin,mid);
merge_sort(arr,mid,end);
merge_core(arr,begin,mid,end);
} // Time O(logn)
其中arr[]為待排序數組,對於一個長度為N的數組,直接調用merge_sort(arr,0,N);則可以排序。
歸並排序總體分為兩步,首先分成兩部分,然后對每個部分進行排序,最后合並。當然也可以分成三部分或其他,然而通常是分成兩部分,因此又稱為二路歸並。merge_core可以將兩個有序數組合並成一個,具體操作如圖所示:
/* merge core: combine two parts which are sorted in ascending order * arr[]: ..., 1, 4, 8, 2, 3, 7, 9, ... * begin__| |__mid |__end * part1: 1, 4, 8 part2: 2, 3, 7, 9 * * combination: * part1:[1] [4] [8] * part2: | [2] [3] | [7] | [9] * | | | | | | | * tmp :[1] [2] [3] [4] [7] [8] [9] * * at last, copyback tmp to arr[begin,end) * */
合並的前提是,兩個數組已經是有序的。其代碼為:
void merge_core(int arr[], int begin, int mid, int end)
{
int i=begin, j=mid, k=0;
int *tmp = (int*)malloc(sizeof(int)*(end-begin));
for(; i<mid && j<end; tmp[k++]=(arr[i]<arr[j]?arr[i++]:arr[j++]));
for(; i<mid; tmp[k++]=arr[i++]);
for(; j<end; tmp[k++]=arr[j++]);
for(i=begin, k=0; i<end; arr[i++]=tmp[k++]);
free(tmp);
} // Time O(n), Space O(n)
其中第6,7兩行,將剩余的部分追加到tmp[]中,然后將tmp[]寫回到arr[]。因此,對於數組使用歸並排序,需要輔助空間O(n)。由於是尾部調用merge_core,當然可以將其寫入到merge_sort尾部,這里為了思路清晰,將其分成兩部分書寫。
2)鏈表的歸並排序
事實上,歸並排序更適合對鏈表排序,因為在合並兩個鏈表時,不需要額外的輔助空間存儲,而且也不需要對數據拷貝,直接移動指針即可。唯一的不便是:需要每次尋找到鏈表的中間節點,然后以此將該鏈表分割成兩部分。尋找中間節點,可以定義兩個指針fast和Mid,fast每次移動兩步,mid每次移動一步,當fast到鏈表尾部時,mid此時處於鏈表中間(不用考慮奇偶情況):
// Merge sort for single list as ascending order
// single list node define
typedef struct __ListNode
{
int val;
struct __ListNode *next;
}ListNode;
// Merge sort for single list without head node
ListNode *merge_sort(ListNode *head)
{
if (head==NULL || head->next==NULL) return head;
ListNode *fast, *mid, H;
// find mid node between head and end
for (H.next=head, fast=mid=&H; fast && fast->next;){
mid = mid->next;
fast = fast->next->next;
}
fast = mid->next;
mid->next = NULL; // cut down mid part from head list
mid = fast;
head = merge_sort(head);
mid = merge_sort(mid);
return merge_core(head,mid);
}
注意,找到鏈表的中間節點后,務必將其指向NULL,以保證確實將鏈表分成兩部分。然后將兩個鏈表head與mid進行合並。由於合並后可能會修改鏈表頭結點,因此要返回新的鏈表頭結點。下面是合並操作:
// merge single list without head node (ascending order)
ListNode *merge_core(ListNode *i, ListNode *j)
{
ListNode H, *p;
for (p=&H; i && j; p=p->next){
if (i->val < j->val){
p->next = i;
i = i->next;
}
else{
p->next = j;
j = j->next;
}
}
p->next = (i ? i:j);
return H.next;
}
鏈表合並時,不需要像數組那樣,直接可以將鏈表尾部p->next指向剩余的i或j,即可完成合並。可以看出,歸並排序更適合於對鏈表排序,而快速排序適合於數組排序。
注:本文涉及的源碼:merge sort : https://git.oschina.net/eudiwffe/codingstudy/blob/master/src/sort/mergesort.c
