【資料結構與演算法】內部排序之一:插入排序和希爾排序的N中實現(不斷優化,附完整原始碼)

蘭亭風雨發表於2014-02-28

轉載請註明出處:http://blog.csdn.net/ns_code/article/details/20043459


前言

    本來想將所有的內部排序總結為一篇博文,但是隨著研究的深入,還是放棄了這個念頭,斟前酌後,還是覺得分開來寫比較好,具體原因,看完本篇博文也就自然明瞭了。

    本篇文章主要探討插入排序和希爾排序,之所將二者放在一起,很明顯,是因為希爾排序是建立在插入排序的基礎之上的。

    注:以下各排序演算法的N種實現方法大部分都是我根據演算法思想,自己寫出來的,或者是參考其本身的經典實現,我自己都已測試通過,但不敢保證一定都沒問題,如果有疑問,歡迎指出。


插入排序

    插入排序的思想很簡單,它的基本操作就是將一個資料插入到已經排好序的序列中,從而得到一個新的有序序列。根據查詢插入位置的實現思路不同,它又可以分為:直接插入排序、折半插入排序、2-路插入排序。。。這裡,我們主要探討下直接插入排序和折半插入排序。

    直接插入排序

    直接插入排序是最基本的插入排序方法,也是一種最簡單的排序方法。其基本實現思想如下:

    1、首先把第一個元素單獨看做一個有序序列,依次將後面的元素插入到該有序序列中;

    2、插入的時候,將該元素逐個與前面有序序列中的元素進行比較,找到合適的插入位置,形成新的有序序列;

    3、當有序序列擴大為整個原始序列的大小時,排序結束。

      第一種實現方法

    按照該思想,我第一次寫出來的實現程式碼如下:

/*
第一種程式碼形式
插入排序後的順序為從小到大
*/
void Insert_Sort1(int *arr,int len)
{
	int i;
	//從第1個元素開始迴圈執行插入排序
	for(i=1;i<len;i++)	
	{	//將第i個元素分別與前面的元素比較,插入適當的位置
		if(arr[i]<arr[i-1])
		{	//一直向左進行比較,直到插入到適當的位置
			int key = arr[i];
			int count = 0;	//用來記錄key在與前面元素時向左移動的位置
			while(i>0 && key<arr[i-1])
			{
				arr[i] = arr[i-1];
				arr[i-1] = key;
				i--;
				count++;
			}
			//將待插入的數定位到下一個元素,
			//因為後面還要執行i++,所以這裡不再加1
			i += count;  
		} 
	}
}

      第二種實現方法

     很明顯,上面的程式碼有些冗雜,如果面試的時候讓你手寫插入排序的程式碼,很難一下子寫出來。於是,我考慮將while迴圈去掉,直接在後面再來一個for迴圈,每次比較,遇到比自己大的就交換,直到遇到比自己小的,才退出for迴圈。這樣程式碼改成了如下形式:

/*
第二種程式碼形式
插入排序後的順序為從小到大
*/
void Insert_Sort2(int *arr,int len)
{
	int i,j;
	for(i=1;i<len;i++)
		for(j=i-1;j>=0 && arr[j]>arr[j+1];j--)
		{
			//交換元素數值
			//由於不會出現自己與自己交換的情況,
			//因此可以安全地用該交換方法
			arr[j]^=arr[j+1];
			arr[j+1]^=arr[j];
			arr[j]^=arr[j+1];
		}
}

      第三種實現方法

    上面的程式碼要用到資料的交換,即每次要插入的元素要逐個地與前面比它大的元素互換位置,而資料交換需要三步賦值操作,我們完全可以避免進行如此多的操作(排序演算法中一般都會盡量避免資料的交換操作),為了提高執行效率(雖然該執行效率的提高可能並沒有那麼顯著),我們再回過頭來看第一種實現方法,我們可以通過key變數先將待插入的資料儲存起來,在比較時只將元素右移一位即可,最後再將key放到要插入的位置,這樣可以減少兩個賦值操作的執行時間。這樣我們可以把程式碼改成如下實現形式:

/*
第三種程式碼形式
插入排序後的順序為從小到大
*/
void Insert_Sort3(int *arr,int len)
{
	int i,j;
	for(i=1;i<len;i++)
		if(arr[i] < arr[i-1])
		{	//向前逐個比較,直到需要插入的地方
			int key = arr[i];
			for(j=i-1;j>=0 && arr[j]>key;j--)
				arr[j+1] = arr[j];
			arr[j+1] = key;	   //插入key
		}
}

    這也是最常見的實現形式,如果在面試中要手寫插入排序的話,直接把這種實現程式碼寫出來就可以了。

    另外,很明顯可以看出來,對於長度為n的待排序咧,直接插入排序的平均時間複雜度為O(n*n),而且直接插入排序的比較次數與原始序列的中各元素的位置密切相關,待排序的序列越接近於有序,需要比較的次數就越小,時間複雜度也就越小。

    折半插入排序

     直接插入排序演算法簡單,且容易實現,當待排序的長度n很小時,是一種很好的排序方法,尤其當原始序列接近有序時,效率更好。如果待排序的長度n很大,則不適宜採用直接排序。這時我們可以考慮對其做些改進,我們可以從減少比較和移動的次數入手,因此可以採用折半插入排序,其思想類似於折半查詢,這裡不再詳細說明,直接給出實現程式碼:

/*
插入排序後的順序為從小到大
*/
void BInsert_Sort(int *arr,int len)
{
	int i;
	//從第1個元素開始迴圈執行插入排序
	for(i=1;i<len;i++)	
	{	
		int low =0;
		int high = i-1;
		int key = arr[i];
		//迴圈至要插入的兩個點之間
		while(low<=high)
		{
			int mid = (low+high)/2;	
			if(key<arr[mid])
				high = mid-1;
			else
				low = mid+1;
		}
		//迴圈結束後low=high+1,並且low位置即為key要插入的位置

		//從low到i的元素依次後移一位
		int j;
		for(j=i;j>low;j--)
			arr[j] = arr[j-1];
		//將key插入到low位置處
		arr[low] = key;
	}
}
    從程式碼中可以看出,折半插入排序所需附加的儲存空間與直接插入排序相等,時間上來看,折半插入排序減少了比較的次數,但是元素的移動次數並沒有減少。因此,折半插入排序的平均時間複雜度仍為O(n*n)。

希爾排序

    希爾排序(shell排序),又稱縮小增量排序。上面我們提到,直接插入排序在原始序列越接近有序的情況下,排序效率越高,希爾排序正是利用了直接插入排序的這個優勢。希爾排序的基本思想如下:

    它將序列按照某個增量間隔分為幾個子序列,分別對子序列進行插入排序,而後再取另一增量間隔,對劃分的子序列進行插入排序,依次往後。。。待序列已經大致有序,最後再對整個序列進行插入排序(即增量間隔為1),從而得到有序序列。

    本文的重點放在排序演算法的各種程式碼實現上,因此不再對具體的實現思想做過多的闡述,讀者可以查閱相關資料或書籍來熟悉希爾排序的具體思想。由於希爾排序要用到插入排序,因此,我們依次根據上面基本插入排序的三種不同實現方法來書寫希爾排序的程式碼。

    第一種實現方法

    仔細分析希爾排序的實現思想,會發現,如果要迴圈對各個子序列依次進行插入排序,我們需要在直接插入排序程式碼的外面再加一層for迴圈,用來迴圈所有的子序列。我們根據插入排序的第一種實現方法寫出的程式碼如下:

/*
第一種形式的程式碼
對長為len的陣列進行一趟增量為ader的插入排序
本演算法在插入排序演算法的第一種實現形式上進行修改得到
*/
void Shell_Insert1(int *arr,int len,int ader)
{
	int i,k;
	//迴圈對ader個子序列進行插入排序操作
	for(k=0;k<ader;k++)
		for(i=ader+k;i<len;i+=ader)		//對一個子序列進行插入排序操作
		{	//將第i個元素分別與前面的每隔ader個位置的元素比較,插入適當的位置
			if(arr[i]<arr[i-ader])
			{	//一直向左進行比較,直到插入到適當的位置
				int key = arr[i];
				int count = 0;	//用來記錄key在與前面元素比較時向左移動了幾個ader長度
				while(i>k && key<arr[i-ader])
				{
					arr[i] = arr[i-ader];
					arr[i-ader] = key;
					i -= ader;
					count++;
				}
				//將待插入的數定位到下一個元素,執行下一次插入排序
				//因為後面還要執行i+=ader,所以這裡回到原位置即可
				i += count*ader;  
			} 
		}
}

    第二種實現方法

    很明顯,與上面插入排序的第一種實現方法一樣,更加冗雜,現在我們用插入排序的第二種實現方法來實現希爾排序,同樣採用新增外層for迴圈的方式,來迴圈對每個子序列進行插入排序。程式碼如下:

/*
第二種形式的程式碼
對長為len的陣列進行一趟增量為ader的插入排序
本演算法在插入排序演算法的第三種實現形式上進行修改得到
*/
void Shell_Insert2(int *arr,int len,int ader)
{
	int i,j,k;
	//迴圈對ader個子序列各自進行插入排序
	for(k=0;k<ader;k++)
		for(i=ader+k;i<len;i+=ader)
			for(j=i-ader;j>=k && arr[j]>arr[j+ader];j-=ader)
			{
				//交換元素數值
				arr[j]^=arr[j+ader];
				arr[j+ader]^=arr[j];
				arr[j]^=arr[j+ader];
			}
}

    第二種實現方法的改進

    上面的程式碼中需要三個for迴圈,因為我們是迴圈對每個子序列進行插入排序的,實際上我們還可以這樣做:對每個子序列交叉進行排序。比如,第1個子序列中的第2個元素A5(A5表示它在總序列A中的位置序號是5,下同)剛進行完插入排序操作,便接著對第2個子序列中的第2個元素A6進行插入排序操作。這樣我們就可以少寫一個for迴圈,但實際比較的次數還是相同的,只是程式碼更加簡潔。如下:

/*
在第二種程式碼的形式上繼續精簡程式碼
交叉進行各個子序列的插入排序
*/
void Shell_Insert2_1(int *arr,int len,int ader)
{
	int i,j;
	//交叉對ader個子序列進行插入排序
		for(i=ader;i<len;i++)
			for(j=i-ader;j>=0 && arr[j]>arr[j+ader];j-=ader)
			{
				//交換元素數值
				//由於不會出現自己與自己交換的情況
				//因此可以安全地用該交換方法
				arr[j]^=arr[j+ader];
				arr[j+ader]^=arr[j];
				arr[j]^=arr[j+ader];
			}
}

    第三種實現方法

    同樣,根據插入排序的第三種實現方法,迴圈逐個對每個子序列進行插入排序操作,我們可以得到希爾排序的實現方法,如下:

/*
第三種形式的程式碼
對長為len的陣列進行一趟增量為ader的插入排序
本演算法在插入排序演算法的第二種實現形式上進行修改得到
*/
void Shell_Insert3(int *arr,int len,int ader)
{
	int i,j,k;
	//迴圈對ader個子序列各自進行插入排序
	for(k=0;k<ader;k++)
		for(i=ader+k;i<len;i+=ader)
			if(arr[i] < arr[i-ader])
			{
				int key = arr[i];
				for(j=i-ader;j>=k && arr[j]>key;j-=ader)
					arr[j+ader] = arr[j];
				arr[j+ader] = key;
			}
}

    第三種實現方法的改進

    我們可以對該方法做出同樣的改進,對各個子序列進行交叉排序,程式碼如下:

/*
在第三種程式碼的形式上繼續精簡程式碼
交叉進行各個子序列的插入排序
*/
void Shell_Insert3_1(int *arr,int len,int ader)
{
	int i,j;
	//對ader子序列交叉進行插入排序
	for(i=ader;i<len;i++)
		if(arr[i] < arr[i-ader])
		{
			int key = arr[i];
			for(j=i-ader;j>=0 && arr[j]>key;j-=ader)
				arr[j+ader] = arr[j];
			arr[j+ader] = key;
		}
}
    同樣,如果在面試中要手寫希爾排序的程式碼,推薦這種方法實現的程式碼。

    希爾排序的時間複雜度根據選擇的增量序列不同會有所不同,但一般都會比n*n小(序列長度為n)。

    在選擇增量序列時,應使增量序列中的值沒有除1之外的公因子,並且最後一個增量值必須為1.

    看如下兩個增量序列:

n/2、n/4、n/8...1

1、3、7...2^k-1

    第一個序列稱為希爾增量序列,使用希爾增量時,希爾排序在最壞情況下的時間複雜度為O(n*n)。

    第二個序列稱為Hibbard增量序列,使用Hibbard增量時,希爾排序在最壞情況下的時間複雜度為O(n^3/2)。 

    經驗研究表明,在實踐中使用這些增量序列要比使用上面兩個增量序列的效果好的多,其中最好的序列是

{1、5、9、41、109...}

    該序列中的項或是9*4^i-9*2^i+1,或是4^i-3*2^i+1。

    希爾排序的效能是完全可以接受的,即時是對數以萬計的n來說也是如此。程式設計的簡單特點使得它成為對適度的大量輸入資料進行排序時經常選用的演算法。


完整程式碼下載

    各種實現方式的完整的程式碼打包下載地址:http://download.csdn.net/detail/mmc_maodun/6969381



相關文章