【算法】快速排序算法的編碼和優化

 

參考資料算法

 
《算法(第4版)》          — — Robert Sedgewick, Kevin Wayne
《啊哈! 算法》              — — 啊哈磊
《數據結構(教材)》     — — 嚴蔚敏,吳偉民
 
 

快速排序算法的編碼描述

 

快排的基本思路

 

 

 
快速排序的基本思路是:
  1. 先經過第一趟排序,將數組原地劃分爲兩部分其中一部分的全部數據都小於另外一部分的全部數據。原數組被劃分爲2
  2. 經過遞歸的處理, 再對原數組分割的兩部分分別劃分爲兩部分,一樣是使得其中一部分的全部數據都小於另外一部分的全部數據。 這個時候原數組被劃分爲了4
  3. 就1,2被劃分後的最小單元子數組來看,它們仍然是無序的,可是! 它們所組成的原數組卻逐漸向有序的方向前進。
  4. 到最後, 數組被劃分爲多個由一個元素或多個相同元素組成的單元, 這時候整個數組就有序了
 
總結: 經過第一趟排序,將原數組A分爲B和C兩部分, 總體上B<C, 第二躺排序時候將B劃分爲B1,B2兩部分, 使得B1<B2, 同理C1<C2。那麼經過兩趟排序, 從B1/B2/C1/C2的長度的單元看待整個數組, 從左至右 B1<B2<C1<C2, 數組是「有序」的, 而且隨着排序的深刻,原數組有序性愈來愈強
 
總體的排序過程以下圖所示(暫且無論實現的具體細節)
 

 

如上圖所示, 數組
3 1 4 1 5 9 2 6 5 3

 

經過第一趟排序被分紅了2 1 1 和4 5 9 3 6 5 3兩個子數組,且對任意元素,左子數組總小於右子數組
經過不斷遞歸處理,最終獲得
1 1 2 3 3 4 5 5 6

 

這個有序的數組
 

快排的實現步驟

 
快排具體的實現步驟以下圖所示:
 
 

 

 
圖中的步驟3,4不難理解,這裏就很少贅述,由於步驟3中的遞歸思想是你們比較熟悉的, 步驟4中的「組合」其實就只是個概念上的詞,由於全部的子數組原本就鏈接在一塊兒,只要全部的遞歸結束了,整個數組就是有序的。
 

下面我就只講解1和2步驟, 而在1,2中,關鍵在於如何實現「劃分」數組

 

切分的關鍵點: 基準元素, 左遊標和右遊標

 

劃分的過程有三個關鍵點:「基準元素」, 「左遊標」 和「右遊標」。數據結構

 
  • 基準元素:它是將數組劃分爲兩個子數組的過程當中, 用於界定大小的值, 以它爲判斷標準, 將小於它的數組元素「劃分」到一個「小數值數組」裏, 而將大於它的數組元素「劃分」到一個「大數值數組」裏面。這樣,咱們就將數組分割爲兩個子數組, 而其中一個子數組裏的元素恆小於另外一個子數組裏的元素
  • 左遊標 它一開始指向待分割數組最左側的數組元素。在排序過程當中,它將向右移動
  • 右遊標: 它一開始指向待分割數組最右側的數組元素。在排序過程當中,它將向左移動
 
【注意】
1.上面描述的基準元素/右遊標/左遊標都是針對單趟排序過程的, 也就是說,在總體排序過程的多趟排序中,各趟排序取得的基準元素/右遊標/左遊標通常都是不一樣的
2. 在不一樣的教材裏,基準元素也叫「樞軸」,「關鍵字」, 「劃分」也叫「切分」
 
那這基準元素-右遊標-左遊標三個關鍵點是如何融會貫通,搞定一趟切分(劃分)的呢?

 一趟切分的具體過程

 
 

 

 
切分的具體過程如圖所示。在下圖中,基準元素是v,   左遊標是i, 右遊標是j
i一開始指向數組頭部元素的位置lo, 切分時向右移動, j一開始指向數組末端元素hi,隨後向左移動, 當左右遊標相遇的時候,一趟切分就完成了。
 
固然, 看到這裏你可能很懵懂,你可能會問:
  • 「基準元素v是怎麼選的?」
  • 遊標i,j的移動的過程當中發生了什麼事情(好比元素交換)?,
  • 爲何左右遊標相遇時一趟切分就完成了?
讓咱們繼續往下看:
 
基準元素的選取
 
首先,在原則上,基準元素的選取是任意的
但咱們通常選取數組的第一個元素爲基準元素(假設數組是隨機分佈的
 
下面以啊哈磊老師的圖示爲例:
 
假設下面的是咱們的待排序的數組的話, 根據咱們的頭元素做爲基準元素的原則,士兵i下面的數組元素 「6」 就是咱們選定的第一趟排序的基準元素
 

 

 
(做爲入門,啊哈磊老師的《啊哈,算法》裏的圖示仍是頗有趣的! 這裏向你們安利一下)
 
【注意】下面在優化中會講關於基準元素的選取的訣竅, 但在快排的基礎編碼裏,咱們只要記住把頭部元素看成基準元素就夠了(假設數組元素是隨機分佈的)
 
左右遊標掃描和元素交換
 
在選取了基準元素以後, 切分就正式開始了。這時候,左右遊標開始分別向右/左移動,它們遵循的規則分別是:
 
  • 左遊標掃描, 跨過全部小於基準元素的數組元素, 直到遇到一個大於或等於基準元素的數組元素, 在那個位置停下
  • 右遊標掃描, 跨過全部大於基準元素的數組元素, 直到遇到一個大於或等於基準元素的數組元素,在那個位置停下
 
當左右遊標掃描分兩種狀況(或者說是兩個前後階段...)
  1. 左右遊標沒有相遇
  2. 左右遊標相遇了
 
在下圖中, 左遊標就是士兵i, 而右遊標是士兵j啦。
 

 

 
 
1.首先,右遊標j會向左跨過全部大於基準元素的元素, 因此士兵j向左跨過了板磚8和10, 而後當他遇到了「小於等於」基準元素6的元素5時候, 「哎呀, 不能再前進了,在這裏打住吧!」, 因而右遊標就在5處停了下來,
2.而後, 士兵i(左遊標)跨過了小於基準元素6的1和2,而後遇到了「大於等於」6的7,在7處停了下來。
3.  停下來以後, 左右遊標所指的數組元素交換了它們的值(兩個士兵交換了他們腳下的板磚)
 
下圖同上:
 

 

 
遊標掃描和元素交換的意義
 
很明顯, 兩個遊標士兵的「工做」 就是不斷靠近,並檢查有沒有小於(大於)規定要求(即基準元素6)的板磚(元素),一旦發現, 就「丟」到對面去, 而當他們相遇的時候, 大小關係嚴格的兩塊子數組也就分割出來了
 
【注意】
1.要注意一點: 咱們選取的基準元素和左遊標最初指定的元素是相同的! 那麼就咱們就會發現一個問題: 當左遊標向右掃描的時候,第一個遇到的「大於或等於」的元素就是它自己, 那麼問題來了: 需不須要停下來呢? 固然根據邏輯思考能夠得出這是沒必要要的,因此下面我會結合算法指出這一細節: 左遊標向右掃描的時候其實忽略了它最初所指的位置——頭元素的比較
2. 必須等一個「士兵」(遊標)先走完, 另外一個「士兵」(遊標)才能走不能每人輪流走一步...
 
左右遊標相遇
 
承接上文, 此次眼看士兵i和士兵j就要相遇了! 首先士兵j先走,當它遇到3的位置的時候,由於3「小於等於」6,因此士兵j就停下來了。再而後士兵i向右走,但由於他和士兵j「碰頭」了,因此士兵i只能無奈地「提早」在3停住了(若是沒和j碰面士兵i是能走到9的!)
 
因此這就是左右遊標掃描相遇時候遵循的原則: 只相遇, 不交叉
 

 

【注意】這裏你可能會問: 在咱們制定的規則裏, 左遊標先掃描和右遊標先掃描有區別嗎?  (若是你這樣想的話就和我想到一塊去了...嘿嘿),由於就上圖而言,兩種狀況下一趟排序中兩個遊標相遇的位置是不一樣的(通常而言,除非相遇位置的下方的元素恰好和基準元素相同):dom

  • 若是右遊標先掃描,左右遊標相遇的位置應該是3上方(圖示)
  • 但若是左遊標先掃描, 左右遊標相遇的位置倒是9上方
經過編碼驗證和翻閱書籍,我得出的結論是:對排序的劃分過程有影響,但對最終結果是沒有具體的影響的。特別的,在《數據結構》這本書中採起的是右遊標先掃描,而在《算法(第四版)》書中,則採起左遊標先掃描的策略
 
基準元素歸位
 
當到達了我上面所說的「左右遊標相遇」這個階段後, 咱們發現, 左右兩個子數組已經基本有序了,即分紅了 1 2 5 4 3和9 7 10 8 這兩段元素,其中前一段元素都小於後一段元素
 
等等! 好像有兩個數字違和感很強地打破了這個大小關係, 那就是6! (基準元素)
以下所示:
6 1 2 5 4 3 9 7 10 8

 

這時候咱們發現整個數組的組成是這樣的: 大小居中的基準元素 + 小數值數組 + 大數值數組
因此咱們只要把基準元素6和遊標相遇元素3換一下, 不就能夠變成: 小數值數組 + 大小居中的基準元素 +   大數值數組 了嗎?
1 2 5 4 3 6 9 7 10 8

 

 
如圖所示
 
 

 

 
至此, 一趟排序結束, 回到中間的6已經處於有序狀態,只要再對左右兩邊的元素進行遞歸處理就能夠了

 總結一趟排序的過程

 
OK,這裏讓咱們總結下一趟快速排序的四個過程:
 

 

 
一趟排序全過程圖示
 
(A - Z 字母排序, A最小, Z最大)
 

 

 

快速排序代碼展現

 

具體的代碼

這是咱們的輔助函數exchange: 用於交換任意兩個數組元素的位置:
// 交換兩個數組元素
private static void exchange(int [] a , int i, int j) {
  int temp = a[i];
  a[i] = a[j];
  a[j] = temp;
}

 

 
這是切分函數partition, 它完成了一輪排序的主要工做,使得待分割數組以基準元素爲界,分紅了一個小數值數組和一個大數值數組
private static int partition (int[] a, int low, int high) {
  int i = low, j = high+1// i, j爲左右掃描指針 PS: 思考下爲何j比i 多加一個1呢?
  int pivotkey = a[low];  // pivotkey 爲選取的基準元素(頭元素)
  while(true) { 
    while (a[--j]>pivotkey) {   if(j == low) break; }  // 右遊標左移
    while(a[++i]<pivotkey) {   if(i == high) break;  }  // 左遊標右移
    if(i>=j) break;    // 左右遊標相遇時候中止, 因此跳出外部while循環
    else exchange(a,i, j) ;  // 左右遊標未相遇時中止, 交換各自所指元素,循環繼續 
  }
  exchange(a, low, j); // 基準元素和遊標相遇時所指元素交換,爲最後一次交換
  return j;  // 一趟排序完成, 返回基準元素位置
}

 

 
這是主體函數sort, 將partition遞歸處理
private static void sort (int [] a,  int low, int high) {
  if(high<= low) { return; } // 終止遞歸
  int j = partition(a, low, high);  // 調用partition進行切分
  sort(a,  low,  j-1);   // 對上一輪排序(切分)時,基準元素左邊的子數組進行遞歸
  sort(a,  j+1,  high); // 對上一輪排序(切分)時,基準元素右邊的子數組進行遞歸
}

 

 

對切分函數partition的解讀

 
1. 直觀上看, partition由兩部分組成: 外部while循環和兩個並列的內部while循環。
 
2. 內部While循環的做用是使得左右遊標相互靠近
例如對:
while (a[--j]>pivotkey) {  ...   }

先將右遊標左移一位,而後判斷指向的數組元素和基準元素pivotkey比較大小, 若是該元素大於基準元素, 那麼循環繼續,j再次減1,右遊標再次左移一位...... (循環體能夠看做是空的)函數

 
3.外部While循環的做用是不斷經過exchange使得「逆序」元素的互相交換, 不斷向左子數組小於右子數組的趨勢靠近, 
if(i>=j) break

從i < j到 i == j 表明了「遊標未相遇」到「遊標相遇」的過分過程,此時跳出外部循環, 切分已接近完成,緊接着經過 exchange(a, low, j) 交換基準元素和相遇遊標所指元素的位置, low是基準元素的位置(頭部元素), j是當前兩個遊標相遇的位置性能

 
4. 第一個內部while循環體裏面的的  if(j == low) break;判斷實際上是多餘的,能夠去除。
由於在
while (a[--j]>pivotkey) {   if(j == low) break; }  // 右遊標左移

中,當隨着右遊標左移,到j = low + 1的時候,有 a[--j] == pivotkey爲true(二者都是基準元素),自動跳出了while循環,因此就不須要在循環體裏再判斷 j == low 了測試

 
5. 注意一個細節: j 比 i 多加了一個1,爲何? 以下
int i = low, j = high+1

結合下面兩個While循環中的判斷條件:優化

while (a[--j]>pivotkey) {  ...   }
while (a[++i]<pivotkey) {  ...   } 

可知道, 左遊標 i 第一次自增的時候, 跳過了對基準元素 a[low] 所執行的 a[low] < pivotkey判斷, 這是由於在咱們當前的算法方案裏,基準元素和左遊標初始所指的元素是同一個,因此沒有執行a[low]>pivotke這個判斷的必要。因此跳過( 一開始a[low] == pivotkey,若是執行判斷那麼一開始就會跳出內While循環,這顯然不是咱們但願看到的)ui

 
而相比之下,右遊標卻必需要對它初始位置所指的元素執行a[++i]<pivotkey , 因此 j 比 i 多加了一個
 
 
 

對主體函數sort的解讀

 
1. high<= low是判斷遞歸結束的條件
2. int j = partition(a, low, high);  有兩種做用: 一是進行一輪切分二是取得上一輪的基準元素的最終位置j, 傳遞給另外兩個sort函數,經過另外兩個sort函數的調用
sort(a,  low,  j-1);  
sort(a,  j+1,  high);

進行下一輪遞歸,設置j -1 和j + 1 是由於上一輪基準元素的位置已是有序的了,不要再歸入下一輪遞歸裏編碼

 
快速排序QuickSort類的所有代碼:
public class QuickSort {
  // 交換兩個數組元素
  private static void exchange(int [] a , int i, int j) {
    int temp = a[i];
    a[i] = a[j];
    a[j] = temp;
  }
 
  private static int partition (int[] a, int low, int high) {
    int i = low, j = high+1;      // i, j爲左右掃描指針 PS: 思考下爲何j比i 多加一個1呢?
    int pivotkey = a[low];  // pivotkey 爲選取的基準元素(頭元素)
    while(true) { 
      while (a[--j]>pivotkey) {   if(j == low) break; }  // 右遊標左移
      while(a[++i]<pivotkey) {   if(i == high) break;  }  // 左遊標右移
      if(i>=j) break;    // 左右遊標相遇時候中止, 因此跳出外部while循環
      else exchange(a,i, j) ;  // 左右遊標未相遇時中止, 交換各自所指元素,循環繼續 
    }
    exchange(a, low, j); // 基準元素和遊標相遇時所指元素交換,爲最後一次交換
    return j;  // 一趟排序完成, 返回基準元素位置
  }
 
  private static void sort (int [] a,  int low, int high) {
    if(high<= low) { return; } // 當high == low, 此時已經是單元素子數組,天然有序, 故終止遞歸
    int j = partition(a, low, high);  // 調用partition進行切分
    sort(a,  low,  j-1);   // 對上一輪排序(切分)時,基準元素左邊的子數組進行遞歸
    sort(a,  j+1,  high); // 對上一輪排序(切分)時,基準元素右邊的子數組進行遞歸
  }
   
  public static void sort (int [] a){ //sort函數重載, 只向外暴露一個數組參數
    sort(a, 0, a.length - 1);
  }
}

 

 
測試代碼
public class Test {
  public static void main (String [] args) {
    int [] array = {4,1,5,9,2,6,5,6,1,8,0,7 };
    QuickSort.sort(array);
    for (int i = 0; i < array.length; i++) {
      System.out.print(array[i]);
    }
  }
}

 

結果:
01124556789

 

 

優化點一 —— 切換到插入排序

 
對於小數組而言, 快速排序比插入排序要慢, 因此在排序小數組時應該切換到插入排序。
只要把sort函數中的
if(high<= low) { return; }

 

改爲:
if(high<= low + M) {  Insertion.sort(a,low, high) return; } // Insertion表示一個插入排序類

 

就能夠了,這樣的話,這條語句就具備了兩個功能:
1. 在適當時候終止遞歸
2. 當數組長度小於M的時候(high-low <= M), 不進行快排,而進行插排
 
轉換參數M的最佳值和系統是相關的,通常來講, 5到15間的任意值在多數狀況下都能使人滿意
 
例如, 將sort函數改爲:
  private static void sort (int [] a,  int low, int high) {
    if(high<= low + 10) {  Insertion.sort(a,low, high) return; } // Insertion表示一個插入排序類
    int j = partition(a, low, high);  // 調用partition進行切分
    sort(a,  low,  j-1);   // 對上一輪排序(切分)時,基準元素左邊的子數組進行遞歸
    sort(a,  j+1,  high); // 對上一輪排序(切分)時,基準元素右邊的子數組進行遞歸
  }

 

 

優化點二 —— 基準元素選取的隨機化

 
上面說過,基準元素的選取是任意的,可是不一樣的選取方式對排序性能的影響很大。
 
在上面全部的快速排序的例子中,咱們都是固定選取基準元素,這種操做作了一個假設性的前提:數組元素的分佈是隨機的而若是數組不是隨機的,而是有必定順序的,甚至在最壞的狀況下:徹底正序或徹底逆序, 這個時候麻煩就來了: 快排所消耗的時間大大延長,徹底達不到快排應有的效果。
 
因此爲了保證快排算法的隨機化,咱們必須進行一些優化。
 
下面介紹的方法有三種:
 
  1. 排序前打亂數組的順序
  2. 經過隨機數保證取得的基準元素的隨機性
  3. 三數取中法取得基準元素(推薦)
 
1. 排序前打亂數組的順序
public static void sort (int [] a){
  StdRandom.shuffle(a)  // 外部導入的亂序算法,打亂數組的分佈
  sort(a, 0, a.length - 1);
}

 

固然來了,由於亂序函數的運行,這會增長一部分耗時,但這多是值得的
 
2.經過隨機數保證取得的基準元素的隨機性
  private static int getRandom (int []a, int low, int high) {
    int RdIndex = (int) (low + Math.random()* (high - low)); // 隨機取出其中一個數組元素的下標
    exchange(a, RdIndex, low);  // 將其和最左邊的元素互換
    return a[low];   }
 
  private static int partition (int[] a, int low, int high) {
    int i = low, j = high+1;      // i, j爲左右掃描指針 PS: 思考下爲何j比i 多加一個1呢?
    int pivotkey = getRandom (a, low, high); // 基準元素隨機化
    while(true) { 
      while (a[--j]>pivotkey) {   if(j == low) break; }  // 右遊標左移
      while(a[++i]<pivotkey) {   if(i == high) break;  }  // 左遊標右移
      if(i>=j) break;    // 左右遊標相遇時候中止, 因此跳出外部while循環
      else exchange(a,i, j) ;  // 左右遊標未相遇時中止, 交換各自所指元素,循環繼續 
    }
    exchange(a, low, j); // 基準元素和遊標相遇時所指元素交換,爲最後一次交換
    return j;  // 一趟排序完成, 返回基準元素位置
  }

 

 
3.  三數取中法(推薦)
通常認爲, 當取得的基準元素是數組元素的中位數的時候,排序效果是最好的,可是要篩選出待排序數組的中位數的成本過高, 因此只能從待排序數組中選取一部分元素出來再取中位數, 經大量實驗顯示: 當篩選數組的長度爲3時候,排序效果是比較好的, 因此由此發展出了三數取中法:
 
三數取中法 分別取出數組的最左端元素,最右端元素和中間元素, 在這三個數中取出中位數,做爲基準元素
 
package mypackage;
 
public class QuickSort {
  // 交換兩個數組元素
  private static void exchange(int [] a , int i, int j) {
    int temp = a[i];
    a[i] = a[j];
    a[j] = temp;
  }
 
  // 選取左中右三個元素,求出中位數, 放入數組最左邊的a[low]中
  private static int selectMiddleOfThree(int[] a, int low, int high) {
    int middle = low + (high -  low)/2// 取得位於數組中間的元素middle
    if(a[low]>a[high])    {      exchange(a, low, high);  //此時有 a[low] < a[high]     }     if(a[middle]>a[high]){     exchange(a, middle, high); //此時有 a[low], a[middle] < a[high]     }     if(a[middle]>a[low]) {     exchange(a, middle, low); //此時有a[middle]< a[low] < a[high]     }     return a[low];  // a[low]的值已經被換成三數中的中位數, 將其返回   }     private static int partition (int[] a, int low, int high) {     int i = low, j = high+1;      // i, j爲左右掃描指針 PS: 思考下爲何j比i 多加一個1呢?     int pivotkey = selectMiddleOfThree( a, low, high);     while(true) {        while (a[--j]>pivotkey) {   if(j == low) break; }  // 右遊標左移       while(a[++i]<pivotkey) {   if(i == high) break;  }  // 左遊標右移       if(i>=j) break;    // 左右遊標相遇時候中止, 因此跳出外部while循環       else exchange(a,i, j) ;  // 左右遊標未相遇時中止, 交換各自所指元素,循環繼續      }     exchange(a, low, j); // 基準元素和遊標相遇時所指元素交換,爲最後一次交換     return j;  // 一趟排序完成, 返回基準元素位置   }     private static void sort (int [] a,  int low, int high) {     if(high<= low) { return; } // 當high == low, 此時已經是單元素子數組,天然有序, 故終止遞歸     int j = partition(a, low, high);  // 調用partition進行切分     sort(a,  low,  j-1);   // 對上一輪排序(切分)時,基準元素左邊的子數組進行遞歸     sort(a,  j+1,  high); // 對上一輪排序(切分)時,基準元素右邊的子數組進行遞歸   }       public static void sort (int [] a){ //sort函數重載, 只向外暴露一個數組參數     sort(a, 0, a.length - 1);   } }

 

 
 

優化點三 —— 去除沒必要要的邊界檢查

 
我在上面說過:「 第一個內部while循環體裏面的的  if(j == low) break;判斷實際上是多餘的,能夠去除」 
(請把文章往上翻到標題—「對切分函數partition的解讀」中的第4點)
 
那麼, 能不能把另一個邊界檢查  if(i == high) break; 也去除呢? 固然是不能直接去除的,可是咱們能夠經過一些技巧使得咱們可以去除它
 
首先要理解的是 if(i == high) break;的做用: 防止 i 增長到超過數組的上界, 形成數組越界的錯誤。
那麼按照一樣的思考方式,對於
while(a[++i]<pivotkey) {   if(i == high) break;  }

咱們只要嘗試把這一做用交給a[++i]<pivotkey去完成, 不就能夠把 if(i == high) break; 給去掉了嗎?

 
這裏的技巧就是: 在排序前先把整個數組中最大的元素移到數組的最右邊,這樣的話, 就算左遊標i增長(右移)到數組的最右端,a[++i]<pivotkey也會斷定爲false(數組最大值固然是大於或等於基準元素的), 從而沒法越界。
 
代碼:
public class QuickSort {
  // 交換兩個數組元素
  private static void exchange(int [] a , int i, int j) {
    int temp = a[i];
    a[i] = a[j];
    a[j] = temp;
  }
 
  //將原數組裏最大的元素移到最右邊, 構造「哨兵」
  private static void Max(int [] a) {
    int max = 0;
    for(int i = 1; i<a.length;i++) {
      if(a[i]>a[max]) {
        max = i;     }     }
    exchange(a, max, a.length -1);
  }
 
  private static int partition (int[] a, int low, int high) {
    int i = low, j = high+1;      // i, j爲左右掃描指針 PS: 思考下爲何j比i 多加一個1呢?
    int pivotkey = a[low];  // pivotkey 爲選取的基準元素(頭元素)
    while(true) { 
      while (a[--j]>pivotkey) {   }  // 空的循環體
      while(a[++i]<pivotkey) {   }  // 空的循環體
      if(i>=j) break;    // 左右遊標相遇時候中止, 因此跳出外部while循環
      else exchange(a,i, j) ;  // 左右遊標未相遇時中止, 交換各自所指元素,循環繼續 
    }
    exchange(a, low, j); // 基準元素和遊標相遇時所指元素交換,爲最後一次交換
    return j;  // 一趟排序完成, 返回基準元素位置
  }
 
  private static void sort (int [] a,  int low, int high) {
    if(high<= low) { return; } // 當high == low, 此時已經是單元素子數組,天然有序, 故終止遞歸
    int j = partition(a, low, high);  // 調用partition進行切分
    sort(a,  low,  j-1);   // 對上一輪排序(切分)時,基準元素左邊的子數組進行遞歸
    sort(a,  j+1,  high); // 對上一輪排序(切分)時,基準元素右邊的子數組進行遞歸
  }
   
  public static void sort (int [] a){ //sort函數重載, 只向外暴露一個數組參數
    Max(a); // 將原數組裏最大元素移到最右邊, 構造「哨兵」
    sort(a, 0, a.length - 1);
  }
}

 

 
若是看到這裏對「哨兵」這個概念還不是很清楚的話,看看下面這張圖示:
 
《三種哨兵》
 

 

 
關於哨兵三再說幾句: 在處理內部子數組的時候,右子數組中最左側的元素能夠做爲左子數組右邊界的哨兵(可能有點繞)
 

優化點四 —— 三切分快排(針對大量重複元素)

 
普通的快速排序還有一個缺點, 那就是會交換一些相同的元素
 
回憶一下我在前面提到的快排中對左右遊標指定的規則:
  • 左遊標向右掃描, 跨過全部小於基準元素的數組元素, 直到遇到一個大於或等於基準元素的數組元素, 在那個位置停下。
  • 右遊標向左掃描, 跨過全部大於基準元素的數組元素,直到遇到一個大於或等於基準元素的數組元素,在那個位置挺停下
 
特別的, 當左右遊標都指向和基準元素相同的元素時候, 沒必要要的交換就發生了
如圖:
(下圖中基準元素是6)
 

 

 
因此由此人們研究出了三切分快排(三路劃分) , 在左右遊標的基礎上,再增長了一個遊標,用於處理和基準元素相同的元素
 

 

 
代碼以下:
package mypackage;
 
public class Quick3way {
  public static void exchange(int [] a , int i, int j) {
    int temp = a[i];
    a[i] = a[j];
    a[j] = temp;
  }
 
  public static void sort (int [] a, int low, int high) {
    if(low>=high)  { return; }
    int lt = low, gt = high, i =low+1;
    int v = a[low];
    while(i<=gt) {
      int aValue = a[i];
      if(aValue>v) { exchange(a, i, gt--);  }
      else if(aValue<v) { exchange(a, i++, lt++); }
      else{ i++; }
    }
    sort(a, low, lt-1);
    sort(a, gt+1, high);
  }
 
  public static void sort (int [] a) {
    sort(a, 0, a.length - 1);
  }
}

 

 
 
切分軌跡:
 
(A - Z 字母排序, A最小, Z最大)
 

 

(很差意思,pdf書裏的截圖實在太模糊了,因此我本身 用手機照了一張)

 

相關文章
相關標籤/搜索