編程珠璣——快速排序總結

快速排序

快速排序是二十世紀最偉大的10大算法之一。如何寫好一個快速排序通常不是一件容易的事,同時面試中或者實際應用中也經常要用到快速排序。

編程珠璣第11章專門介紹了快速排序,涉及到了基本的算法思想實現和一些優化,這裏進行簡單的總結以便以後快速的回憶。


基本思想

快速排序用到了分治,它的基本思想是:通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然後再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。

我們直觀上可以想到式子:T(n)=2T(n/2)+On ,從而得到時間複雜度爲O(nlgn) ,好於O(n2) 的時間複雜度。

實現

至於具體怎麼分割數組,《編程珠璣》首先提到了一種方法,一趟快速排序流程如下:

  • 設置兩個變量l、y,[l,r]表示要排序的數組範圍,排序開始的時候:l=0,r=N-1;
  • 以範圍內的第一個數組元素作爲關鍵比較數據,賦值給pivot,即pivot=A[l];
  • 設index=l, 從i=l+1開始搜索,即由左開始向右搜索(i++),若找到一個小於key的值A[i],將A[i]和A[++index]互換;
  • i遍歷到範圍[l,r]的最右邊r之後,再將pivot元素A[l]與A[index]互換(index此時指向最後一個小於pivot的元素的位置)

整個過程也是比較教科書式的,爲了直觀一些,舉個例子int A[]={3,4,5,1,2,6};那麼整個排序過程如下

  • 開始,l=0,r=5,pivot = A[0]=3;第一次分割之後數組如下:
    這裏寫圖片描述

  • 分割之後,分爲兩塊左邊,l=0,r=1; 右邊l=3,r=5. 兩邊進行第二次分割之後數組如下:
    這裏寫圖片描述
    最後再將l=4,r=5的數組快進行partition排序,即完成了整個數組的排序。

實現代碼如下:

void quicksort(int a[],int l,int r)
{
    if(l>=r) return;
    int pivot = a[l];
    int index = l;
    for(int i=l+1;i<=r;i++)
    {
        if(a[i]<pivot)
            swap(a[++index],a[i]);
    }
    swap(a[l],a[index]);
    quicksort(a,l,index-1);
    quicksort(a,index+1,r);
}

特殊情況

上述講了一般情況下的快排思想,平均時間複雜度爲Onlgn 。但是一些特殊情況的輸入下仍然會導致時間複雜度爲On2

全是重複元素

比如數組 a={3,3,3,3,3,3}.此時可以看到,第一次我們將數組分成長度1和5的兩塊;之後將長度爲5的數組塊又分成長度1和長度4的數組塊….沒有達到分治的效果。

此時時間複雜度爲Tn=Tn1+On ,顯然就是On2 的複雜度了。針對這樣的情況,我們可以稍微改變快排的思路,每一趟的快排前兩步和上述的快排一樣

  1. 和上述一樣;
  2. 和上述一樣;
  3. 設i=l+1,j=r;
  4. 從i開始向後搜索,即由左開始向右搜索(i++),找到第一個大於等於key的值A[i];從j開始向前搜索,即由右開始向左搜索(j–),找到第一個小於等於key的值A[i];若i>j,終止此步,否則將A[j]和A[i]互換,重複此步驟直到終止;
  5. 交換元素A[l]與元素A[j]。

這樣所有元素都相同的情況也能基本實現每次partition二分,此時時間複雜都也變成了ONlgN

實現代碼如下:

void quicksort(int a[],int l,int r)
{
    if(l>=r) return;

    int pivot = a[l];
    int i=l;
    int j=r+1;

    while(1)
    {
        do{i++;}while((i<=r)&&a[i]<pivot);
        while(a[--j]>pivot);
        if(i>j) break;
        swap(a[i],a[j]);
    }
    swap(a[l],a[j]);
    quicksort(a,l,j-1);
    quicksort(a,j+1,r);
}

輸入已經排好序

面對這種情況,上述已經優化的快排代碼又會出現ON2 的時間複雜度。因爲它會首先圍繞最小的元素劃分,將數組大小分爲1和N-1,然後是圍繞第二小的元素劃分,依此類推,還是需要ON2 的時間。

解決這種情況的方法很簡單就是隨機選擇劃分元素,通過把A[l]與A[l,r]中的一個隨機項交換來實現這一點。只需要加兩行代碼:

int num=rand()%(r-l+1) + l;
swap(a[l],a[num]);

這樣期望的運行時間就爲O(NlgN) 啦。

優化

《編程珠璣》課後練習11.11提出了一種快排的小優化點,如下圖:
這裏寫圖片描述
之前我們都沒有單獨考慮過重複的元素,若是將數組中和比較元素t相同的所有元素都放到中間位置,那麼之後進行下一趟快排的時候就會多過濾掉一些元素,起到了優化的效果。實現代碼如下:

void quciksort(int a[],int l,int r)
{
    if(l>=r) return;
    int m=n=r+1;
    int pivot = a[l];
    for(int i=r;i>=l;i--)
    {
       while(a[i]<pivot) i--;
       if(a[i]==pivot)
          swap(a[--m],a[i]);
       else{//a[i]>pivot
          swap(a[--n],a[i]);
          swap(a[--m],a[i]);
       }
    }
   quciksort(a,l,m-1);
   quciksort(a,n,r);
} 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章