1. 程式人生 > >【內部排序】八:歸併排序(Merge Sort)詳解與程式碼

【內部排序】八:歸併排序(Merge Sort)詳解與程式碼

歸併排序是多次將兩個或兩個以上的有序表合併成一個新的有序表。最簡單的歸併是直接將兩個有序的子表合併成一個有序的表。  

2-路歸併排序

在內部排序中,通常採用的是2-路歸併排序。即:將含有n個元素的序列看成是n個有序的子序列,每個子序列的長度為1,而後兩兩合併,得到n/2個長度為2或1的有序子序列,再進行兩兩合併。。。直到最後由兩個有序的子序列合併成為一個長度為n的有序序列。2-路歸併的核心操作是將一維陣列中前後相鄰的兩個有序序列歸併為一個有序序列。

一、排序演算法思路:

1、兩個有序表歸併為一個有序表Merge();

設兩個有序表存放在同一陣列中相鄰位置a[start,mid],a[mid+1,end]。合併過程中,設定i,j指標,其初值分別指向這兩個記錄區的起始位置。合併時依次比較a[i]和a[j]的關鍵字,取關鍵字較小的記錄複製到b[k]中,然後將被複制記錄的指標i或j加1,以及指向複製位置的指標k加1。

重複這一過程直至兩個輸入的子檔案有一個已全部複製完畢(不妨稱其為空),此時將另一非空的子檔案中剩餘記錄依次複製到b中即可。最後複製回a陣列中。

/*
將有序的a[start...mid]和有序的a[mid+1...end]歸併為有序的b[0...end-start+1],
而後再將b[0...end-start+1]複製到a[start...end]中,使a[start...end]有序
*/
void Merge(int *a,int *b,int start,int mid,int end)
{
	int i = start;			//i、j分別為第1、2段的下標
	int j = mid+1;
	int k = 0;				//k是b的下標

	//比較兩個有序序列中的元素,將較小的元素插入到b中
	while(i<=mid && j<=end)
	{	
		if(a[i]<=a[j])				//將第1段中的記錄放入b中
		{
			b[k] = a[i];
			i++;k++;
		}	
		else						//將第2段中的記錄放入b中
		{
			b[k] = a[j];
			j++;k++;
		}

	}
	//將a序列中剩餘的元素複製到b中,這兩個語句只可能執行其中一個
	while(i<=mid)					//將第1段餘下部分複製到b
	{
		b[k] = a[i];
		i++;k++;
	}	
	while(j<=end)					//將第2段餘下部分複製到b
	{
		b[k] = a[j];
		j++;k++;
	}

	//將b中的元素複製回到a中
	for(i=0;i<k;i++)
		a[i+start] = b[i];
}

2、利用Merge()完成一趟歸併排序MergePass();

分析:
      在某趟歸併中,設各子檔案長度為length(最後一個子檔案的長度可能小於length),則歸併前R[1..n]中共有 個有序的子檔案:R[0..length],R[length+1..2length],…, 
注意:
     呼叫歸併操作將相鄰的一對子檔案進行歸併時,必須對子檔案的個數可能是奇數、以及最後一個子檔案的長度小於length這兩種特殊情況進行特殊處理:
  ① 若子檔案個數為奇數,則最後一個子檔案無須和其它子檔案歸併(即本趟輪空);
  ② 若子檔案個數為偶數,則要注意最後一對子檔案中後一子檔案的區間上界是n-1。

//利用Merge()完成一趟歸併排序
void MergePass(int *a,int *b,int length,int n)
{  
	int i;
	for (i=0;i+2*length-1<n;i=i+2*length) 
		Merge(a,b,i,i+length-1,i+2*length-1);	//歸併length長的兩相鄰子表 	
	if (i+length-1<n)							//餘下兩個子表,後者長度小於length,子表個數為偶數,奇數輪空
		Merge(a,b,i,i+length-1,n-1);  			//歸併這兩個子表
}

3、完成二路歸併排序演算法Merge_Sort()。

 採用自底向上的基本思想,第1趟歸併排序時,將待排序的檔案a[1..n]看作是n個長度為1的有序子檔案,將這些子檔案兩兩歸併,若n為偶數,則得到 個長度為2的有序子檔案;若n為奇數,則最後一個子檔案輪空(不參與歸併),故本趟歸併完成後,前 個有序子檔案長度為2,但最後一個子檔案長度仍為1;第2趟歸併則是將第1趟歸併所得到的 個有序的子檔案兩兩歸併,如此反覆,直到最後得到一個長度為n的有序檔案為止。上述的每次歸併操作,均是將兩個有序的子檔案合併成一個有序的子檔案,故稱其為"二路歸併排序"。類似地有k(k>2)路歸併排序。

//完成二路歸併排序
void Merge_Sort(int *a,int n)		//自底向上
{
	int length;
	int *b = (int *)malloc(n*sizeof(int));		//一個臨時陣列,用來儲存歸併後有序序列的陣列
	for (length=1;length<n;length=2*length)		//進行log2n趟歸併
	{
		MergePass(a,b,length,n);
	}
}

二、演算法分析

1、穩定性

      歸併排序是一種穩定的排序。
2、儲存結構要求
     可用順序儲存結構。也易於在連結串列上實現。
3、時間複雜度
     對長度為n的檔案,需進行 趟二路歸併,每趟歸併的時間為O(n),故其時間複雜度無論是在最好情況下還是在最壞情況下均是O(nlgn)。
4、空間複雜度

     需要一個輔助向量來暫存兩有序子檔案歸併的結果,故其輔助空間複雜度為O(n),顯然它不是就地排序。

**每趟歸併排序,元素不一定歸位。