[算法]——歸並排序(Merge Sort)


歸並排序(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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM