【算法】快速排序算法的编码和优化
?
參考資料
《算法(第4版)》????????? — — Robert Sedgewick, Kevin Wayne 《啊哈! 算法》????????????? — — 啊哈磊 《數(shù)據(jù)結(jié)構(gòu)(教材)》???? — — 嚴(yán)蔚敏,吳偉民 ? ?快速排序算法的編碼描述
?快排的基本思路
??
? 快速排序的基本思路是:?
如上圖所示, 數(shù)組 3 1 4 1 5 9 2 6 5 3?
通過第一趟排序被分成了2 1 1 和4 5 9 3 6 5 3兩個(gè)子數(shù)組,且對(duì)任意元素,左子數(shù)組總小于右子數(shù)組 通過不斷遞歸處理,最終得到 1 1 2 3 3 4 5 5 6?
這個(gè)有序的數(shù)組 ?快排的實(shí)現(xiàn)步驟
? 快排具體的實(shí)現(xiàn)步驟如下圖所示: ??
圖中的步驟3,4不難理解,這里就不多贅述,因?yàn)椴襟E3中的遞歸思想是大家比較熟悉的, 步驟4中的“組合”其實(shí)就只是個(gè)概念上的詞,因?yàn)樗械淖訑?shù)組本來就連接在一起,只要所有的遞歸結(jié)束了,整個(gè)數(shù)組就是有序的。 ?下面我就只講解1和2步驟, 而在1,2中,關(guān)鍵在于如何實(shí)現(xiàn)“劃分”
切分的關(guān)鍵點(diǎn):?基準(zhǔn)元素,?左游標(biāo)和右游標(biāo)
?劃分的過程有三個(gè)關(guān)鍵點(diǎn):“基準(zhǔn)元素”, “左游標(biāo)” 和“右游標(biāo)”。
?- 基準(zhǔn)元素:它是將數(shù)組劃分為兩個(gè)子數(shù)組的過程中, 用于界定大小的值, 以它為判斷標(biāo)準(zhǔn), 將小于它的數(shù)組元素“劃分”到一個(gè)“小數(shù)值數(shù)組”里, 而將大于它的數(shù)組元素“劃分”到一個(gè)“大數(shù)值數(shù)組”里面。這樣,我們就將數(shù)組分割為兩個(gè)子數(shù)組, 而其中一個(gè)子數(shù)組里的元素恒小于另一個(gè)子數(shù)組里的元素
- 左游標(biāo): 它一開始指向待分割數(shù)組最左側(cè)的數(shù)組元素。在排序過程中,它將向右移動(dòng)
- 右游標(biāo): 它一開始指向待分割數(shù)組最右側(cè)的數(shù)組元素。在排序過程中,它將向左移動(dòng)
?一趟切分的具體過程
?
? 切分的具體過程如圖所示。在下圖中,基準(zhǔn)元素是v,?? 左游標(biāo)是i, 右游標(biāo)是j i一開始指向數(shù)組頭部元素的位置lo, 切分時(shí)向右移動(dòng), j一開始指向數(shù)組末端元素hi,隨后向左移動(dòng), 當(dāng)左右游標(biāo)相遇的時(shí)候,一趟切分就完成了。 ? 當(dāng)然, 看到這里你可能很懵懂,你可能會(huì)問:- “基準(zhǔn)元素v是怎么選的?”
- 游標(biāo)i,j的移動(dòng)的過程中發(fā)生了什么事情(比如元素交換)?,
- 為什么左右游標(biāo)相遇時(shí)一趟切分就完成了?
?
? (作為入門,啊哈磊老師的《啊哈,算法》里的圖示還是很有趣的! 這里向大家安利一下) ? 【注意】下面在優(yōu)化中會(huì)講關(guān)于基準(zhǔn)元素的選取的訣竅, 但在快排的基礎(chǔ)編碼里,我們只要記住把頭部元素當(dāng)作基準(zhǔn)元素就夠了(假設(shè)數(shù)組元素是隨機(jī)分布的) ? 左右游標(biāo)掃描和元素交換 ? 在選取了基準(zhǔn)元素之后, 切分就正式開始了。這時(shí)候,左右游標(biāo)開始分別向右/左移動(dòng),它們遵循的規(guī)則分別是: ?- 左游標(biāo)向右掃描, 跨過所有小于基準(zhǔn)元素的數(shù)組元素, 直到遇到一個(gè)大于或等于基準(zhǔn)元素的數(shù)組元素, 在那個(gè)位置停下。
- 右游標(biāo)向左掃描, 跨過所有大于基準(zhǔn)元素的數(shù)組元素, 直到遇到一個(gè)大于或等于基準(zhǔn)元素的數(shù)組元素,在那個(gè)位置停下
?
? 1.首先,右游標(biāo)j會(huì)向左跨過所有大于基準(zhǔn)元素的元素, 所以士兵j向左跨過了板磚8和10, 然后當(dāng)他遇到了“小于等于”基準(zhǔn)元素6的元素5時(shí)候, “哎呀, 不能再前進(jìn)了,在這里打住吧!”, 于是右游標(biāo)就在5處停了下來, 2.然后, 士兵i(左游標(biāo))跨過了小于基準(zhǔn)元素6的1和2,然后遇到了“大于等于”6的7,在7處停了下來。 3.? 停下來之后, 左右游標(biāo)所指的數(shù)組元素交換了它們的值(兩個(gè)士兵交換了他們腳下的板磚) ? 下圖同上:?
游標(biāo)掃描和元素交換的意義 ? 很明顯, 兩個(gè)游標(biāo)士兵的“工作” 就是不斷靠近,并檢查有沒有小于(大于)規(guī)定要求(即基準(zhǔn)元素6)的板磚(元素),一旦發(fā)現(xiàn), 就“丟”到對(duì)面去, 而當(dāng)他們相遇的時(shí)候, 大小關(guān)系嚴(yán)格的兩塊子數(shù)組也就分割出來了 ? 【注意】 1.要注意一點(diǎn): 我們選取的基準(zhǔn)元素和左游標(biāo)最初指定的元素是相同的! 那么就我們就會(huì)發(fā)現(xiàn)一個(gè)問題: 當(dāng)左游標(biāo)向右掃描的時(shí)候,第一個(gè)遇到的“大于或等于”的元素就是它本身, 那么問題來了: 需不需要停下來呢? 當(dāng)然根據(jù)邏輯思考可以得出這是不必要的,所以下面我會(huì)結(jié)合算法指出這一細(xì)節(jié): 左游標(biāo)向右掃描的時(shí)候其實(shí)忽略了它最初所指的位置——頭元素的比較 2. 必須等一個(gè)“士兵”(游標(biāo))先走完, 另一個(gè)“士兵”(游標(biāo))才能走,不能每人輪流走一步... ? 左右游標(biāo)相遇 ? 承接上文, 這次眼看士兵i和士兵j就要相遇了! 首先士兵j先走,當(dāng)它遇到3的位置的時(shí)候,因?yàn)?“小于等于”6,所以士兵j就停下來了。再然后士兵i向右走,但因?yàn)樗褪勘鴍“碰頭”了,所以士兵i只能無奈地“提前”在3停住了(如果沒和j碰面士兵i是能走到9的!) ? 所以這就是左右游標(biāo)掃描相遇時(shí)候遵循的原則: 只相遇, 不交叉?
【注意】這里你可能會(huì)問: 在我們制定的規(guī)則里, 左游標(biāo)先掃描和右游標(biāo)先掃描有區(qū)別嗎?? (如果你這樣想的話就和我想到一塊去了...嘿嘿),因?yàn)榫蜕蠄D而言,兩種情況下一趟排序中兩個(gè)游標(biāo)相遇的位置是不同的(一般而言,除非相遇位置的下方的元素剛好和基準(zhǔn)元素相同):
- 如果右游標(biāo)先掃描,左右游標(biāo)相遇的位置應(yīng)該是3上方(圖示)
- 但如果左游標(biāo)先掃描, 左右游標(biāo)相遇的位置卻是9上方
?
這時(shí)候我們發(fā)現(xiàn)整個(gè)數(shù)組的組成是這樣的: 大小居中的基準(zhǔn)元素 + 小數(shù)值數(shù)組 + 大數(shù)值數(shù)組 所以我們只要把基準(zhǔn)元素6和游標(biāo)相遇元素3換一下, 不就可以變成: 小數(shù)值數(shù)組 + 大小居中的基準(zhǔn)元素 +?? 大數(shù)值數(shù)組 了嗎? 即 1 2 5 4 3 6 9 7 10 8?
? 如圖所示 ? ??
至此, 一趟排序結(jié)束, 回到中間的6已經(jīng)處于有序狀態(tài),只要再對(duì)左右兩邊的元素進(jìn)行遞歸處理就可以了?總結(jié)一趟排序的過程
? OK,這里讓我們總結(jié)下一趟快速排序的四個(gè)過程: ??
? 一趟排序全過程圖示 ? (A - Z 字母排序, A最小, Z最大)?
?快速排序代碼展示
?具體的代碼
這是我們的輔助函數(shù)exchange: 用于交換任意兩個(gè)數(shù)組元素的位置: // 交換兩個(gè)數(shù)組元素 private static void exchange(int [] a , int i, int j) {int temp = a[i];a[i] = a[j];a[j] = temp; }?
? 這是切分函數(shù)partition, 它完成了一輪排序的主要工作,使得待分割數(shù)組以基準(zhǔn)元素為界,分成了一個(gè)小數(shù)值數(shù)組和一個(gè)大數(shù)值數(shù)組 private static int partition (int[] a, int low, int high) {int i = low, j = high+1;?// i, j為左右掃描指針 PS: 思考下為什么j比i 多加一個(gè)1呢?int pivotkey = a[low];? // pivotkey 為選取的基準(zhǔn)元素(頭元素)while(true) {?while (a[--j]>pivotkey) {?? if(j == low) break; }? // 右游標(biāo)左移while(a[++i]<pivotkey) {?? if(i == high) break;? }? // 左游標(biāo)右移if(i>=j) break;??? // 左右游標(biāo)相遇時(shí)候停止, 所以跳出外部while循環(huán)else exchange(a,i, j) ;? // 左右游標(biāo)未相遇時(shí)停止, 交換各自所指元素,循環(huán)繼續(xù)? ??}exchange(a, low, j); // 基準(zhǔn)元素和游標(biāo)相遇時(shí)所指元素交換,為最后一次交換return j;? // 一趟排序完成, 返回基準(zhǔn)元素位置 }?
? 這是主體函數(shù)sort, 將partition遞歸處理 private static void sort (int [] a,? int low, int high) {if(high<= low) { return; } // 終止遞歸int j = partition(a, low, high);? // 調(diào)用partition進(jìn)行切分sort(a,? low,? j-1);?? // 對(duì)上一輪排序(切分)時(shí),基準(zhǔn)元素左邊的子數(shù)組進(jìn)行遞歸sort(a,? j+1,? high); // 對(duì)上一輪排序(切分)時(shí),基準(zhǔn)元素右邊的子數(shù)組進(jìn)行遞歸 }?
?對(duì)切分函數(shù)partition的解讀
? 1. 直觀上看, partition由兩部分組成: 外部while循環(huán)和兩個(gè)并列的內(nèi)部while循環(huán)。 ? 2. 內(nèi)部While循環(huán)的作用是使得左右游標(biāo)相互靠近 例如對(duì): while (a[--j]>pivotkey) {? ...?? }先將右游標(biāo)左移一位,然后判斷指向的數(shù)組元素和基準(zhǔn)元素pivotkey比較大小, 如果該元素大于基準(zhǔn)元素, 那么循環(huán)繼續(xù),j再次減1,右游標(biāo)再次左移一位...... (循環(huán)體可以看作是空的)
? 3.外部While循環(huán)的作用是不斷通過exchange使得“逆序”元素的互相交換, 不斷向左子數(shù)組小于右子數(shù)組的趨勢靠近,? 對(duì) if(i>=j) break;?從i < j到 i == j 代表了“游標(biāo)未相遇”到“游標(biāo)相遇”的過度過程,此時(shí)跳出外部循環(huán), 切分已接近完成,緊接著通過 exchange(a, low, j) 交換基準(zhǔn)元素和相遇游標(biāo)所指元素的位置, low是基準(zhǔn)元素的位置(頭部元素), j是當(dāng)前兩個(gè)游標(biāo)相遇的位置
? 4. 第一個(gè)內(nèi)部while循環(huán)體里面的的? if(j == low) break;判斷其實(shí)是多余的,可以去除。 因?yàn)樵?/span> while (a[--j]>pivotkey) {?? if(j == low) break; }? // 右游標(biāo)左移中,當(dāng)隨著右游標(biāo)左移,到j(luò) = low + 1的時(shí)候,有 a[--j] == pivotkey為true(兩者都是基準(zhǔn)元素),自動(dòng)跳出了while循環(huán),所以就不需要在循環(huán)體里再判斷 j == low 了
? 5. 注意一個(gè)細(xì)節(jié): j 比 i 多加了一個(gè)1,為什么? 如下 int i = low, j = high+1結(jié)合下面兩個(gè)While循環(huán)中的判斷條件:
while (a[--j]>pivotkey) {? ...?? } while (a[++i]<pivotkey) {? ...? }?可知道, 左游標(biāo) i 第一次自增的時(shí)候, 跳過了對(duì)基準(zhǔn)元素 a[low] 所執(zhí)行的 a[low] < pivotkey判斷, 這是因?yàn)?span style="text-decoration:underline;">在我們當(dāng)前的算法方案里,基準(zhǔn)元素和左游標(biāo)初始所指的元素是同一個(gè),所以沒有執(zhí)行a[low]>pivotke這個(gè)判斷的必要。所以跳過( 一開始a[low] == pivotkey,如果執(zhí)行判斷那么一開始就會(huì)跳出內(nèi)While循環(huán),這顯然不是我們希望看到的)
? 而相比之下,右游標(biāo)卻必須要對(duì)它初始位置所指的元素執(zhí)行a[++i]<pivotkey , 所以 j 比 i 多加了一個(gè) ? ? ?對(duì)主體函數(shù)sort的解讀
? 1. high<= low是判斷遞歸結(jié)束的條件 2. int j = partition(a, low, high);? 有兩種作用: 一是進(jìn)行一輪切分, 二是取得上一輪的基準(zhǔn)元素的最終位置j, 傳遞給另外兩個(gè)sort函數(shù),通過另外兩個(gè)sort函數(shù)的調(diào)用 sort(a,? low,? j-1);?? sort(a,? j+1,? high);進(jìn)行下一輪遞歸,設(shè)置j -1 和j + 1 是因?yàn)樯弦惠喕鶞?zhǔn)元素的位置已經(jīng)是有序的了,不要再納入下一輪遞歸里
? 快速排序QuickSort類的全部代碼: public class QuickSort {// 交換兩個(gè)數(shù)組元素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 多加一個(gè)1呢?int pivotkey = a[low];? // pivotkey 為選取的基準(zhǔn)元素(頭元素)while(true) {?while (a[--j]>pivotkey) {?? if(j == low) break; }? // 右游標(biāo)左移while(a[++i]<pivotkey) {?? if(i == high) break;? }? // 左游標(biāo)右移if(i>=j) break;??? // 左右游標(biāo)相遇時(shí)候停止, 所以跳出外部while循環(huán)else exchange(a,i, j) ;? // 左右游標(biāo)未相遇時(shí)停止, 交換各自所指元素,循環(huán)繼續(xù)? ??? }exchange(a, low, j); // 基準(zhǔn)元素和游標(biāo)相遇時(shí)所指元素交換,為最后一次交換return j;? // 一趟排序完成, 返回基準(zhǔn)元素位置 ? }private static void sort (int [] a,? int low, int high) {if(high<= low) { return; } // 當(dāng)high == low, 此時(shí)已是單元素子數(shù)組,自然有序, 故終止遞歸int j = partition(a, low, high);? // 調(diào)用partition進(jìn)行切分sort(a,? low,? j-1);?? // 對(duì)上一輪排序(切分)時(shí),基準(zhǔn)元素左邊的子數(shù)組進(jìn)行遞歸sort(a,? j+1,? high); // 對(duì)上一輪排序(切分)時(shí),基準(zhǔn)元素右邊的子數(shù)組進(jìn)行遞歸 ? }public static void sort (int [] a){ //sort函數(shù)重載, 只向外暴露一個(gè)數(shù)組參數(shù)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]);}} }?
結(jié)果: 01124556789?
?優(yōu)化點(diǎn)一 —— 切換到插入排序
? 對(duì)于小數(shù)組而言, 快速排序比插入排序要慢, 所以在排序小數(shù)組時(shí)應(yīng)該切換到插入排序。 只要把sort函數(shù)中的 if(high<= low) { return; }?
改成: if(high<= low + M) {? Insertion.sort(a,low, high) return; } // Insertion表示一個(gè)插入排序類?
就可以了,這樣的話,這條語句就具有了兩個(gè)功能: 1. 在適當(dāng)時(shí)候終止遞歸 2. 當(dāng)數(shù)組長度小于M的時(shí)候(high-low <= M), 不進(jìn)行快排,而進(jìn)行插排 ? 轉(zhuǎn)換參數(shù)M的最佳值和系統(tǒng)是相關(guān)的,一般來說, 5到15間的任意值在多數(shù)情況下都能令人滿意 ? 例如, 將sort函數(shù)改成: ? private static void sort (int [] a,? int low, int high) {if(high<= low + 10) {? Insertion.sort(a,low, high) return; } // Insertion表示一個(gè)插入排序類int j = partition(a, low, high);? // 調(diào)用partition進(jìn)行切分sort(a,? low,? j-1);?? // 對(duì)上一輪排序(切分)時(shí),基準(zhǔn)元素左邊的子數(shù)組進(jìn)行遞歸sort(a,? j+1,? high); // 對(duì)上一輪排序(切分)時(shí),基準(zhǔn)元素右邊的子數(shù)組進(jìn)行遞歸}?
?優(yōu)化點(diǎn)二 —— 基準(zhǔn)元素選取的隨機(jī)化
? 上面說過,基準(zhǔn)元素的選取是任意的,但是不同的選取方式對(duì)排序性能的影響很大。 ? 在上面所有的快速排序的例子中,我們都是固定選取基準(zhǔn)元素,這種操作做了一個(gè)假設(shè)性的前提:數(shù)組元素的分布是隨機(jī)的。而如果數(shù)組不是隨機(jī)的,而是有一定順序的,甚至在最壞的情況下:完全正序或完全逆序, 這個(gè)時(shí)候麻煩就來了:?快排所消耗的時(shí)間大大延長,完全達(dá)不到快排應(yīng)有的效果。 ? 所以為了保證快排算法的隨機(jī)化,我們必須進(jìn)行一些優(yōu)化。 ? 下面介紹的方法有三種:?
當(dāng)然來了,因?yàn)閬y序函數(shù)的運(yùn)行,這會(huì)增加一部分耗時(shí),但這可能是值得的 2.通過隨機(jī)數(shù)保證取得的基準(zhǔn)元素的隨機(jī)性 ? private static int getRandom (int []a, int low, int high) {int RdIndex = (int) (low + Math.random()* (high - low)); // 隨機(jī)取出其中一個(gè)數(shù)組元素的下標(biāo)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 多加一個(gè)1呢?int pivotkey = getRandom (a, low, high); // 基準(zhǔn)元素隨機(jī)化while(true) {?while (a[--j]>pivotkey) {?? if(j == low) break; }? // 右游標(biāo)左移while(a[++i]<pivotkey) {?? if(i == high) break;? }? // 左游標(biāo)右移if(i>=j) break;??? // 左右游標(biāo)相遇時(shí)候停止, 所以跳出外部while循環(huán)else exchange(a,i, j) ;? // 左右游標(biāo)未相遇時(shí)停止, 交換各自所指元素,循環(huán)繼續(xù)? ??? }exchange(a, low, j); // 基準(zhǔn)元素和游標(biāo)相遇時(shí)所指元素交換,為最后一次交換return j;? // 一趟排序完成, 返回基準(zhǔn)元素位置}?
? 3.? 三數(shù)取中法(推薦) 一般認(rèn)為, 當(dāng)取得的基準(zhǔn)元素是數(shù)組元素的中位數(shù)的時(shí)候,排序效果是最好的,但是要篩選出待排序數(shù)組的中位數(shù)的成本太高, 所以只能從待排序數(shù)組中選取一部分元素出來再取中位數(shù), 經(jīng)大量實(shí)驗(yàn)顯示: 當(dāng)篩選數(shù)組的長度為3時(shí)候,排序效果是比較好的, 所以由此發(fā)展出了三數(shù)取中法: ? 三數(shù)取中法: 分別取出數(shù)組的最左端元素,最右端元素和中間元素, 在這三個(gè)數(shù)中取出中位數(shù),作為基準(zhǔn)元素。 ? package mypackage;public class QuickSort {// 交換兩個(gè)數(shù)組元素private static void exchange(int [] a , int i, int j) {int temp = a[i];a[i] = a[j];a[j] = temp;}// 選取左中右三個(gè)元素,求出中位數(shù), 放入數(shù)組最左邊的a[low]中private static int selectMiddleOfThree(int[] a, int low, int high) {int middle = low + (high -? low)/2;? // 取得位于數(shù)組中間的元素middleif(a[low]>a[high])??? {? exchange(a, low, high);? //此時(shí)有 a[low] < a[high] ??? }if(a[middle]>a[high]){ exchange(a, middle, high); //此時(shí)有 a[low], a[middle] < a[high] ??? }if(a[middle]>a[low]) {exchange(a, middle, low); //此時(shí)有a[middle]< a[low] < a[high] ??? }return a[low];? // a[low]的值已經(jīng)被換成三數(shù)中的中位數(shù), 將其返回 ? }private static int partition (int[] a, int low, int high) {int i = low, j = high+1;????? // i, j為左右掃描指針 PS: 思考下為什么j比i 多加一個(gè)1呢?int pivotkey = selectMiddleOfThree( a, low, high);while(true) {?while (a[--j]>pivotkey) {?? if(j == low) break; }? // 右游標(biāo)左移while(a[++i]<pivotkey) {?? if(i == high) break;? }? // 左游標(biāo)右移if(i>=j) break;??? // 左右游標(biāo)相遇時(shí)候停止, 所以跳出外部while循環(huán)else exchange(a,i, j) ;? // 左右游標(biāo)未相遇時(shí)停止, 交換各自所指元素,循環(huán)繼續(xù)? ??? }exchange(a, low, j); // 基準(zhǔn)元素和游標(biāo)相遇時(shí)所指元素交換,為最后一次交換return j;? // 一趟排序完成, 返回基準(zhǔn)元素位置 ? }private static void sort (int [] a,? int low, int high) {if(high<= low) { return; } // 當(dāng)high == low, 此時(shí)已是單元素子數(shù)組,自然有序, 故終止遞歸int j = partition(a, low, high);? // 調(diào)用partition進(jìn)行切分sort(a,? low,? j-1);?? // 對(duì)上一輪排序(切分)時(shí),基準(zhǔn)元素左邊的子數(shù)組進(jìn)行遞歸sort(a,? j+1,? high); // 對(duì)上一輪排序(切分)時(shí),基準(zhǔn)元素右邊的子數(shù)組進(jìn)行遞歸 ? }public static void sort (int [] a){ //sort函數(shù)重載, 只向外暴露一個(gè)數(shù)組參數(shù)sort(a, 0, a.length - 1);} }
?
? ?優(yōu)化點(diǎn)三 —— 去除不必要的邊界檢查
? 我在上面說過:“ 第一個(gè)內(nèi)部while循環(huán)體里面的的? if(j == low) break;判斷其實(shí)是多余的,可以去除”? (請(qǐng)把文章往上翻到標(biāo)題—“對(duì)切分函數(shù)partition的解讀”中的第4點(diǎn)) ? 那么, 能不能把另外一個(gè)邊界檢查? if(i == high) break; 也去除呢? 當(dāng)然是不能直接去除的,但是我們可以通過一些技巧使得我們能夠去除它 ? 首先要理解的是 if(i == high) break;的作用: 防止 i 增加到超過數(shù)組的上界, 造成數(shù)組越界的錯(cuò)誤。 那么按照同樣的思考方式,對(duì)于 while(a[++i]<pivotkey) {?? if(i == high) break;? }我們只要嘗試把這一作用交給a[++i]<pivotkey去完成, 不就可以把 if(i == high) break; 給去掉了嗎?
? 這里的技巧就是: 在排序前先把整個(gè)數(shù)組中最大的元素移到數(shù)組的最右邊,這樣的話, 就算左游標(biāo)i增加(右移)到數(shù)組的最右端,a[++i]<pivotkey也會(huì)判定為false(數(shù)組最大值當(dāng)然是大于或等于基準(zhǔn)元素的), 從而無法越界。 ? 代碼: public class QuickSort {// 交換兩個(gè)數(shù)組元素private static void exchange(int [] a , int i, int j) {int temp = a[i];a[i] = a[j];a[j] = temp;}//將原數(shù)組里最大的元素移到最右邊, 構(gòu)造“哨兵”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 多加一個(gè)1呢?int pivotkey = a[low];? // pivotkey 為選取的基準(zhǔn)元素(頭元素)while(true) {?while (a[--j]>pivotkey) {?? }? // 空的循環(huán)體while(a[++i]<pivotkey) {?? }? // 空的循環(huán)體if(i>=j) break;??? // 左右游標(biāo)相遇時(shí)候停止, 所以跳出外部while循環(huán)else exchange(a,i, j) ;? // 左右游標(biāo)未相遇時(shí)停止, 交換各自所指元素,循環(huán)繼續(xù)?}exchange(a, low, j); // 基準(zhǔn)元素和游標(biāo)相遇時(shí)所指元素交換,為最后一次交換return j;? // 一趟排序完成, 返回基準(zhǔn)元素位置 ? }private static void sort (int [] a,? int low, int high) {if(high<= low) { return; } // 當(dāng)high == low, 此時(shí)已是單元素子數(shù)組,自然有序, 故終止遞歸int j = partition(a, low, high);? // 調(diào)用partition進(jìn)行切分sort(a,? low,? j-1);?? // 對(duì)上一輪排序(切分)時(shí),基準(zhǔn)元素左邊的子數(shù)組進(jìn)行遞歸sort(a,? j+1,? high); // 對(duì)上一輪排序(切分)時(shí),基準(zhǔn)元素右邊的子數(shù)組進(jìn)行遞歸 ? }public static void sort (int [] a){ //sort函數(shù)重載, 只向外暴露一個(gè)數(shù)組參數(shù)Max(a); // 將原數(shù)組里最大元素移到最右邊, 構(gòu)造“哨兵”sort(a, 0, a.length - 1);} }?
? 如果看到這里對(duì)“哨兵”這個(gè)概念還不是很清楚的話,看看下面這張圖示: 《三種哨兵》 ??
關(guān)于哨兵三再說幾句: 在處理內(nèi)部子數(shù)組的時(shí)候,右子數(shù)組中最左側(cè)的元素可以作為左子數(shù)組右邊界的哨兵(可能有點(diǎn)繞) ?優(yōu)化點(diǎn)四 —— 三切分快排(針對(duì)大量重復(fù)元素)
? 普通的快速排序還有一個(gè)缺點(diǎn), 那就是會(huì)交換一些相同的元素 ? 回憶一下我在前面提到的快排中對(duì)左右游標(biāo)指定的規(guī)則:- 左游標(biāo)向右掃描, 跨過所有小于基準(zhǔn)元素的數(shù)組元素, 直到遇到一個(gè)大于或等于基準(zhǔn)元素的數(shù)組元素, 在那個(gè)位置停下。
- 右游標(biāo)向左掃描, 跨過所有大于基準(zhǔn)元素的數(shù)組元素,直到遇到一個(gè)大于或等于基準(zhǔn)元素的數(shù)組元素,在那個(gè)位置挺停下
?
? 所以由此人們研究出了三切分快排(三路劃分) , 在左右游標(biāo)的基礎(chǔ)上,再增加了一個(gè)游標(biāo),用于處理和基準(zhǔn)元素相同的元素 ??
? 代碼如下: 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書里的截圖實(shí)在太模糊了,所以我自己用手機(jī)照了一張)?
?
其實(shí)啊,我只是把你們喝咖啡的時(shí)間,都用來喝啤酒而已總結(jié)
以上是生活随笔為你收集整理的【算法】快速排序算法的编码和优化的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: iOS开发-Xcode入门ObjC程序
- 下一篇: 如何让编码更加的标准