排序算法

算法的穩定性:

概念

假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,ri=rj,且ri在rj之前,而在排序後的序列中,ri仍在rj之前,則稱這種排序算法是穩定的;否則稱爲不穩定的。

判斷方法

對於不穩定的排序算法,只要舉出一個實例,即可說明它的不穩定性;而對於穩定的排序算法,必須對算法進行分析從而得到穩定的特性。需要注意的是,排序算法是否爲穩定的是由具體算法決定的,不穩定的算法在某種條件下可以變爲穩定的算法,而穩定的算法在某種條件下也可以變爲不穩定的算法。
再如,快速排序原本是不穩定的排序方法,但若待排序記錄中只有一組具有相同關鍵碼的記錄,而選擇的軸值恰好是這組相同關鍵碼中的一個,此時的快速排序就是穩定的。

生成N個隨機數:

在實際中,要測試各種排序算法,通常要輸入大量的數據,爲了簡化程序,能快速看到排序算法的效果,編寫一個隨機生成整型數組來進行測試,下面的函數用來生成n個不相等的整數。
//創建一個隨機數組,arr保存生成的數據,n爲數組元素的數量,min爲生成數據的最小值,max表示生成數據的最大值,數組中生成的數都不重複。
int CreateData(int arr[],int n,int min,int max) 
{
    int i,j,flag;
    srand(time(NULL));//取時間做隨機數種子,這樣種子酒不會重複,產生的隨機數也就不會重複。
    if((max-min+1)<n) //最大數與最小數之差小於產生數組中元素的數量,生成數據不成功,因爲數組中的元素都是不重複的。
		return 0; 
    for(i=0;i<n;i++)
    {
        do
        {
            arr[i]=(max-min+1)*rand()/(RAND_MAX+1)+min;
            flag=0;
            for(j=0;j<i;j++)
            {
                if(arr[i]==arr[j])
                    flag=1;
            }
        }while(flag);       
    }
    return 1;
}
上面的函數需要包含頭文件:#include<stdlib.h>,#include <time.h>。

1.冒泡排序算法:

冒泡排序算法的思想:對待排序記錄關鍵字從前往後進行多遍掃描,當發現相鄰兩個關鍵字的次序與排序要求的規則不符時,就將這兩個記錄進行交換。這樣,關鍵字較小的記錄將逐漸從後面向前面移動,就像氣泡在水中向上浮一樣,所以該算法稱爲冒泡排序算法。代碼如下:
void Bubble(int a[],int length)
{
     int i,j,tmp;
     /*外層循環式控制循環次數,之所以i<length-1,是因爲,
     當前length-1個數都排好序後,最後一個數不用在排序了*/
     for(i=0; i<length-1; i++)
     {
          /*內層循環是進行逐個比較,若滿足大於或小於關係則交換,之所以j<length-1-i,
          是因爲當前已經有i個數已經排好序了,故當前只需要比較前length-1-i個數就好了。*/
          for(j=0; j<length-1-i; j++)
          {
               //若滿足關係,則交換。
               if(a[j] > a[j+1])
               {
                    tmp = a[j];
                    a[j] = a[j+1];
                    a[j+1] = tmp;
               }
          }
     }
}

從上面的程序可以看出,使用冒泡排序發對n個數據進行排序,一共需要進行n-1次的比較。如果本來就是有序的數據,也需要進行n-1次比較。這就造成了冒泡排序算法雖然簡單,但效率較差。
     爲了提升冒泡排序算法的效率,可對Bubble函數進行改進,當在某一遍掃描時,發現數據都已經按順序排列了(就是在此遍掃面過程中沒有出現數據之間的兩兩交換),就不再進行後面的掃描,而結束排序過程。
     具體實現是:可以設置一個標誌變量flag,在每一遍掃描之前將其設置爲0,在掃描數據的過程中,若有數據交換,則設置其值爲1.在一遍掃描完成之後,判斷flag的值,若其值爲0,表示在這一遍掃遍中已經沒有數據進行交換,數據已經按順序排列,就不需要進行後續的掃描。
實現程序如下:
int bubbleSort(int arr[],int len)
{
     int i,j,tmp,flag=0;//增加標誌變量flag判斷本次掃描是否有交換。
     if(len < 0 )
          return -1;
     for(i=0;i<len-1;i++)
     {
          for(j=0;j<len-1-i;j++)
          {
               if(arr[j]<arr[j+1])
               {
                    tmp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = tmp;
                    flag=1;//本次掃描有交換,就將flag置1
               }
          }
          if(0 == flag)//如果本次掃描flag爲0則本次掃描沒有發生交換,那麼數組已然有序,break退出,不用在掃描了
               break;
          else
               flag = 0;//否則將flag置爲默認的0值。繼續下一次掃描(冒泡排序操作)。
     }

     return 0;
}

算法分析

 若文件的初始狀態是正序的,一趟掃描即可完成排序。所需的關鍵字比較次數和記錄移動次數均達到最小值n-1;
所以,冒泡排序最好的時間複雜度爲O(n)。
初始文件是反序的,需要進行n-1趟排序。每趟排序要進行n-i次關鍵字的比較(1≤i≤n-1),且每次比較都必須移動記錄三次來達到交換記錄位置。在這種情況下,比較和移動次數均達到最大值:
比較:n(n-1)/2=O(n^2)
移動:3n(n-1)/2=O(n^2)

冒泡排序的最壞時間複雜度爲O(n^2)。
綜上,因此冒泡排序總的平均時間複雜度爲O(n^2)。

冒泡排序畢竟是一種效率低下的排序方法,在數據規模很小時,可以採用。數據規模比較大時,最好用其它排序方法。

算法穩定性

冒泡排序就是把小的元素往前調或者把大的元素往後調。比較是相鄰的兩個元素比較,交換也發生在這兩個元素之間。所以,如果兩個元素相等,我想你是不會再無聊地把他們倆交換一下的;如果兩個相等的元素沒有相鄰,那麼即使通過前面的兩兩交換把兩個相鄰起來,這時候也不會交換,所以相同元素的前後順序並沒有改變,所以冒泡排序是一種穩定排序算法。


2.選擇排序:

選擇排序法就是在當前的集合中選擇最小的數與第一個數進行交換,然後將集合縮小一個,即是將已經選出的最小數排除。然後再在剩下的集合中重複上面步驟,直到所有的都排好序。注意只剩最後一個數的時候不需比較。
還有一種解釋就是每一趟從待排序的數據元素中選出最小的一個元素,順序放在已排好序的數列的最後,直到全部待排序的數據元素排完直接選擇排序和直接插入排序類似,都將數據分爲有序區和無序區,所不同的是直接插入排序是將無序區的第一個元素直接插入到有序區以形成一個更大的有序區,而直接選擇排序是從無序區選一個最小的元素直接放到有序區的最後。
代碼如下:
bool SelectSort(int arr[],int len)
{
	if(NULL == arr || len<=0)
		return false;
	int i,j,tmp,minIndex;
	for(i=0; i<len-1; i++)
	{
		minIndex = i;
		for(j=i+1; j<len; j++)
		{
			if(arr[j]<arr[minIndex])
				minIndex = j;
		}
		if(minIndex != i)
		{
			tmp = arr[i];
			arr[i] = arr[minIndex];
			arr[minIndex] = tmp;
		}
	}
	return true;
}
性能分析:
比較次數O(n^2),比較次數與關鍵字的初始狀態無關,總的比較次數N=(n-1)+(n-2)+...+1=n*(n-1)/2。交換次數O(n),最好情況是,已經有序,交換0次;最壞情況是,逆序,交換n-1次。交換次數比冒泡排序少多了,由於交換所需CPU時間比比較所需的CPU時間多,n值較小時,選擇排序比冒泡排序快。選擇排序的賦值操作介於 0 和 3 (n - 1) 次之間。以爲每次交換兩個數都要進行3次交換操作。由於選擇排序會改變數的相對位置,所以選擇排序不是一個穩定的排序算法,例如: { 2, 2, 1}, 第一次選的時候變成 { 1, 2, 2 }, 兩個2的次序就變了
直接選擇排序的平均時間複雜度爲O(n2)。

3.直接插入排序:

插入排序:首先將第一個元素視爲一個集合,然後將後面的一個元素插入此集合並使集合有序,然後依次將後面的元素按照同樣的方法插入到集合中,注意每次插入完成後集合都是有序的。插入排序(Insertion Sort)的算法描述是一種簡單直觀的排序算法。它的工作原理是通過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入插入排序在實現上,通常採用in-place排序(即只需用到O(1)的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,爲最新元素提供插入空間。每次處理就是將無序數列的第一個元素與有序數列的元素從後往前逐個進行比較,找出插入位置,將該元素插入到有序數列的合適位置中。基本插入排序的時間複雜度爲O(n的平方),屬於穩定排序的一種(通俗地講,就是兩個相等的數不會交換位置) 。

bool InsertSort(int arr[],int len)
{
	if(NULL == arr || len<=0)
		return false;
	int i,j,currentInsertValue;
	for(i=1; i<len; i++)
	{
		currentInsertValue = arr[i];
		for(j=i-1; j>=0; j--)
		{
			if(currentInsertValue < arr[j])
			{
				arr[j+1] = arr[j]
			}
			else
				break;
		}
		arr[j+1] = currentInsertValue;
	}
	return true;
}

算法的時間複雜度
如果目標是把n個元素的序列升序排列,那麼採用插入排序存在最好情況和最壞情況。最好情況就是,序列已經是升序排列了,在這種情況下,需要進行的比較操作需(n-1)次即可。最壞情況就是,序列是降序排列,那麼此時需要進行的比較共有n(n-1)/2次。插入排序的賦值操作是比較操作的次數加上 (n-1)次。平均來說插入排序算法的時間複雜度爲O(n^2)。因而,插入排序不適合對於數據量比較大的排序應用。但是,如果需要排序的數據量很小,例如,量級小於千,那麼插入排序還是一個不錯的選擇。
 直接插入排序屬於穩定的排序,時間複雜性爲o(n^2),空間複雜度爲O(1)。

4.快速排序:

快速排序(Quicksort)是對冒泡排序的一種改進。由C. A. R. Hoare在1962年提出。它的基本思想是:通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然後再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。設要排序數組是A[0]……A[N-1],首先任意選取一個數據(通常選用第1個數據)作爲關鍵數據,然後將所有比它小的數都放到它前面,所有比它大的數都放到它後面,這個過程稱爲一趟快速排序。值得注意的是,快速排序不是一種穩定的排序算法,也就是說,多個相同的值的相對位置也許會在算法結束時產生變動。
#include <iostream>
using namespace std;

/*進行一趟快速排序,將第一個元素作爲基準。返回此趟拍完序的基準應該處的位置
此函數還進行一趟排序處理,將大於基準的元素都放入基準元素的右方,小於基準的元素放入基準的左方。*/
int Position(int arr[],int left,int right)
{
	//取最左邊的元素爲基準元素
	int base = arr[left];
	while(left<right)
	{
		//找到右邊第一個小於基準的數將他放入左邊
		while(left<right && base<=arr[right])
			right--;
		arr[left] = arr[right];
		//找到左邊第一個大於基準的數,將它放入右邊
		while(left<right && base>=arr[left])
			left++;
		arr[right] = arr[left];
	}
	//最後將基準值放入該插入的位置,並返回此位置
	arr[left] = base;
	return left;
}

bool QuickSort(int arr[],int left,int right)
{
	if(NULL==arr || right<=left)
		return false;
	int pos;
	if(left<right)
	{
		//以基準元素將數組分爲兩部分,獲得基準元素在數組中的位置
		pos = Position(arr,left,right);
		//遞歸排序數組左部分
		QuickSort(arr,left,pos-1);
		//遞歸排序數組右部分
		QuickSort(arr,pos+1,right);
	}
	return true;
}

int main()
{
	int arr[] = {5};
	int len = sizeof(arr)/sizeof(int);
	int i;
	for(i=0; i<len; i++)
		cout<<arr[i]<<" ";
	cout<<endl;
	if(QuickSort(arr,0,len-1))
	{
		for(i=0; i<len; i++)
			cout<<arr[i]<<" ";
		cout<<endl;
	}
	return 0; 
}

快速排序算法分析:

快速排序的時間主要耗費在劃分操作上,對長度爲 k 的區間進行劃分,共需 k-1 次關鍵字的比較。

 

最壞時間複雜度:最壞情況是每次劃分選取的基準都是當前無序區中關鍵字最小(或最大)的記錄,劃分的結果是基準左邊的子區間爲空(或右邊的子區間爲空),而劃分所得的另一個非空的子區間中記錄數目,僅僅比劃分前的無序區中記錄個數減少一個。因此,快速排序必須做 n-1 次劃分,第 i 次劃分開始時區間長度爲 n-i-1, 所需的比較次數爲 n-i(1<=i<=n-1), 故總的比較次數達到最大值 Cmax =n(n-1)/2=O(n^2) 。如果按上面給出的劃分算法,每次取當前無序區的第 1 個記錄爲基準,那麼當文件的記錄已按遞增序(或遞減序)排列時,每次劃分所取的基準就是當前無序區中關鍵字最小(或最大)的記錄,則快速排序所需的比較次數反而最多。

 

最好時間複雜度:在最好情況下,每次劃分所取的基準都是當前無序區的“中值”記錄,劃分的結果與基準的左、右兩個無序子區間的長度大致相等。總的關鍵字比較次數爲 O(n×lgn)

 

用遞歸樹來分析最好情況下的比較次數更簡單。因爲每次劃分後左、右子區間長度大致相等,故遞歸樹的高度爲 O(lgn), 而遞歸樹每一層上各結點所對應的劃分過程中所需要的關鍵字比較次數總和不超過 n,故整個排序過程所需要的關鍵字比較總次數C(n)=O(n×lgn) 。因爲快速排序的記錄移動次數不大於比較的次數,所以快速排序的最壞時間複雜度應爲 O(n^2 ),最好時間複雜度爲 O(n×lgn)

 

基準關鍵字的選取:在當前無序區中選取劃分的基準關鍵字是決定算法性能的關鍵。 ①“三者取中”的規則,即在當前區間裏,將該區間首、尾和中間位置上的關鍵字比較,以三者之中值所對應的記錄作爲基準,在劃分開始前將該基準記錄和該區的第個記錄進行交換,此後的劃分過程與上面所給的 Partition 算法完全相同。 ② 取位於 low  high 之間的隨機數k(low<=k<=high),  R[k] 作爲基準;選取基準最好的方法是用一個隨機函數產生一個位於 low  high 之間的隨機數k(low<=k<=high),  R[k] 作爲基準 , 這相當於強迫 R[low..high] 中的記錄是隨機分佈的。用此方法所得到的快速排序一般稱爲隨機的快速排序。隨機的快速排序與一般的快速排序算法差別很小。但隨機化後,算法的性能大大提高了,尤其是對初始有序的文件,一般不可能導致最壞情況的發生。算法的隨機化不僅僅適用於快速排序,也適用於其他需要數據隨機分佈的算法。

 

平均時間複雜度:儘管快速排序的最壞時間爲 O(n^2 ), 但就平均性能而言,它是基於關鍵字比較的內部排序算法中速度最快的,快速排序亦因此而得名。它的平均時間複雜度爲 O(n×lgn)

 

空間複雜度:快速排序在系統內部需要一個棧來實現遞歸。若每次劃分較爲均勻,則其遞歸樹的高度爲 O(lgn), 故遞歸後所需棧空間爲 O(lgn) 。最壞情況下,遞歸樹的高度爲 O(n), 所需的棧空間爲 O(n) 

 

穩定性:

快速排序是不穩定的


5.歸併排序:

來看歸併排序,其的基本思路就是將數組分成二組A,B,如果這二組組內的數據都是有序的,那麼就可以很方便的將這二組數據進行排序。如何讓這二組組內數據有序了?

可以將A,B組各自再分成二組。依次類推,當分出來的小組只有一個數據時,可以認爲這個小組組內已經達到了有序,然後再合併相鄰的二個小組就可以了。這樣通過先遞的分解數列,再合數列就完成了歸併排序。

使用二路合併排序法進行排序時,需要佔用較大的輔助空間,輔助空間的大小與待排序列一樣多。

程序代碼如下:
//將有序的兩個序列first到mid 和mid到last 合併
void MergeArray(int arr[],int first, int mid,int last,int tmp[])
{
         int i = first;
         int j = mid + 1;
         int k = 0;

         while(i<=mid && j<=last)
        {
                 if(arr[i] < arr[j])
                        tmp[k++] = arr[i++];
                 else
                        tmp[k++] = arr[j++];
        }

         while(i<=mid)
                tmp[k++] = arr[i++];
         while(j<=last)
                tmp[k++] = arr[j++];
         for(i=0; i<k; i++)
                arr[first+i] = tmp[i]; //注意這裏是 arr[first+i] = tmp[i];不是arr[i] = tmp[i];
}
//合併平排序的核心函數
void MergeSortCore(int arr[],int first, int last,int tmp[])
{
         int mid;
         if(first < last)
        {
                mid = (first+last)/2;
                MergeSortCore(arr,first,mid,tmp);//排序前半序列
                MergeSortCore(arr,mid+1,last,tmp);//排序後半序列
                MergeArray(arr,first,mid,last,tmp);//合併前半和後半序列
        }
}

//在此函數中調用MergeSortCore函數
bool MergeSort(int arr[],int len)
{
         if(NULL == arr || len<=0)
                 return false ;
         int* tmp = new int[len];
         if(!tmp)
                 return false ;
        MergeSortCore(arr,0,len-1,tmp);
         delete[] tmp;
         return true ;
}

有的書上是在mergearray()合併有序數列時分配臨時數組,但是過多的new操作會非常費時。因此作了下小小的變化。只在MergeSort()中new一個臨時數組。後面的操作都共用這一個臨時數組。
對於N個元素的數組來說, 如此劃分需要的層數是以2爲底N的對數, 每一層中, 每一個元素都要複製到結果數組中, 並複製回來, 所以複製2N次, 那麼對於歸併排序,它的時間複雜度爲O(N*logN), 而比較次數會少得多, 最少需要N/2次,最多爲N-1次, 所以平均比較次數在兩者之間. 它的主要問題還是在於在內存中需要雙倍的空間.
二、算法分析
1、穩定性
       歸併排序是一種穩定的排序。
2、存儲結構要求
      可用順序存儲結構。也易於在鏈表上實現。
3、時間複雜度
      對長度爲n的文件,需進行 趟二路歸併,每趟歸併的時間爲O(n),故其時間複雜度無論是在最好情況下還是在最壞情況下均是O(nlgn)。
4、空間複雜度
      需要一個輔助向量來暫存兩有序子文件歸併的結果,故其輔助空間複雜度爲O(n),顯然它不是就地排序。
  注意:
      若用單鏈表做存儲結構,很容易給出就地的歸併排序
歸併算法將兩個有序的數組合併到一個數組中並使之有序,這兩個數組並不一定相同大小,但需要一個額外的數組存放歸併結果。算法比較兩個數組相同位置的元素,將小的放入結果數組中,如此往復,如果其中一個先到達末尾,則將另外一個剩下部分放入結果數組中。 
   歸併排序將數組不斷劃分, 第一次分成兩半, 第二次分成四份, 如此直到得到只有一個元素的數組返回, 假定一個元素是有序的, 然後將兩個數據項歸併到兩個元素的有序數組中, 再次返回, 將這一對兩個元素的數組歸併到一個四個元素的數組中, 返回最外層的時候, 這個數組將會有兩個分別有序的子數組, 再次歸併則完成排序.


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