QuickSort

快速排序 Quick Sort

我們已經知道,在決策樹計算模型下,任何一個基於比較來確定兩個元素相對位置的排序算法需要Ω(nlogn)計算時間。如果我們能設計一個需要O(n1ogn)時間的排序算法,則在漸近的意義上,這個排序算法就是最優的。許多排序算法都是追求這個目標。

下面介紹快速排序算法,它在平均情況下需要O(nlogn)時間。這個算法是由C.A.R.Hoare發明的。

算法的基本思想

快速排序的基本思想是基於分治策略的。對於輸入的子序列L[p..r],如果規模足夠小則直接進行排序,否則分三步處理:

  • 分解(Divide):將輸入的序列L[p..r]劃分成兩個非空子序列L[p..q]和L[q+1..r],使L[p..q]中任一元素的值不大於L[q+1..r]中任一元素的值。
  • 遞歸求解(Conquer):通過遞歸調用快速排序算法分別對L[p..q]和L[q+1..r]進行排序。
  • 合併(Merge):由於對分解出的兩個子序列的排序是就地進行的,所以在L[p..q]和L[q+1..r]都排好序後不需要執行任何計算L[p..r]就已排好序。

這個解決流程是符合分治法的基本步驟的。因此,快速排序法是分治法的經典應用實例之一。

算法的實現

算法Quick_Sort的實現:

注意:下面的記號L[p..r]代表線性表L從位置p到位置r的元素的集合,但是L並不一定要用數組來實現,可以是用任何一種實現方法(比如說鏈表),這裏L[p..r]只是一種記號。

procedure Quick_Sort(p,r:position;var L:List);
const
e=12;
var
q:position;
begin
1  if r-p<=e then Insertion_Sort(L,p,r)//若L[p..r]足夠小則直接對L[p..r]進行插入排序
     else begin
2            q:=partition(p,r,L);//將L[p..r]分解爲L[p..q]和L[q+1..r]兩部分
3            Quick_Sort(p,q,L);  //遞歸排序L[p..q]
4            Quick_Sort(q+1,r,L);//遞歸排序L[q+1..r]		
          end;
end;

對線性表L[1..n]進行排序,只要調用Quick_Sort(1,n,L)就可以了。算法首先判斷L[p..r]是否足夠小,若足夠小則直接對L[p..r]進行排序,Sort可以是任何一種簡單的排序法,一般用插入排序。這是因爲,對於較小的表,快速排序中劃分和遞歸的開銷使得該算法的效率還不如其它的直接排序法好。至於規模多小纔算足夠小,並沒有一定的標準,因爲這跟生成的代碼和執行代碼的計算機有關,可以採取試驗的方法確定這個規模閾值。經驗表明,在大多數計算機上,取這個閾值爲12較好,也就是說,當r-p<=e=12即L[p..r]的規模不大於12時,直接採用插入排序法對L[p..r]進行排序(參見 Sorting and Searching Algorithms: A Cookbook)。當然,比較方便的方法是取該閾值爲1,當待排序的表只有一個元素時,根本不用排序(其實還剩兩個元素時就已經在Partition函數中排好序了),只要把第1行的if語句該爲if p=r then exit else ...。這就是通常教科書上看到的快速排序的形式。

注意:算法Quick_Sort中變量q的值一定不能等於r,否則該過程會無限遞歸下去,永遠不能結束。因此下文中在partition函數里加了限制條件,避免q=r情況的出現。

算法Quick_Sort中調用了一個函數partition,該函數主要實現以下兩個功能:

  1. 在L[p..r]中選擇一個支點元素pivot;
  2. 對L[p..r]中的元素進行整理,使得L[p..q]分爲兩部分L[p..q]和L[q+1..r],並且L[p..q]中的每一個元素的值不大於pivot,L[q+1..r]中的每一個元素的值不小於pivot,但是L[p..q]和L[q+1..r]中的元素並不要求排好序

快速排序法改進性能的關鍵就在於上述的第二個功能,因爲該功能並不要求L[p..q]和L[q+1..r]中的元素排好序。

函數partition可以實現如下。以下的實現方法是原地置換的,當然也有不是原地置換的方法,實現起來較爲簡單,這裏就不介紹了。

function partition(p,r:position;var L:List):position;
var
pivot:ElementType;
i,j:position;
begin
1  pivot:=Select_Pivot(p,r,L); //在L[p..r]中選擇一個支點元素pivot
2  i:=p-1;
3  j:=r+1;
4  while true do
     begin
5      repeat j:=j-1 until L[j]<=pivot;  //移動左指針,注意這裏不能用while循環
6      repeat i:=i+1 until L[i]>=pivot;  //移動右指針,注意這裏不能用while循環
7      if i< j then swap(L[i],L[j])  //交換L[i]和L[j]
8              else if j<>r then return j        //返回j的值作爲分割點
9                           else return j-1;     //返回j前一個位置作爲分割點
     end;
end;

該算法的實現很精巧。其中,有一些細節需要注意。例如,算法中的位置i和j不會超出A[p..r]的位置界,並且該算法的循環不會出現死循環,如果將兩個repeat語句換爲while則要注意當L[i]=L[j]=pivot且i<j時i和j的值都不再變化,會出現死循環。

另外,最後一個if..then..語句很重要,因爲如果pivot取的不好,使得Partition結束時j正好等於r,則如前所述,算法Quick_Sort會無限遞歸下去;因此必須判斷j是否等於r,若j=r則返回j的前驅。

以上算法的一個執行實例如圖1所示,其中pivot=L[p]=5:

圖1 Partition過程的一個執行實例

Partition對L[p..r]進行劃分時,以pivot作爲劃分的基準,然後分別從左、右兩端開始,擴展兩個區域L[p..i]和L[j..r],使得L[p..i]中元素的值小於或等於pivot,而L[j..r]中元素的值大於或等於pivot。初始時i=p-1,且j=i+1,從而這兩個區域是空的。在while循環體中,位置j逐漸減小,i逐漸增大,直到L[i]≥pivot≥L[j]。如果這兩個不等式是嚴格的,則L[i]不會是左邊區域的元素,而L[j]不會是右邊區域的元素。此時若i在j之前,就應該交換L[i]與L[j]的位置,擴展左右兩個區域。 while循環重複至i不再j之前時結束。這時L[p..r]己被劃分成L[p..q]和L[q+1..r],且滿足L[p..q]中元素的值不大於L[q+1..r]中元素的值。在過程Partition結束時返回劃分點q。

尋找支點元素select_pivot有多種實現方法,不同的實現方法會導致快速排序的不同性能。根據分治法平衡子問題的思想,我們希望支點元素可以使L[p..r]儘量平均地分爲兩部分,但實際上這是很難做到的。下面我們給出幾種尋找pivot的方法。

  1. 選擇L[p..r]的第一個元素L[p]的值作爲pivot;
  2. 選擇L[p..r]的最後一個元素L[r]的值作爲pivot;
  3. 選擇L[p..r]中間位置的元素L[m]的值作爲pivot;
  4. 選擇L[p..r]的某一個隨機位置上的值L[random(r-p)+p]的值作爲pivot;

按照第4種方法隨機選擇pivot的快速排序法又稱爲隨機化版本的快速排序法,在下面的複雜性分析中我們將看到該方法具有平均情況下最好的性能,在實際應用中該方法的性能也是最好的。

 

//快速排序一
private static void quicksort(String[] a,int lo0,int hi0){
  
int lo=lo0;
  
int hi=hi0;
  
  
if(lo>=hi)
  
return;
  
//取中間元素
  String mid=a[(lo+hi)/2];
  
while(lo<hi){
         
//從左邊找到第一個a[lo]>mid的元素
         while(lo<hi&&a[lo].compareTo(mid)<0){
    
          lo
++;
          }

      
//從右邊找到第一個a[hi]<mid的元素 
      while(lo<hi&&a[hi].compareTo(mid)>0){
      
          hi
--;}

     
//如果lo在hi左邊則交換2個元素 滿足小的在左 大的在右
     if(lo<hi){
     String T
=a[lo];
     a[lo]
=a[hi];
     a[hi]
=T;
     
//繼續找
     lo++;
     hi
--;
  }

  
  
if(hi<lo){
  
int T=hi;
  hi
=lo;
  lo
=T;
  }

  
//快排左
  quicksort(a,lo0,lo);
  
//快排右
  quicksort(a,lo==lo0?lo+1:lo,hi0);
  }

  }


  
     
//快速排序之二
     
//交換a[i],a[j]的值
     public void swap(int[] a,int i,int j)
       
{
        
int temp = a[i];
        a[i]
=a[j];
        a[j] 
= temp;
       }

       
//找出中間點位置 使得中間點左邊的值《=中間點〈=中間點右邊的值
       public int partition(int[] a,int low,int high)
       
{
          
//中間點元素值
         int pivot;
        
//中間點位置
         int p_pos,tmp,i,j;
        
//假設中間點位置在第一位
        p_pos = low;
       
//假設中間點值爲第一位元素的值 注意中間點元素的值是確定的 假設第一位 但是中間點元素的正確位置需要我們找
        pivot = a[p_pos];
        
//循環數組 
        for(i=low+1;i<=high;i++)
        
{
          
//從第二位開始查找 發現i位元素小於中間點值
         if(a[i]<pivot)
         
{
          
//中間點位置右移一位
          p_pos++;
         
//交換此刻中間點位置所在元素與第i位元素的值
          swap(a,p_pos,i);
         }

        }

        
//此時已經找到中間點元素所在正確位置 將中間點元素位置的值與假設第一位爲中間點值交換 
        swap(a,low,p_pos);
        
//返回該中間點位置
       return p_pos;
       }

       
//快排
       public void quicksort(int[] a,int low,int high)
       
{
        
        
int pivot;
        
//如果低位值還小於高位值
        if(low<high)
        
{
        
//得到低位與高位的中間點
         pivot = partition(a,low,high);
         
//第歸快排這個中間點左邊
         quicksort(a,low,pivot-1);
         
//第歸快排這個中間點右邊
         quicksort(a,pivot+1,high);
        }

        
       }

 

原文

http://algorithm.diy.myrice.com/algorithm/commonalg/sort/internal_sorting/quick_sort/quick_sort.htm

發佈了1 篇原創文章 · 獲贊 0 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章