1. 程式人生 > >資料結構(歸併排序)

資料結構(歸併排序)

1,二路歸併排序設計思路

與快速排序一樣,歸併排序也是基於分治策略的排序,(分治法將問題(divide)成一些小的問題然後遞迴求解,而治(conquer)的階段則將分的階段得到的各答案"修補"在一起,即分而治之)。歸併排序將待排序的元素分成兩個長度相等的子序列,分別為每一個子序列排序,然後把子序列合併成一個總的序列。合併兩個子序列的過程稱為二路歸併。(注:二路歸併演算法是一種穩定排序演算法,他的執行時間不依賴與待排元素的初始序列,因此也就避免了快速排序最差的情況),演算法時間複雜度o(nlog2^{n}),空間複雜度o(n+log2^{n})

1.1,歸併排序的遞迴演算法:

void MergeSort(int *arr, int left, int right)
{
	//left和right是當前參加歸併的元素下標
	if (left < right)
	{
		int mid = (right + left);
		MergeSort(arr, left, mid);
		MergeSort(arr, mid + 1, right);
		Merge(arr, left, right, mid);
	}
}


void Merge(int *arr, int left, int right, int mid)
{
	//arr[left......mid]和arr[mid+1.........right]是兩個有序表將這兩個表合併成一個有序表,先暫時放在array陣列中,再重新放回原來的位置
	int i = left, j = mid + 1, k = 0, s = right - left + 1;//i和j是檢測指標,k是存放指標
	int *array = new int[s];
	while (i <= mid && j <= right)//如果兩個表都沒檢測完,則兩兩比較
	{
		if (arr[i] <= arr[j])
			array[k++] = arr[i++];
		else
			array[k++] = arr[j++];
	}
	while (i <= mid)//第一個表沒有檢測完
	{
		array[k++] = arr[i++];
	}
	while (j <= right)//第二個表沒有檢測完
	{
		array[k++] = arr[j++];
	}
	for (int i = 0;i < s;i++)
		arr[left + i] = array[i];
	free(array);
}

我們可以把分解的過程理解為建立完全二叉樹的過程(遞迴深度是log2^{^{n}}

 在這裡給大家列出(2,4,7,5,8,1,3,6)這個序列歸併排序的過程,從圖中我們可以看到,每次都是在原有元素的基礎上面對半分,即取一半,直到分解為每一組元素剩下一個為止,我們預設一個元素已經有序,然後在依次把每兩個元素合併成一個有序序列,即上圖中的第一步合併操作,從那裡我們可以看到現在已經合併為四個分組,每個分組裡面兩個元素有序排列,接著在進行一次合併,合併為兩個分組,每組四個元素有序排列,接著進行最後一次合併操作,整個序列變為有序,排序結束。

1.2,二路歸併排序迭代演算法

但是在實際上遞迴的歸併排序時間代價和空間代價都很高,應為要做多次遞迴呼叫,最多需要o(n)

的輔助陣列,還需要一個規模為o(log2^{^{n}})的遞迴工作站,實用性最好的是迭代的歸併排序,演算法是利用二路歸併過程的自底向上進行排序,假設待排元素有n個,迭代的歸併排序也需要一個與待排序序列一樣大的可容納n個元素的輔助空間。

//歸併排序的迭代演算法實現
void Merge(int *arr1, int *arr2,int left, int right, int mid)
{
	//arr[left......mid]和arr[mid+1.........right]是兩個有序表將這兩個表合併成一個有序      表,先暫時放在array陣列中,再重新放回原來的位置
	int i = left, j = mid + 1, k = 0, s = right - left + 1;//i和j是檢測指標,k是存放指標
	while (i <= mid && j <= right)//如果兩個表都沒檢測完,則兩兩比較
	{
		if (arr1[i] <= arr1[j])
			arr2[k++] = arr1[i++];
		else
			arr2[k++] = arr1[j++];
	}
	while (i <= mid)//第一個表沒有檢測完
	{
		arr2[k++] = arr1[i++];
	}
	while (j <= right)//第二個表沒有檢測完
	{
		arr2[k++] = arr1[j++];
	}
}


void  MergePass(int *arr1, int *arr2, int len,int n)
{
	//對arr1中的長度為len的歸併項進行一趟二路歸併,結果存放於arr2的相同位置
	int i = 0;
	while (i + 2 * len <= n - 1)//兩兩歸併長度為len的歸併項,批處理歸併長度為len的歸併項
	{
		Merge(arr1, arr2, i, i + 2 * len - 1, i + len - 1);//arr1[i......i+len-1]與ar1[i+len.....i+2*len-1]
		i = i + 2 * len;//i進到下一次兩兩歸併的第一個歸併項
	}
	if (i + len <= n)//特殊情況第二個歸併項不足len
	{
		Merge(arr1, arr2, i, n - 1, i + len - 1);
	}
	else//特殊情況,只剩下一個歸併項
		for (int j = i;j <= n - 1;j++)
		{
			arr2[j] = arr1[j];
		}
}


void MergeSort(int *arr, int n)
{
	int i, len = 1;
	int *arr2 = new int[n];
	while (len < n)//第一趟歸併令長度len為1,以後每次歸併都讓len變為原來2倍
	{
		MergePass(arr, arr2, len,n);
		len *= 2;
		MergePass(arr2, arr, len, n);
                len *= 2;
	}
}

下面我們以一個例項來演示迭代的歸併排序,初始序列是(21,25,49,25*,93,62,72,08,37,16,54)在這裡為了說明排序是穩定的,用了25*做對照,25和25*相對位置沒有變,所以歸併排序是穩定的。

len=1:21,25,49,25*93,62,72,08,37,16,54

len=2:21 2525* 49,62 93,08 7216 37,54

len=4,:21 25 25* 4908 62 72 93,16 37 54

len=8:08 21 25 25* 49 62 72 93,16 37 54

len=16:08 16 21 25 25* 37 49 54 62 72 93

當len=1時,我們把每個元素都看作歸併項,歸併為len=2的歸併項,每一次len都增大原來的二倍。假設陣列元素arr[0]到arr[n-1]已經歸併為長度為len的歸併項,要求再將這些歸併項兩兩歸併,歸併成長度為2len的歸併項,把這些歸併項放在arr2的輔助陣列中,如果n不是2len的整數倍,則歸併完一趟,可能遇到兩種情況:

(1)剩下一個長度為len的歸併項和一個長度小於len的歸併項,這時可以再次呼叫Merge函式將他們歸併稱為一個長度小於2len的歸併項。

(2)只剩下一個歸併項,長度小於或者等於len,由於沒有另一個歸併項與其歸併,可以將直接放到arr2的輔助陣列中,準備參加下一趟歸併操作。

總之,迭代演算法分為兩部分,第一部分先對長度為len的歸併項進行兩兩歸併,直到出現上述情況之一,第二部分處理上述特殊情況。