參考資料
快速排序算法的編碼描述
快排的基本思路


- 先通過第一趟排序,將數組原地划分為兩部分,其中一部分的所有數據都小於另一部分的所有數據。原數組被划分為2份
- 通過遞歸的處理, 再對原數組分割的兩部分分別划分為兩部分,同樣是使得其中一部分的所有數據都小於另一部分的所有數據。 這個時候原數組被划分為了4份
- 就1,2被划分后的最小單元子數組來看,它們仍然是無序的,但是! 它們所組成的原數組卻逐漸向有序的方向前進。
- 到最后, 數組被划分為多個由一個元素或多個相同元素組成的單元, 這時候整個數組就有序了


3 1 4 1 5 9 2 6 5 3
1 1 2 3 3 4 5 5 6
快排的實現步驟

下面我就只講解1和2步驟, 而在1,2中,關鍵在於如何實現“划分”
切分的關鍵點: 基准元素, 左游標和右游標
划分的過程有三個關鍵點:“基准元素”, “左游標” 和“右游標”。
- 基准元素:它是將數組划分為兩個子數組的過程中, 用於界定大小的值, 以它為判斷標准, 將小於它的數組元素“划分”到一個“小數值數組”里, 而將大於它的數組元素“划分”到一個“大數值數組”里面。這樣,我們就將數組分割為兩個子數組, 而其中一個子數組里的元素恆小於另一個子數組里的元素
- 左游標: 它一開始指向待分割數組最左側的數組元素。在排序過程中,它將向右移動
- 右游標: 它一開始指向待分割數組最右側的數組元素。在排序過程中,它將向左移動
一趟切分的具體過程


- “基准元素v是怎么選的?”
- 游標i,j的移動的過程中發生了什么事情(比如元素交換)?,
- 為什么左右游標相遇時一趟切分就完成了?


- 左游標向右掃描, 跨過所有小於基准元素的數組元素, 直到遇到一個大於或等於基准元素的數組元素, 在那個位置停下。
- 右游標向左掃描, 跨過所有大於基准元素的數組元素, 直到遇到一個大於或等於基准元素的數組元素,在那個位置停下
- 左右游標沒有相遇
- 左右游標相遇了





【注意】這里你可能會問: 在我們制定的規則里, 左游標先掃描和右游標先掃描有區別嗎? (如果你這樣想的話就和我想到一塊去了...嘿嘿),因為就上圖而言,兩種情況下一趟排序中兩個游標相遇的位置是不同的(一般而言,除非相遇位置的下方的元素剛好和基准元素相同):
- 如果右游標先掃描,左右游標相遇的位置應該是3上方(圖示)
- 但如果左游標先掃描, 左右游標相遇的位置卻是9上方
6 1 2 5 4 3 9 7 10 8
1 2 5 4 3 6 9 7 10 8


總結一趟排序的過程




快速排序代碼展示
具體的代碼
// 交換兩個數組元素 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; } // 終止遞歸 int j = partition(a, low, high); // 調用partition進行切分 sort(a, low, j-1); // 對上一輪排序(切分)時,基准元素左邊的子數組進行遞歸 sort(a, j+1, high); // 對上一輪排序(切分)時,基准元素右邊的子數組進行遞歸 }
對切分函數partition的解讀
while (a[--j]>pivotkey) { ... }
先將右游標左移一位,然后判斷指向的數組元素和基准元素pivotkey比較大小, 如果該元素大於基准元素, 那么循環繼續,j再次減1,右游標再次左移一位...... (循環體可以看作是空的)
if(i>=j) break;
從i < j到 i == j 代表了“游標未相遇”到“游標相遇”的過度過程,此時跳出外部循環, 切分已接近完成,緊接着通過 exchange(a, low, j) 交換基准元素和相遇游標所指元素的位置, low是基准元素的位置(頭部元素), j是當前兩個游標相遇的位置
while (a[--j]>pivotkey) { if(j == low) break; } // 右游標左移
中,當隨着右游標左移,到j = low + 1的時候,有 a[--j] == pivotkey為true(兩者都是基准元素),自動跳出了while循環,所以就不需要在循環體里再判斷 j == low 了
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循環,這顯然不是我們希望看到的)
對主體函數sort的解讀
sort(a, low, j-1); sort(a, j+1, high);
進行下一輪遞歸,設置j -1 和j + 1 是因為上一輪基准元素的位置已經是有序的了,不要再納入下一輪遞歸里
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
優化點一 —— 切換到插入排序
if(high<= low) { return; }
if(high<= low + M) { Insertion.sort(a,low, high) return; } // Insertion表示一個插入排序類
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); // 對上一輪排序(切分)時,基准元素右邊的子數組進行遞歸 }
優化點二 —— 基准元素選取的隨機化
- 排序前打亂數組的順序
- 通過隨機數保證取得的基准元素的隨機性
- 三數取中法取得基准元素(推薦)
public static void sort (int [] a){ StdRandom.shuffle(a) // 外部導入的亂序算法,打亂數組的分布 sort(a, 0, a.length - 1); }
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; // 一趟排序完成, 返回基准元素位置 }
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(a[++i]<pivotkey) { if(i == high) break; }
我們只要嘗試把這一作用交給a[++i]<pivotkey去完成, 不就可以把 if(i == high) break; 給去掉了嗎?
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); } }


優化點四 —— 三切分快排(針對大量重復元素)
- 左游標向右掃描, 跨過所有小於基准元素的數組元素, 直到遇到一個大於或等於基准元素的數組元素, 在那個位置停下。
- 右游標向左掃描, 跨過所有大於基准元素的數組元素,直到遇到一個大於或等於基准元素的數組元素,在那個位置挺停下




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); } }
