1. 程式人生 > >排序演算法(六)、歸併排序

排序演算法(六)、歸併排序

1、二路歸併排序

“歸併”即“合併”,是指將兩個或者兩個以上有序表組合成一個有序表。假如待排序表含有 n 個記錄,即可以視為 n 個有序的子表。每個子表長度為1,然後兩兩歸併,得到 n/2 個長度為 2 或者 1 的有序表,然後,再兩兩歸併,。。。。如此重複,直到合併成一個長度為 n 的有序表為止。這種排序方法稱為“二路歸併排序”。

遞迴形式的二路歸併演算法,主要包含兩個步驟:

1)、分解:將長度為 n 的待排序表分解成兩個 n/2 大小的子表,然後,遞迴地對兩個子表進行排序。。

2)、合併:將子表合併。

(1)部分程式碼如下:

void MergeSort(int a[], int low, int high)
{
	if (low < high)
	{
		int mid = (low + high) / 2;
		MergeSort(a,low,mid);
		MergeSort(a,mid+1,high);
		Merge(a,low,mid,high);

	}
}

(2)部分程式碼如下:

// MergeSort 歸併排序

void Merge(int a[], int low, int mid, int high)
{
	int i = low, j = mid + 1, k = 0;    // j 是從 mid + 1 開始的。
	int *b = new(nothrow) int[high - low + 1];
	if (!b)  
	{
		cout << "分配失敗" << endl;
		return;
	}

	while (i <= mid && j <= high)  // 分兩路進行比較,把資料較小的放到中介陣列
	{
		if (a[i] <= a[j])
			b[k++] = a[i++];
		else
			b[k++] = a[j++];
	}

	while (i <= mid)    // 若某個區間的資料仍然有剩餘,就直接把剩餘的資料複製到中介陣列
		b[k++] = a[i++];// 這兩個迴圈只會執行其中一個。。因為肯定不會兩邊都有剩餘
	while (j <= high)
		b[k++] = a[j++];

	for (int i = low,k=0; i <= high; i++,k++) // 將 b 陣列的元素複製到原陣列
		a[i] = b[k];

	delete[]b;

}

測試:

int main()
{
	int a[] = { 1, 5, 3, 4, 12, 35, 21, 9 };
	mergesort(a,0,7);

	for (int i = 0; i < 8; i++)
		cout << a[i] << endl;
	return 0;
}

對於非遞迴的形式,只需要更改 merge() 即可

void MergeSort(int arr[], int n)//n代表陣列中元素個數,陣列最大下標是n-1   
{
	int size = 1, low, mid, high;
	while (size <= n - 1)  // 使用步長來控制
	{
		low = 0;
		while (low + size <= n - 1)
		{
			mid = low + size - 1;
			high = mid + size;
			if (high>n - 1)//第二個序列個數不足size   
				high = n - 1;
			Merge(arr, low, mid, high);//呼叫歸併子函式   
			//cout << "low:" << low << " mid:" << mid << " high:" << high << endl;//打印出每次歸併的區間   
			low = high + 1;//下一次歸併是第一關序列的下界   
		}
		size *= 2;//範圍擴大一倍   
	}
}

舉例說明一下:

// 非遞迴版本的 MergeSort
// 使用一個例子來解釋這個排序,假設有 8 個數據,[48],[38],[65],[97],[76],[13],[27],[33],元素個數 n = 8
// 初始時,size = 1,此時,size <= 7,
// 第一次內迴圈時,即 low + size <= n - 1,因為每次 low 都會遞增,所以要保證不越界
// 一開始,low = 0,mid = 0,high = 1,這可以看成是元素的下標。呼叫歸併函式,將 [48],[38]變成 [38,48].
// low = high +1 = 2,mid = 2,high = 3,呼叫歸併函式,將 [69],[57]變成 [57,69].
//low = high +1 = 4,mid = 4,high = 5,呼叫歸併函式,將 [76],[13]變成 [13,76].
//low = high +1 = 6,mid = 6,high = 7,呼叫歸併函式,將 [27],[33]變成 [27,33].,這樣,序列變成了 [38,48],[57,69],[13,76],[27,33]


// 第二次,size = 2,size <=7
//一開始,low = 0,mid = 1,high = 3,[38,48],[57,69]變成了 [38,48,57,69]
//然後,low = 3+1 = 4, mid = 5, high = 7, [13,76],[27,33]變成了[13,27,33,76]


// 第三次,size = 4,size <=7
//一開始,low = 0,mid = 3,high = 7,這樣 [38,48,57,69],[13,27,33,76] 就變成了 [13,27,33,38,48,57,69,76]

效能分析:

空間複雜度:上面的 merge () 函式需要申請輔助單元, n 個單元。但是,每一趟歸併以後,這些空間就被釋放了,所以時間複雜度為 O(n)

時間複雜度: 每一趟歸併的複雜度為 O(n),總共進行 log2 (n) 趟歸併。所以時間複雜度為 O(nlog2 (n))。

升級——原地歸併排序

既然 merge() 函式需要輔助單元,那麼,不使用額外的儲存空間可以嗎?