【從頭學數據結構和算法】歸併排序及其優化(c++實現)

c++實現的歸併排序及其優化



普通歸併排序

原理

“分治”思想:對於待排序數據,分成前後兩部分,對每一部分分別進行排序,排完後再進行合併。其中,對於每一部分的排序也是採取這種分兩段排序再合併的思路。

性質

  • 時間複雜度
    ——最好、最壞和平均:O(nlogn)
  • 空間複雜度
    ——O(n):非原地排序!!
  • 穩定性
    ——穩定

代碼

/*********************************************************** 
* 未優化歸併 
************************************************************/ 
/** 
 * @brief 未優化歸併排序使用的合併函數[從小到大](TODO 不完善) 
 * @param a:待排序數組; start,mid:第一段排好序的待合併元素範圍,相應[mid+1, end]爲第二段 
 * @return void	 
 * @note
 */
template<typename T>
void merge0(T *a, int start, int mid, int end) {
	// 一般可以考慮開闢一個新的滿足總長的數組,然後排序在新的數組上,最後複製回原數組 
	// 這裏開闢兩個數組存放兩段原數據,直接在原數組上存最終排序,並藉助哨兵使代碼簡潔
	
	int size_left = mid - start + 1;	// 左半段數量 
	int size_right = end - mid;			// 右半段數量 
	T* left = new T[size_left + 1];		// 加1是爲了多加一個哨兵位 
	T* right = new T[size_right + 1];
	int i = 0, j = 0, k = 0;			// 循環計數變量 
	
	// 複製 
	for (i = 0; i < size_left; ++i) {
		left[i] = a[start+i];
	}
	for (i = 0; i < size_right; ++i) {
		right[i] = a[mid+1+i];
	}
	// 加哨兵
	left[size_left] = MAX;
	right[size_right] = MAX; 
	
	// 按序生成新的數組
	for (k = start, i = 0, j = 0; k <= end; ++k) {
		if (left[i] <= right[j]) {
			// “<=” 保證穩定性
			a[k] = left[i];
			++i; 
		}
		else {
			a[k] = right[j];
			++j;
		}
	}
	// 釋放動態內存 
	delete [] left;
	delete [] right; 
}
/** 
 * @brief 未優化歸併排序使用的遞歸函數[從小到大](TODO 不完善) 
 * @param a:待排序數組; start,end:待合併元素範圍	
 * @return void	 
 * @note
 *	-可改進點: 1.兩段排序後先看是否需要排序2.對於小數組使用插入排序;
 */
template<typename T>
void merge_sort0_r(T *a, int start, int end) {
	// 凡遞歸,先考慮遞歸終止條件
	if (start >= end) {
		return ;
	} 
	
	int mid = start+((end-start)>>1);	// 中間元素下標
	// 兩段數據分開進行排序
	merge_sort0_r(a, start, mid);
	merge_sort0_r(a, mid+1, end);
	// 合併 
	merge0(a, start, mid, end);
}
/** 
 * @brief 未優化歸併排序(遞歸版)[從小到大](TODO 不完善) 
 * @param a:待排序數組; len:待排序元素	
 * @return void	直接修改原 
 * @note
 *	-時間複雜度:最好 - O(nlog);最壞 - O(nlogn);平均 - O(nlogn)  【與待排序數據的有序度無關】 
 *	-空間複雜度:O(n);  !!!不是原地排序算法!!! 
 *	-穩定性:穩定【取決於merge】 
 *	-改進點: 1:對於arr[mid] <= arr[mid+1]的情況,不進行merge 2:對於小數組, 使用插入排序;
 */
template<typename T> 
void merge_sort0(T *a, int len) {
	// 首先檢查數據的合法性(TODO 不完善).
	if (a == NULL || len <= 1) {
		return;
	}
	
	// 遞歸進行歸併排序
	merge_sort0_r(a, 0, len-1); 
} 

優化1:merge前增加判斷

原理

如果a[mid] <= a[mid+1],說明已經有序,不用再merge

代碼

template<typename T>
void merge_sort1_r(T *a, int start, int end) {
	// 凡遞歸,先考慮遞歸終止條件
	if (start >= end) {
		return ;
	} 
	
	int mid = start+((end-start)>>1);	// 中間元素下標
	// 兩段數據分開進行排序
	merge_sort1_r(a, start, mid);
	merge_sort1_r(a, mid+1, end);
	// 若無序則合併
	if (a[mid] > a[mid+1]) {
		merge0(a, start, mid, end);
	} 
}

優化2:小數據使用插入排序

原理

需要排序的數組足夠長時,歸併排序肯定比插入排序更快,但當數組長度比較小的時候,常量因子起主導作用,由於插入排序的常量因子比較小,插入排序比歸併排序更快。

對歸併排序的優化2:當遞歸排序的子問題變得足夠小時,不繼續遞歸調用歸併排序,而是直接調用插入排序。

代碼

template<typename T>
void merge_sort2_r(T *a, int start, int end) {
	// 凡遞歸,先考慮遞歸終止條件
	if (start >= end) {
		return ;
	} 
	// 若數據量小則直接使用插入排序 
	if (end - start < MIN) {
		insert_sort(a, start, end);
	}
	else {
		int mid = start+((end-start)>>1);	// 中間元素下標
		// 兩段數據分開進行排序
		merge_sort2_r(a, start, mid);
		merge_sort2_r(a, mid+1, end);
		// 若無序則合併
		if (a[mid] > a[mid+1]) {
			merge0(a, start, mid, end);
		} 
	}
}

測試

以20000個整數進行測試,記錄時間,同時assert是否與algorithm中sort結果相同。測試代碼略,可見完整代碼中。
測試結果時間(同時,assert未報錯)如下圖所示:
在這裏插入圖片描述

可見,優化效率提升比較明顯,而且對於20000個數據,歸併排序優於冒泡排序、插入排序、選擇排序
(詳情見
【從頭學數據結構和算法】冒泡排序及其優化(c++實現)
【從頭學數據結構和算法】插入排序及其優化(c++實現)
【從頭學數據結構和算法】選擇排序及其優化(c++實現)


完整代碼

排序代碼及優化及測試 完整代碼在:
https://github.com/zhangdanzhu/basic_data-structure_algorithm/blob/master/sort/cpp/merge_sort.cpp

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章