排序算法分析

原文:http://blog.csdn.net/wangxiaojun911/article/details/4581621程序員

排序(Sorting)是算法應用中最重要的工具。據統計,計算機運行時間的四分之一都耗費在了排序上。判斷排序算法優劣的方法是進行時間和空間效率分析。分析時間效率的三個重要記號是O、Ω和Θ。

O記號起源於P.Bachmann在1892年發表的一篇數論文章。O在一個常數因子內給出某函數的一個上界。對一個函數g(n), O(g(n))表示一個函數集合。O(g(n)) = {f(n):存在正常數c和n0,使對於全部的n>=n0,有0<=f(n)<=cg(n)}。

在使用O記號進行效率統計以前,先假定分析的機器模型。咱們使用單處理器,隨機存取器(random-access machine, RAM)。即指令一條一條執行,沒有併發操做。進一步假定每次運算只消耗一條指令。把每條指令記爲1次(time)。好比:
int n = 10; //1 time
for(int i=0;i < n;i++)//n times
{
...//c條語句

該算法總的運行時間就是每一條語句執行次數之和1+c*n。一般,算法運行時間(或稱時間複雜度)不只與數據規模n相關,而且與數據的輸入形式相關。例如,在一些排序算法中,若輸入的數組已經排好序了,那麼世紀運行時間會大大減小(或增長)。因此,咱們在不可以明確輸入數據狀態的狀況下,定義算法的「最壞狀況」和「最好狀況」。在實際分析中,通常考察最壞狀況的運行時間,即對於規模爲n的狀況下,算法的最長運行時間。這是由於最壞狀況在應用當中頻繁出現,並且即便考察一個隨機的數據輸入,它的結果與最壞狀況同樣差(好比都是輸入規模的二次函數)。算法

很明顯,用來表示上界的O記號能夠很天然的描述這種最壞狀況。設算法最壞運行時間爲f(n),能夠找到一個簡單的函數g(n)使f(n)能夠用O(g(n))來描述。一般咱們說「運行時間是O(n^2)」便表示對於f(n),無論n爲什麼值,也無論具體是怎樣輸入,它都有最壞運行時間O(n^2)。一般選取的g(n)有1, n, n*lgn, n^2 和 n^3, 它們在O記號下的時間複雜度依次遞增。
(注1:咱們在這裏說的lgn,是以2爲底的對數)
(注2:一般,指數函數比多項式函數增加快,多項式又比對數增加快。對於對數來講,無論底數是多少,其漸進線都是同樣的。Exponential functions grow faster than polynomial functions, which grow faster than polylogarithmic functions)api

相似,Ω記號給出了函數的漸進下界。即對一個函數g(n), Ω(g(n))= {f(n):存在正常數c和n0,使對於全部的n>=n0,有0<=cg(n)<=f(n)}。Θ記號則最強,它同時表示上下界。Θ由Knuth提出,是最準確的記號。可是,許多人至今仍然偏心使用O記號。數組

注意,咱們假定g(n)是漸進非負函數,以上記號才能夠成立。
下面分析一些具體的排序算法。

1. 插入排序
插入排序如同打牌,一次從桌上摸起一張牌,並將它插入手中牌中的正確位置上。此算法在排序過程當中將牌分紅了2個部分:還在桌上的牌(未排序)和手中的牌(有序)。排序當中有一個比較過程以及在插入後將插入項以後的牌向後移動的過程。在最壞狀況下,原始牌是逆序排列的,那麼每插入一張牌,全部手中的牌都要向後移動一格。假設有n張牌,那麼移動的次數就是1+2+3+4+...+n = n*(n-1)/2次。即此算法是O(n^2)的。
程序實現:併發

[c-sharp]  view plain copy
 
  1. void insertionSort(int input[])  
  2. {  
  3.   int tmp,i;  
  4.   for(int j=1;j < MAX;j++)  
  5.   {  
  6.     tmp=input[j];  
  7.     i=j-1;  
  8.     while(i>=0 && tmp < input[i])  
  9.     {  
  10.          input[i+1]=input[i];  
  11.          i--;  
  12.     }  
  13.     input[i+1]=tmp;  
  14.   }  
  15. }  



2.冒泡排序
冒泡排序也許是實現最簡單的排序算法。時間複雜度也十分容易計算,也是O(n^2),並且當輸入數據自己有序的時候效率也不會提升!它惟一的用途大概就是用來測試一個程序員是否是弱智。dom

[cpp]  view plain copy
 
  1. void bubble(int *input)  
  2. {  
  3.   for(int i=0;i < (MAX-1);i++)  
  4.     for(int j=0;j<(MAX-1-i);j++)  
  5.       if(input[j]>input[j+1])  
  6.         swap(input[j],input[j+1]);  
  7. }  



3.快速排序
快速排序是一種遞歸算法:將原問題分紅若干個規模小可是結構類似的問題,遞歸地解決這些問題,再合併結果,就獲得了原問題的解。快速排序首先在輸入數據中選擇一個元素做爲「主元」,而後依據主元把數據分紅2個部分,前一個部分的每一個元素都比主元小,後一個部分都比主元大。而後分別在兩個部分快速排序,直到不能再分爲止。這樣整個數據就有序了。函數

[cpp]  view plain copy
 
  1. void qsort(int input[],int start,int end)  
  2. {  
  3.   int i,j;   
  4.   if(start < end)  
  5.   {  
  6.     i=start;j=end+1;   
  7.     while(1){  
  8.       do i++;   
  9.       while(!(input[i]>=input[start]||i==end));  
  10.       do j--;   
  11.       while(!(input[j]<=input[start]||j==start));   
  12.       if(i < j)  
  13.         swap(input[i],input[j]);  
  14.       else  
  15.         break;   
  16.     }  
  17.     swap(input[start],input[j]);  
  18.     qsort(input,start,j-1);  
  19.     qsort(input,j+1,end);  
  20.   }  
  21. }  

 

另外一個版本的快速排序:工具

 

[cpp]  view plain copy
 
  1. int Partition(int input[],int start,int end)  
  2. {  
  3.      int x = input[end];  
  4.      int i = start - 1;  
  5.      for(int j = start; j < end; j++)  
  6.      {  
  7.          if( input[j] <= x)  
  8.          {  
  9.              i++;  
  10.              swap(input[i],input[j]);  
  11.          }   
  12.      }  
  13.      swap(input[i+1],input[end]);  
  14.      return i+1;  
  15. }    
  16. void qsort(int input[],int start,int end)  
  17. {  
  18.      if(start < end)   
  19.      {  
  20.          int q = Partition(input, start, end);  
  21.          qsort(input, start, q-1);  
  22.          qsort(input, q+1, end);       
  23.      }   
  24. }  


考察快速排序的最壞效率,要考慮兩種極端狀況。1、假如每次劃分的兩個部分的元素相等,那麼總遞歸時間T(n)爲2*T(n/2)+n。其中後面的n爲劃分的開銷。可證實,T(n)是O(n*logn)的(事實上,這個解是先猜想,再靠數學概括法加以證實的)。2、假如劃分嚴重不對稱,即分紅一邊是1個元素,一邊是n-1個元素。那麼有T(n)=T(1)+T(n-1)+n,實際上與插入排序同樣,是一個算術級數!也就是說,在這種狀況下,快速排序蛻變成了插入排序,因此時間複雜度爲O(n^2)。
快速排序最神奇的地方在於,對於隨機輸入的數據,它可以自動調整到好的劃分上去,其運行時間與最佳時間很是類似。例如,產生一個99:1的劃分。看似很是的不平衡吧。可是能夠證實,它的運行時間也是O(n*lgn)!
總的來講,雖然算法的時間是O(n^2),可是快速排序在絕大多數狀況下都能達到O(n*logn)的效率,使之成爲了居家旅行,殺人越貨當中不可或缺的排序利器。

4.合併排序
前面提到,既然把數據平均劃分紅兩個部分分別排序,就能夠達到很好的效率O(n*lgn),那麼,是否是存在這樣一種算法呢?合併算法是這樣一種算法:將n個元素分紅各含n/2個元素的子序列,而後對兩個子序列分別排序。測試

[cpp]  view plain copy
 
  1. void mergeSort(int *input,int start,int end)  
  2. {  
  3.   int mid;  
  4.   if(start < end)  
  5.   {  
  6.   mid=(start+end)/2;  
  7.   mergeSort(input,start,mid);  
  8.   mergeSort(input,mid+1,end);  
  9.   merge(input,start,mid,end);  
  10.   }  
  11. }  
  12.   
  13. void merge(int *input,int start,int mid, int end) //輔助函數  
  14. {  
  15.   int n1=mid-start+1;   
  16.   int n2=end-mid;  
  17.   
  18.   int *L=new int[n1+1];   
  19.   int *R=new int[n2+1];  
  20.   for(int i=0;i < n1;i++)  
  21.     L[i]=input[start+i];  
  22.   for(int i=0;i < n2;i++)  
  23.     R[i]=input[mid+1+i];  
  24.   L[n1]=INF;  
  25.   R[n2]=INF;  
  26.   
  27.   int i=0,j=0;   
  28.   for(int k=start;k<=end;k++)  
  29.   {  
  30.     if(L[i]<=R[j])  
  31.       input[k]=L[i++];  
  32.     else  
  33.       input[k]=R[j++];  
  34.   }  
  35.   
  36.   delete L,R;  
  37. }  

 

能夠看出時間複雜度爲T(n)=2*T(n/2)+n,也就是O(n*lgn)!但須要指出的是,合併排序算法必須創建輔助數組L和R,它們的規模如同n同樣線性增加。即它不是一個原地(in place)排序算法,在虛擬環境中不可以很好的工做。

5.堆排序
堆能夠被視爲一棵徹底二叉樹(除去最後一層之外就是一個滿二叉樹,最後一層結點從左到右開始填)。咱們在堆排序中使用「最大堆」(MAX-HEAP),即每一個結點的值都比它的父結點要小。固然也有「最小堆」,原理都是同樣的。
咱們把須要排序的數組看做一個堆,堆的每一個結點和數組中放該結點的那個元素對應。注意,堆的長度heap-size與數組長度length不盡相同,前者可能小於後者,任何在heap-size以後的元素都不屬於相應的堆。
堆具備徹底二叉樹的一些有趣性質:
設n0爲度爲0的結點總數(葉子結點);
n1爲度爲1的結點總數(徹底二叉樹中只有一個,或者沒有);
n2爲度爲2的結點總數。
如今咱們來求解葉子數量n0。
有n0+n1+n2=n---(1)
0*n0+1*n1+2*n2=n-1----(2)
消去n2,有n0=(n-n1+1)/2----(3)
根據式(3), 既然n1要麼爲1,要麼爲0,那麼葉子節點要麼等於n/2,要麼等於(n+1)/2。這意味着在整個堆中,有一半(當n1=1),或者略少於一半(當n1=0)的結點是有子結點的。且這些結點都集中在數組的低位部分。
如下函數對於制定的輸入input,使根爲i的子樹成爲最大堆。這是堆排序中最重要的操做,稱爲堆的保持。此算法將遍歷根爲1的全部子樹。在最壞的狀況下,子樹的底層剛好半滿,這時,須要遍歷的結點數爲:
n * (0+1+2+...+2^(n-1)) / (1+2+3+...+2^(n-1)+2^(n-1)) 
= n * (2^n - 1) / (2^n + 2^(n-1)-1) 
< n * 2/3.
即全部結點的三分之二。計算運行時間:T(n) <= T(2/3*n)+ Θ(1).可證實T(n)=O(lgn).ui

[cpp]  view plain copy
 
  1. void MaxHeapify(int *input,int i)  
  2. {  
  3.   int largest;  
  4.   int l = i*2;  
  5.   int r = i*2+1;  
  6.   if( l <= HeapSize && input[l] > input[i])  
  7.     largest = l;  
  8.   else  
  9.     largest = i;  
  10.   if( r <= HeapSize && input[r] > input[largest])  
  11.     largest = r;  
  12.   if(largest != i)  
  13.   {  
  14.     swap(input[i],input[largest]);  
  15.     MaxHeapify(input, largest);   
  16.   }  
  17. }   

 

針對一個數組創建堆。注意只要對有子結點的結點進行堆的保持就能夠了。能夠證實創建堆是O(n)的。

 

[cpp]  view plain copy
 
  1. int HeapSize = 0;  
  2. void BuildMaxHeap(int *input)  
  3. {  
  4.   HeapSize = MAX-1;  
  5.   for(int i = MAX/2; i >= 0; i--)  
  6.     MaxHeapify(input,i);  
  7. }   

 

如下是堆排序的調用算法。此過程調用了O(lgn)的堆保持函數並循環n-1次。即獲得了O(n*lgn)的時間複雜度。

[cpp]  view plain copy
 
  1. void HeapSort(int *input)  
  2. {  
  3.   BuildMaxHeap(input);  
  4.   for(int i = MAX-1; i > 0; i--) // no need to process when i = 0  
  5.   {  
  6.     swap(input[0],input[i]);  
  7.     HeapSize--;  
  8.     MaxHeapify(input,0);  
  9.     }  
  10. }  

堆排序總能達到O(n*lgn)的運行效率,就像合併排序同樣。堆排序又是一種原地排序算法,就像快速排序和插入排序同樣。所以,堆排序結合了以上幾種排序的優勢。可是,堆排序的實現不如快速排序的緊湊和簡單。有資料代表,快速排序在實際應用中優於堆排序。參考文獻:Thomas H. Cormen, Charles E. Leiserson, Introduction to Algorithms, 2ed

相關文章
相關標籤/搜索