排序

概述:

如果被排序的文件可以裝載於內存中,則稱該排序方法爲內部排序(internal sort)。若排序文件來看磁帶或磁盤,則稱做外部排序(external sort)。兩者的主要區別是,內部排序可以輕鬆訪問任意項,而外部排序必須按順序訪問項,或至少要按大數據塊來訪問。

我們這裏說說八大排序就是內部排序。


    

    當n較大,則應採用時間複雜度爲O(nlog2n)的排序方法:快速排序、堆排序或歸併排序序。

   快速排序:是目前基於比較的內部排序中被認爲是最好的方法,當待排序的關鍵字是隨機分佈時,快速排序的平均時間最短;

1.插入排序

1.1直接插入排序

        直接插入排序,就像我們撲克牌一樣。當我們拿到一張牌時,我們習慣將這張牌插入到手上已有的牌當中。

       示例:


        我們知道,該方法是從前面排好了的序列是插入。那做法就是先搜索要排的數在前面的位置,然後將比它大的數往後移動,然後插入即可。我們可以從這一過程中,可以看到三個過程,即搜索-移動-插入。移動和插入很難有所改變,那可以在搜索上下功夫了。我們知道搜索有順序搜索和二分搜索這兩個基本的方法。

         插入排序約平均使用N^2/4次比較,N^2/4次半交換(移動),在最壞的情況下,比較與交換次數加倍。從算法思想上很容易看出比較與移動的次數是相同,對於隨機輸入,每個元素平均移動一半路程就折回,因此,應當計算對角線之下一半的元素。

        對於順序搜索,我們可以將搜索和移動合併同時進行,即從後往前搜索,比較一次就移動一次,直到找到爲止,然後插入,即代碼爲:

//插入排序
void InsertSort(int *d,int n)
{
	int i,j;
	int tmp;
	for(i=1;i<n;i++){
		j=i;tmp=d[i];//複製爲哨兵,即存儲待排序元素
		while(j>0&&d[j-1]>tmp)//從後往前找,邊找邊移動
			d[j--]=d[j-1];
		d[j]=tmp;//找到後就插入
	}
}

最壞時間複雜度:1+2...+n-1=n(n-1)/2

時間複雜度:O(n^2).

1.2二分插入排序

       二分插入排序,實際上是將前面的搜索用二分搜索,其它是一樣的,其代碼爲:

void insertDichotomySort(int *d,int n)
{
	int i,m,a,b;//a,b爲二分法中的頭和尾,m爲中間索引值
	int tmp;//保存要插入的值
	for(i=1;i<n;i++){
		tmp=d[i];//保存要插入的值
		a=0;b=i;//在[0,i]之內搜索,不是[0,i-1]的原因是,可能這個範圍內找不到位置,
		//但如果包含i,則必定會找到相應的位置,即本身
		while(b>a){
			m=(a+b)/2;
			if(d[m]<d[i])
				a=m+1;//由於要插入的值比中間值要大,那中間值必定不是要插入的地方,那至少在這點後面一個點
			//同時也避免了最後a與b只相差1時的無窮循環了
			else
				b=m;//因爲這裏還包含等於
		}
		m=i;
		while(m>=a)//移動
			d[m--]=d[m-1];
		d[a]=tmp;//插入
	}
}

        其實二分搜索插入並不佔多少優勢,主要是它依然要移動,同時在for循環中加入了if條件判斷,使之性能下降。但如果量很大的時候,二分法的優勢可能會體現出來

1.3 二路插入排序

        二分搜索插入只減少了搜索的次數,但還是沒減少移動的次數。爲了減少所需移動的數量,程序員想出採用二路分叉插入,這樣就不必要移動所有的數據,只需要移動一半數據。下面中排序的頭一項被放置在一個輸出區域的中心,而且通過向右或左移動騰出空間。使用些法,可以增加一個輔助空間,也可以不使用,但程序要複雜點。
        如何實現呢?我們不可以這樣真實地模擬兩路插入,除非使用鏈表。實際上我們可以使用循環數組(即可把數組當作環形空間來看)來實現,以數組的第一個點作爲標準,然後兩頭插入,只需要記住兩頭的最終位置即可。

代碼如下:
void insert2Sort(int *d,int n)
{
	int i,j;
	int first=0,//只是記錄左右端點的索引,而不是個數
		last=n;
	int *t=new int[n+1];//多申請一個空間,讓t[0]和t[n],這樣就不必用求餘符號,減小計算量
	memset(t,0,sizeof(int)*(n+1));
	t[0]=d[0];t[n]=d[0];
	for(i=1;i<n;i++){
		if(d[i]>d[0]){//t[0]的右邊插入排序
			j=++first;//記錄最右邊的索引
			while(j>0&&t[j-1]>d[i])//從後往前找,邊找邊移動
				t[j--]=t[j-1];
			t[j]=d[i];
		}
		else{
			j=--last;
			while(j<n&&t[j+1]<d[i])////從前往後找,邊找邊移動
				t[j++]=t[j+1];
			t[j]=d[i];
		}
	}

//	for(i=0;i<n;i++)//由於從last到first本身就是由小到大的順序,所以只需要將t中last到first平移到0到n-1即可
//		d[i]=t[(i+last)%n];
	memcpy(d,t+last,sizeof(int)*(n-last+1));//利用系統函數平移
	memcpy(d+n-last,t,sizeof(int)*(first+1));
}
三者的比較:


1.3希爾排序

         插入排序速度慢,因爲它進行的唯一交換涉及到鄰接項,因此,在數組中,項只能一次移動一個位置。例如,如果最小鍵剛好就在數組的末端,則需要N步將它移動位。希爾排序是插入排序的簡單擴展,它允許離得很遠的元素進行交換,所以提高了速度。
       希爾排序的實質就是分組插入排序,該方法又稱縮小增量排序,因DL.Shell於1959年提出而得名。 該方法的基本思想是:先將整個待排元素序列分割成若干個子序列(由相隔某個“增量”的元素組成的)分別進行直接插入排序,然後依次縮減增量再進行排序,待整個序列中的元素基本有序(增量足夠小)時,再對全體元素進行一次直接插入排序。因爲直接插入排序在元素基本有序的情況下(接近最好情況),效率是很高的,因此希爾排序在時間效率上比前兩種方法有較大提高。


操作方法:
選擇一個增量序列t1,t2,…,tk,其中ti>tj,tk=1;我們可以簡單地設增量序列dk = {n/2 ,n/4, n/8 .....1},也可以是其它方式
按增量序列個數k0,對序列進行k0 趟排序;
每趟排序,根據對應的增量ti,將待排序列分割成若干長度爲m 的子序列,分別對各子表進行直接插入排序。僅增量因子爲1 時,整個序列作爲一個表來處理,表長度即爲整個序列的長度。
void shellSort(int *d,int n)
{
	//每次步長變化爲前一次的一半
	for(int dk=n/2;dk>0;dk/=2){
		//起始索引每次要加1,但不能超過步長dk,
		//即起始索引爲0~dk-1,共移動dk次,即要排dk次直接插入排序
		for(int k=0;k<dk;k++){
			//每一組的直接插入
			for(int i=k+dk;i<n;i+=dk){
				int tmp=d[i];
				int j=i;
				while(j>k&&d[j-dk]>tmp){
					d[j]=d[j-dk];
					j-=dk;
				}
				d[j]=tmp;
			}
		}
	}
}



很明顯希爾排序少了幾個量極

2.交換排序

2.1冒泡法

2.1.1基本冒泡法

 冒泡法是最簡單的排序算法之一,但不是最有效的排序算法之一。從名字上看,就知道和冒泡有關。在水裏,大的往下沉,小的往上冒。如果我們把一組數豎着看,小的數跑着前面,大的往後面跑。其實際做法就是,拿出數組中任意一個數,與下一個數比較,如果大,則往後移動;如果小,自身則不動,讓下一個值進行比較。若假設a是所有中最大的數,索引到它後,由於它後面的所有數都要比a小,故會一起交換下去,這樣一次的排序,就把最大值排到最後。當第二循環時,把第二大的數排到倒數第二位上。依此類推,就排好序了。同時也告訴我們怎麼找第n大的數。

時間複雜度:O(n^2).冒泡排序在最壞情況下,約平均使用N^2/2次細瘦,N^2/2次交換。

具體流程如下:


其代碼爲:

void bubbleSort(int *d,int n)
{
	for(int i=0;i<n-1;i++)//要進行n-1次循環比較
		for(int j=0;j<n-i-1;j++)//每一次冒泡
			if(d[j]>d[j+1])//前一個和後一個比較,如果大於後面那個數,則交換其值
				swap(d[j],d[j+1]);
}

2.2標誌冒泡法

        我們先來看看普通冒泡法的具體過程:

我們從上面可以看到最後4行是相同的,也就是說最後四次我們可以不要了。那爲什麼最後四行會是一樣的呢?我們來分析下冒泡法的原理的過程。冒泡法是每次把剩下數中的最大值給排到最後,但又不僅僅是將最大排到最後,中間過程會有很多排序,所以到最後可能會提前排好。知道了這原因,那我們怎麼確定最後停止的位置呢?每次都會把大的往後移,既然最後沒有移動了,說明沒有交換了,所以我們只要把最後交換的地方給記錄下來即可。這就是標記法。程序如下:
void bubbleFlagSort(int *d,int n)
{
	int pos;
	int i=n-1;
	while(i>0){//因爲我們不能確定循環次數,所以我們用while
		pos=0;
		for(int j=0;j<i;j++){
			if(d[j]>d[j+1]){//記錄最後一次交換的位置
				swap(d[j],d[j+1]);
				pos=j;
			}
		}
		i=pos;
	}
}



2.3正反兩趟標記冒泡法

        前面的兩種冒泡只是從一邊開始,我們可以像二路插入排序一樣,考慮兩頭冒泡。從左往右時,把大的往後移,然後從右往左移時,把小的往前移,同時採用標記減少循環次數。具體過程如下:


        從上面的過程可以看到,首先是將最大值放入最右邊,然後把最小值放入最左邊,當然這個過程中還有別的交換。圖中的R表示從左往右掃,L表示從右往左掃,其值代表最後一

void bubbleFlagTwoSideSort(int *d,int n)
{
	int beforePos=0,
		afterPos=n-1;//設置前後標誌
	int ib,ia;//用於前後標誌的中間變量
	int i;
	while(beforePos<afterPos){//如果左標誌大於右標誌,說明循環已完
		ib=afterPos;ia=beforePos;//左右標誌的初始值的原則是,如果從頭到尾都沒有交換的話,那值是多少,很明顯
		for(i=beforePos;i<afterPos;i++){
			if(d[i]>d[i+1]){
				swap(d[i],d[i+1]);
				ia=i;//其值代表交換中的前一個值,以致afterPos不需要減一了
			}
		}
		afterPos=ia;
//		cout<<afterPos<<"R: ";
//		Print(d,n);
		for(i=afterPos;i>beforePos;i--){
			if(d[i-1]>d[i]){
				swap(d[i-1],d[i]);
				ib=i;//其值代表交換中的後一個值,以致beforePos不需要加一了
			}
		}
		beforePos=ib;
//		cout<<beforePos<<"L: ";
//		Print(d,n);
	}
}

次交換的位置。結合上面兩種方法,可以寫出如下程序:



2.2快速排序

3 選擇排序

3.1 簡單選擇排序

3.2 堆排序

4 並歸排序

5 基數排序

網絡資源:

http://kb.cnblogs.com/page/210687/

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