一些常用的排序算法代碼(JAVA)

冒泡排序 Bubble Sort
最簡單的排序方法是冒泡排序方法。這種方法的基本思想是,將待排序的元素看作是豎着排列的“氣泡”,較小的元素比較輕,從而要往上浮。在冒泡排序算法中我們要對這個“氣泡”序列處理若干遍。所謂一遍處理,就是自底向上檢查一遍這個序列,並時刻注意兩個相鄰的元素的順序是否正確。如果發現兩個相鄰元素的順序不對,即“輕”的元素在下面,就交換它們的位置。顯然,處理一遍之後,“最輕”的元素就浮到了最高位置;處理二遍之後,“次輕”的元素就浮到了次高位置。在作第二遍處理時,由於最高位置上的元素已是“最輕”元素,所以不必檢查。一般地,第i遍處理時,不必檢查第i高位置以上的元素,因爲經過前面i-1遍的處理,它們已正確地排好序。這個算法可實現如下。

procedure Bubble_Sort(var L:List);
var
i,j:position;
begin
1 for i:=First(L) to Last(L)-1 do
2 for j:=First(L) to Last(L)-i do
3 if L[j]>L[j+1] then
4 swap(L[j],L[j+1]); //交換L[j]和L[j+1]
end;
java 代碼
 public static void sort(short array[]) {
  boolean change;
  for (int i = 1; i < array.length; i++) {
   change = false;
   for (int j = array.length - 1; j >= i; j--) {
    if (array[j] < array[j - 1]) {
     short o = array[j];
     array[j] = array[j - 1];
     array[j - 1] = o;
     change = true;
    }
   }
   if (!change) {
    break;
   }
  }
 }
上述算法將較大的元素看作較重的氣泡,每次最大的元素沉到表尾。其中First(L)和Last(L)分別表示線性表L的第一個元素和最後一個元素的位置,swap(x,y)交換變量x,y的值。上述算法簡單地將線性表的位置當作整數用for循環來處理,但實際上線性表可能用鏈表實現;而且上述算法將線性表元素的值當作其鍵值進行處理。不過這些並不影響表達該算法的基本思想。今後如果不加說明,所有的算法都用這種簡化方式表達。

容易看出該算法總共進行了n(n-1)/2次比較。如果swap過程消耗的時間不多的話,主要時間消耗在比較上,因而時間複雜性爲O(n2)。但是如果元素類型是一個很大的紀錄,則Swap過程要消耗大量的時間,因此有必要分析swap執行的次數。

顯然算法Bubble_Sort在最壞情況下調用n(n-1)/2次Swap過程。我們假設輸入序列的分佈是等可能的。考慮互逆的兩個輸入序列L1=k1,k2,..,kn和L2=kn,kn-1,..,k1。我們知道,如果ki>kj,且ki在表中排在kj前面,則在冒泡法排序時必定要將kj換到ki前面,即kj向前浮的過程中一定要穿過一次ki,這個過程要調用一次Swap。對於任意的兩個元素ki和kj,不妨設ki>kj,或者在L1中ki排在kj前面,或者L2在中ki排在kj前面,兩者必居其一。因此對於任意的兩個元素ki和kj,在對L1和L2排序時,總共需要將這兩個元素對調一次。n個元素中任取兩個元素有Cn2 種取法,因此對於兩個互逆序列進行排序,總共要調用Cn2 =n(n-1)/2次Swap,平均每個序列要調用n(n-1)/4次Swap。那麼算法Bubble_Sort調用Swap的平均次數爲n(n-1)/4。

可以對冒泡算法作一些改進,如果算法第二行的某次內循環沒有進行元素交換,則說明排序工作已經完成,可以退出外循環。可以用一個布爾變量來記錄內循環是否進行了記錄交換,如果沒有則終止外循環。

冒泡法的另一個改進版本是雙向掃描冒泡法(Bi-Directional Bubble Sort)。設被排序的表中各元素鍵值序列爲:

483 67 888 50 255 406 134 592 657 745 683

對該序列進行3次掃描後會發現,第3此掃描中最後一次交換的一對紀錄是L[4]和L[5]:

50 67 255 134 | 406 483 592 657 683 745 888

顯然,第3次掃描(i=3)結束後L[5]以後的序列都已經排好序了,所以下一次掃描不必到達Last(L)-i=11-4=7,即第2行的for 循環j不必到達7,只要到達4-1=3就可以了。按照這種思路,可以來回地進行掃描,即先從頭掃到尾,再從尾掃到頭。這樣就得到雙向冒泡排序算法:

procedure Bi-Directional_Bubble_Sort(var L:List);
var
low,up,t,i:position;
begin
1 low:=First(L);up:=Last(L);
2 while up>low do
begin
3 t:=low;
4 for i:=low to up-1 do
5 if L[i]>L[i+1] then
begin
6 swap(L[i],L[i+1]);
7 t:=i;
end;
8 up:=t;
9 for i:=up downto low+1 do
10 if L[i]< L[i-1] then
begin
11 swap(L[i],L[i-1]);
12 t:=i;
end;
13 low:=t;
end;
end;
java 代碼
  void sort(int a[]) throws Exception {
 int j;
 int limit = a.length;
 int st = -1;
 while (st < limit) {
     boolean flipped = false;
     st++;
     limit--;
     for (j = st; j < limit; j++) {
  if (a[j] > a[j + 1]) {
      int T = a[j];
      a[j] = a[j + 1];
      a[j + 1] = T;
      flipped = true;

  }
     }
     if (!flipped) {
  return;
     }
     for (j = limit; --j >= st;) {

  if (a[j] > a[j + 1]) {
      int T = a[j];
      a[j] = a[j + 1];
      a[j + 1] = T;
      flipped = true;
  }
     }
     if (!flipped) {
  return;
     }
 }
     }

算法利用兩個變量low和up記錄排序的區域L[low..up],用變量t 記錄最近一次交換紀錄的位置,4-7行從前向後掃描,9-12行從後向前掃描,每次掃描以後利用t所記錄的最後一次交換記錄的位置,並不斷地縮小需要排序的區間,直到該區間只剩下一個元素。

直觀上來看,雙向冒泡法先讓重的氣泡沉到底下,然後讓輕的氣泡浮上來,然後再讓較大氣泡沉下去,讓較輕氣泡浮上來,依次反覆,直到排序結束。

雙向冒泡排序法的性能分析比較複雜,目前暫缺,那位朋友知道請告訴我。

冒泡排序法和雙向冒泡排序法是原地置換排序法,也是穩定排序法,如果算法Bubble_Sort中第3行的比較條件L[j]>L[j+1]改爲L[j]>= L[j+1],則不再是穩定排序法。

選擇排序 Selection Sort
選擇排序的基本思想是對待排序的記錄序列進行n-1遍的處理,第i遍處理是將L[i..n]中最小者與L[i]交換位置。這樣,經過i遍處理之後,前i個記錄的位置已經是正確的了。

選擇排序算法可實現如下。

procedure Selection_Sort(var L:List);
var
i,j,s:position;
begin
1 for i:=First(L) to Last(L)-1 do
begin
2 s:=i;
3 for j:=i+1 to Last(L) do
4 if L[j]< L[s] then
5 s:=j; //記錄L[i..n]中最小元素的位置
6 swap(L[i],L[s]); //交換L[i],L[s]
end;
end;
算法Selection_Sort中裏面的一個for循環需要進行n-i次比較,所以整個算法需要


次比較。

顯而易見,算法Selection_Sort中共調用了n-1次swap過程。選擇排序法是一個原地置換排序法,也是穩定排序法。


插入排序 Insertion Sort
插入排序的基本思想是,經過i-1遍處理後,L[1..i-1]己排好序。第i遍處理僅將L[i]插入L[1..i-1]的適當位置,使得L[1..i]又是排好序的序列。要達到這個目的,我們可以用順序比較的方法。首先比較L[i]和L[i-1],如果L[i-1]≤ L[i],則L[1..i]已排好序,第i遍處理就結束了;否則交換L[i]與L[i-1]的位置,繼續比較L[i-1]和L[i-2],直到找到某一個位置j(1≤j≤i-1),使得L[j] ≤L[j+1]時爲止。圖1演示了對4個元素進行插入排序的過程,共需要(a),(b),(c)三次插入。


圖1 對4個元素進行插入排序

在下面的插入排序算法中,爲了寫程序方便我們可以引入一個哨兵元素L[0],它小於L[1..n]中任一記錄。所以,我們設元素的類型ElementType中有一個常量-∞,它比可能出現的任何記錄都小。如果常量-∞不好事先確定,就必須在決定L[i]是否向前移動之前檢查當前位置是否爲1,若當前位置已經爲1時就應結束第i遍的處理。另一個辦法是在第i遍處理開始時,就將L[i]放入L[0]中,這樣也可以保證在適當的時候結束第i遍處理。下面的算法中將對當前位置進行判斷。

插入排序算法如下:

procedure Selection_Sort(var L:List);
var
i,j:position;
v:ElementType;
begin
1 for i:=First(L)+1 to Last(L) do
begin
2 v:=L[i];
3 j:=i;
4 while (j<>First(L))and(L[j-1]< v) do //循環找到插入點
begin
5 L[j]:=L[j-1]; //移動元素
6 j:=j-1;
end;
7 L[j]:=v; //插入元素
end;
end;
java 代碼
  void sort(int a[]) throws Exception {
    int tmp; //The number currently being sorted is stored here while we make room for it
    int tmp2;//Used for swapping
    int j;
 
    for (int i=1; i<=a.length; i++) {
     
// Invariant: a[0..i-1] sorted
      tmp=a[i];
      for (j=i-1; j>=0 && a[j]>tmp; j--) {
        a[j+1]=a[j];          
      }
      //Now we've found a[i]'s place
      a[j+1]=tmp;
    }
  } //end of sort
下面考慮算法Insertion_Sort的複雜性。對於確定的i,內while循環的次數爲O(i),所以整個循環體內執行了∑O(i)=O(∑i),其中i從2到n。即比較次數爲O(n2)。如果輸入序列是從大到小排列的,那麼內while循環次數爲i-1次,所以整個循環體執行了∑(i-1)=n(n-1)/2次。由此可知,最壞情況下,Insertion_Sort要比較Ω(n2)次。

如果元素類型是一個很大的紀錄,則算法第5行要消耗大量的時間,因此有必要分析移動元素的次數。經過分析可知,平均情況下第5行要執行n(n-1)/4次,分析方法與冒泡排序的分析相同。

如果移動元素要消耗大量的時間,則可以用鏈表來實現線性表,這樣Insertion_Sort可以改寫如下(當然前一個算法同樣也適用於鏈表,只不過沒下面這個好,但是下面算法這個比較複雜):

注意:在下面的算法中鏈表L增加了一個哨兵單元,其中的元素爲-∞,即線性表L的第一個元素是L^.next^

procedure Selection_Sort_II(var L:PList);
var
i,j,tmp:Position;
begin
1 if L^.next=nil then exit; //如果鏈表L爲空則直接退出
2 i:=L^.next; //i指向L的第一個元素,注意,L有一個哨兵元素,因此L^.next^纔是L的第一個元素
3 while i^.next<>nil do
begin
4 tmp:=i^.next; //tmp指向L[i]的下一個位置
5 j:=L;
6 while (j<>i)and(tmp^.data>=j^.next^.data) do //從前向後找到tmp的位置,tmp應該插在j後面
7 j:=j^.next;
8 if j<>i then //j=i說明不需要改變tmp的位置
begin
9 i^.next:=tmp^.next; //將tmp從i後面摘除
10 tmp^.next:=j^.next; //在j後面插入tmp
11 j^.next:=tmp;
end
12 else i:=i^.next; //否則i指向下一個元素
end;
end;
上述改進算法主要是利用鏈表刪除和插入元素方便的特性,對於數組則不適用。

插入排序法是一個原地置換排序法,也是一個穩定排序法。插入法雖然在最壞情況下複雜性爲θ(n2),但是對於小規模輸入來說,插入排序法是一個快速的原地置換排序法。許多複雜的排序法,在規模較小的情況下,都使用插入排序法來進行排序,比如快速排序和桶排序。 





快速排序 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;
java 代碼
   /** This is a generic version of C.A.R Hoare's Quick Sort
    * algorithm.  This will handle arrays that are already
    * sorted, and arrays with duplicate keys.<BR>
    *
    * If you think of a one dimensional array as going from
    * the lowest index on the left to the highest index on the right
    * then the parameters to this function are lowest index or
    * left and highest index or right.  The first time you call
    * this function it will be with the parameters 0, a.length - 1.
    *
    * @param a       an integer array
    * @param lo0     left boundary of array partition
    * @param hi0     right boundary of array partition
    */

   void QuickSort(int a[], int lo0, int hi0) throws Exception
   {
      int lo = lo0;
      int hi = hi0;
      int mid;
      // pause for redraw
      if ( hi0 > lo0)
      {
         /* Arbitrarily establishing partition element as the midpoint of
          * the array.
          */
         mid = a[ ( lo0 + hi0 ) / 2 ];
         // loop through the array until indices cross
         while( lo <= hi )
         {
            /* find the first element that is greater than or equal to
             * the partition element starting from the left Index.
             */
            while( ( lo < hi0 ) && ( a[lo] < mid ) )
               ++lo;
            /* find an element that is smaller than or equal to
             * the partition element starting from the right Index.
             */

            while( ( hi > lo0 ) && ( a[hi] > mid ) )
               --hi;
            // if the indexes have not crossed, swap
            if( lo <= hi )
            {
               swap(a, lo, hi);
               ++lo;
               --hi;
            }
         }
         /* If the right index has not reached the left side of array
          * must now sort the left partition.
          */
         if( lo0 < hi )
            QuickSort( a, lo0, hi );
         /* If the left index has not reached the right side of array
          * must now sort the right partition.
          */

         if( lo < hi0 )
            QuickSort( a, lo, hi0 );
      }
   }
   private void swap(int a[], int i, int j)
   {
      int T;
      T = a[i];
      a[i] = a[j];
      a[j] = T;
   }
   public void sort(int a[]) throws Exception
   {
      QuickSort(a, 0, a.length - 1);
   }

對線性表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的快速排序法又稱爲隨機化版本的快速排序法,在下面的複雜性分析中我們將看到該方法具有平均情況下最好的性能,在實際應用中該方法的性能也是最好的。






線性時間排序算法

我們已經知道,通過比較確定兩個元素之間相對位置的比較排序算法計算時間複雜性下界爲O(nlogn),要想改進這個下界,就必須對輸入的數據作某些限制。下面介紹的幾種排序算法都可以在O(n)時間內對一個線性表進行排序,但是他們要求輸入數據滿足某種條件。

計數排序
基數排序
桶排序
計數排序 Counting Sort

計數排序是一個非基於比較的線性時間排序算法。它對輸入的數據有附加的限制條件:

1. 輸入的線性表的元素屬於有限偏序集S;

2. 設輸入的線性表的長度爲n,|S|=k(表示集合S中元素的總數目爲k),則k=O(n)。

在這兩個條件下,計數排序的複雜性爲O(n)。

計數排序算法的基本思想是對於給定的輸入序列中的每一個元素x,確定該序列中值小於x的元素的個數。一旦有了這個信息,就可以將x直接存放到最終的輸出序列的正確位置上。例如,如果輸入序列中只有17個元素的值小於x的值,則x可以直接存放在輸出序列的第18個位置上。當然,如果有多個元素具有相同的值時,我們不能將這些元素放在輸出序列的同一個位置上,因此,上述方案還要作適當的修改。

假設輸入的線性表L的長度爲n,L=L1,L2,..,Ln;線性表的元素屬於有限偏序集S,|S|=k且k=O(n),S={S1,S2,..Sk};則計數排序算法可以描述如下:

1. 掃描整個集合S,對每一個Si∈S,找到在線性表L中小於等於Si的元素的個數T(Si);

2. 掃描整個線性表L,對L中的每一個元素Li,將Li放在輸出線性表的第T(Li)個位置上,並將T(Li)減1。

具體的實現如下。

注意:在以下的討論中,爲了方便,我們假設線性表是用數組來實現的,並且假設線性表的元素類型TElement爲整型,其值在1..k之間,線性表的長度爲n,且k=O(n)。這些假設對計數排序算法沒有實質的影響,但是可以使以下介紹的算法看起來容易理解。

在下面的計數排序算法中,我們假設L爲輸入的長度爲n的線性表,輸出的排序結果存放在線性表R中。算法中還用到一個輔助表tmp用於對輸入元素進行計數。

Type

TElement=1..k;

TList=array [1..maxlength] of TElement;

TPosition=integer;



procedure Counting_Sort(var L,R:TList);

var

i,j:integer;

tmp:TList;

begin

1 for i:=1 to k do tmp[i]:=0;

2 for j:=1 to n do inc(tmp[L[j]]);

//執行完上面的循環後,tmp[i]的值是L中等於i的元素的個數

3 for i:=2 to k do tmp[i]:=tmp[i]+tmp[i-1];

//執行完上面的循環後,tmp[i]的值是L中小於等於i的元素的個數

4 for j:=n downto 1 do //注意這裏的downto保證了排序的穩定性

begin

5 R[tmp[L[j]]]:=L[j];//L[j]存放在輸出數組R的第tmp[L[j]]個位置上

6 dec(tmp[L[j]]); //tmp[L[j]]表示L中剩餘的元素中小於等於L[j]的元素的個數

end;

end;

圖1所示的是Counting_Sort作用於一個輸入數組L[1..8]上的過程,其中L的每一個元素都是不大於k=6的正整數。











圖1 計數排序算法演示

容易理解,算法的第(l)行是對數組tmp初始化。第(2)行檢查每個輸入元素。如果輸入元素的鍵值爲i,則tmp[i]增1。因此,在第(2)行執行結束後,tmp[i]中存放着值等於i的輸入元素個數,i=1,2,..,k。算法的第(3)行,對每個i=1,2,..,i,統計值小於或等於i的輸入元素個數。最後在(4)-(8)行中,將每個元素L[j]存放到輸出數組R中相應的最終位置上。如果所有n個元素的值都不相同,則共有tmp[L[j]]個元素的鍵值小於或等於L[j],而小於L[j]的元素有tmp[L[j]]-1個,因此tmp[L[j]]就是L[j]在輸出數組R中的正確位置。當輸入元素有相同的值時,每將一個L[j]存放到數組R時,tmp[L[j]]就減1,使下
個值等於L[j]的元素存放在輸出數組R中存放元素L[j]的前一個位置上。

計數排序算法的計算時間複雜性很容易分析。其中,第(1)行需要O(k)時間;第(2)行需要O(n)時間,第(3)行需要O(k)時間;第(4)-(8)行的for循環需要O(n)時間。這樣,整個算法所需的計算間爲O(n+k)。當k=O(n)時,算法的計算時間複雜性爲O(n)。

我們看到,計數排序算法沒有用到元素間的比較,它利用元素的實際值來確定它們在輸出數組中的位置。因此,計數排序算法不是一個基於比較的排序算法,從而它的計算時間下界不再是Ω(nlogn)。另一方面,計數排序算法之所以能取得線性計算時間的上界是因爲對元素的取值範圍作了一定限制,即k=O(n)。如果k=n2,n3,..,就得不到線性時間的上界。此外,我們還看到,由於算法第4行使用了downto語句,經計數排序,輸出序列中值相同的元素之間的相對次序與他們在輸入序列中的相對次序相同,換句話說,計數排序算法是一個穩定的排序算法,但顯然不是原地置換排序算法。

基數排序 Radix Sort

基數排序是一種用在老式穿卡機上的算法。一張卡片有80列,每列可在12個位置中的任一處穿孔。排序器可被機械地"程序化"以檢查每一迭卡片中的某一列,再根據穿孔的位置將它們分放12個盒子裏。這樣,操作員就可逐個地把它們收集起來。其中第一個位置穿孔的放在最上面,第二個位置穿孔的其次,等等。

對十進制數字來說,每列中只用到10個位置(另兩個位置用於編碼非數值字符)。一個d位數佔用d個列。因爲卡片排序器一次只能查看一個列,要對n張片上的d位數進行排序就要有個排序算法。

直感上,大家可能覺得應該按最重要的一位排序,然後對每個盒子中的數遞歸地排序,最後把結果合併起來。不幸的是,爲排序每一個盒子中的數,10個盒子中的9個必須先放在一邊,這個過程產生了許多要加以記錄的中間卡片堆。

與人們的直感相反,基數排序是首先按最不重要的一位數字排序來解決卡片排序問題的。同樣,把各堆卡片收集成一迭,其中0號盒子中的在1號盒子中的前面,後者又在2號盒子中的前面,等等。然後對整個一迭卡片按次重要位排序,並把結果同樣地合併起來。重複這個過程,直到對所有的d位數字都進行了排序。所以,僅需要n遍就可將一迭卡片排好序。圖1說明了基數排序作“一迭”7個三位數的過程。第一列爲輸入,其餘各列示出了對各個數位進行逐次排序後表的情形。垂直向上的箭頭指示了當前要被加以排序的數位。

 
圖1 基數排序作用於一個由7個3位數組成的表上的過程

關於這個算法很重要的一點就是按位排序要穩定。由卡片排序器所故的排序是穩定的,但操作員在把卡片從盒子裏拿出來時不能改變他們的次序,即使某一盒子中所有卡片在給定列上的穿孔位置都相同。

在一臺典型的順序隨機存取計算機上,有時採用基數排序來對有多重域關鍵字的記錄進行排序。例如,假設我們想根據三個關鍵字處、月和日來對日期排序。對這個問題,可以用帶有比較函數的排序算法來做。給定兩個日期,先比較年份,如果相同,再比較月份,如果再相同,再比較日。這兒我們可以採用另一個方法,即用一種穩定的排序方法對所給信息進行三次排序:先對日,其次對月,再對年。

基數排序的代碼是很簡單的、下面的過程假設長度爲n的數組A中的每個元素都有d位數字,其中第1位是最低的,第d位是最高位。

procedure Radix_Sort(var L:List;d:integer);

var

i:integer;

begin

1 for i:=1 to d do

2 使用一種穩定的排序方法來對數組L按數字i進行排序;

end;

基數排序的正確性可以通過對正在被排序的列進行歸納而加以證明。對本算法時間代價的分析要取決於選擇哪種穩定的中間排序算法。當每位數字都界於l到k之間,且k不太大時,可以選擇計數排序。對n個d位數的每一遍處理的時間爲O(n+k),共有d遍,故基數排序的總時間爲θ(dn+kd)。當d爲常數,k=O(n)時,基數排序有線性運行時間。

某些計算機科學家傾向於把一個計算機字中所含位數看成是θ(lgn)。具體一點說,假設共有dlgn位數字,d爲正常數。這樣,如果待排序的每個數恰能容於一個計算機字內,我們就可以把它視爲一個以n爲基數的d位數。看一個例子:對一百萬個64位數排序。通過把這些數當作是以216爲基數的四位數,用基數排序四遍就可完成排序。這與一個典型的O(nlgn)比較排序相比要好得多,後者對每一個參加排序的數約要lgn=20次操作。但有一點不理想,即採用計數排序作爲中間穩定排序算法的基數排序版本不能夠進行原地置換排序,而很多O(nlgn)比較排序算法卻是可以的。因此,當內存比較緊張時,一般來說選擇快速排序更合適些。



桶排序 Bin Sort

平均情況下桶排序以線性時間運行。像計數排序一樣,桶排序也對輸入作了某種假設, 因而運行得很快。具體來說,計數排序假設輸入是由一個小範圍內的整數構成,而桶排序則 假設輸入由一個隨機過程產生,該過程將元素一致地分佈在區間[0,1)上。

桶排序的思想就是把區間[0,1)劃分成n個相同大小的子區間,或稱桶,然後將n個輸入數分佈到各個桶中去。因爲輸入數均勻分佈在[0,1)上,所以一般不會有很多數落在 一個桶中的情況。爲得到結果,先對各個桶中的數進行排序,然後按次序把各桶中的元素列 出來即可。

在桶排序算法的代碼中,假設輸入是個含n個元素的數組A,且每個元素滿足0≤ A[i]<1。另外還需要一個輔助數組B[O..n-1]來存放鏈表實現的桶,並假設可以用某種機制來維護這些表。

桶排序的算法如下,其中floor(x)是地板函數,表示不超過x的最大整數。

procedure Bin_Sort(var A:List);

begin

1 n:=length(A);

2 for i:=1 to n do

3 將A[i]插到表B[floor(n*A[i])]中;

4 for i:=0 to n-1 do

5 用插入排序對錶B[i]進行排序;

6 將表B[0],B[1],...,B[n-1]按順序合併;

end;



圖1 Bin_Sort的操作

圖1演示了桶排序作用於有10個數的輸入數組上的操作過程。(a)輸入數組A[1..10]。(b)在該算法的第5行後的有序表(桶)數組B[0..9]。桶i中存放了區間[i/10,(i+1)/10]上的值。排序輸出由表B[O]、B[1]、...、B[9]的按序並置構成。

要說明這個算法能證確地工作,看兩個元素A[i]和A[j]。如果它們落在同一個桶中,則它們在輸出序列中有着正確的相對次序,因爲它們所在的桶是採用插入排序的。現假設它們落到不同的桶中,設分別爲B[i'']和B[j'']。不失一般性,假設i''<j''。在算法的代碼中,當第6行中將B中的表並置起來時,桶B[i'']中的元素先於桶B[j'']中的元素,因而在輸出序列中A[i]先於A[j]。現在要證鰽[i]≤A[j]。假設情況正好相反,我們有:

i''=floor(n*A[i])≥floor(n*A[j])=j''

得矛盾 (因爲i''<j''),從而證明桶排序能正確地工作。

現在來分析算法的運行時間。除第5行外,所有各行在最壞情況的時間都是O(n)。第5行中檢查所有桶的時間是O(n)。分析中唯一有趣的部分就在於第5行中插人排序所花的時間。

爲分析插人排序的時間代價,設ni爲表示桶B[i]中元素個數的隨機變量。因爲插入排序以二次時間運行,故爲排序桶B[i]中元素的期望時間爲E[O(ni2)]=O(E[ni2]),對各個桶中的所有元素排序的總期望時間爲:

(1)

爲了求這個和式,要確定每個隨機變量ni的分佈。我們共有n個元素,n個桶。某個元素落到桶B[i]的概率爲l/n,因爲每個桶對應於區間[0,1)的l/n。這種情況與投球的例子很類似:有n個球 (元素)和n個盒子 (桶),每次投球都是獨立的,且以概率p=1/n落到任一桶中。這樣,ni=k的概率就服從二項分佈B(k;n,p),其期望值爲E[ni]=np=1,方差V[ni]=np(1-p)=1-1/n。對任意隨機變量X,有:

  (2)

將這個界用到(1)式上,得出桶排序中的插人排序的期望運行時間爲O(n)。因而,整個桶排序的期望運行時間就是線性的。

下面的Java Applet程序演示了桶排序的基本思想。



在該演示程序中,線性表的元素類型爲整型,桶的標號爲整數,算法將值爲i的元素放入標號爲i的桶中,再按照桶的標號的順序將元素依次取出,就得到了最終的排序結果。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章