“傻文,如果你也跟我一樣沒有耐性,看我的文章吧,專爲沒有耐性的朋友準備”
研究了幾天這個快速排序的算法,可能我比較笨,斷斷續續加起來估計超過5個小時的時間了。
因爲我很沒有耐性,所以總是看一點忘一點。
從我本身來說 我覺得這個算法的邏輯性還是很強的,閱讀時真的需要保持清醒,因爲我不確定我能說的足夠清楚讓大家一次明白。
好,我準備先講一下快速排序的算法描述。
其實快速排序和冒泡排序屬於同一大類:交換排序中,主要思想都是把一個大數和一個小數交換位置,以這種方法來使數組逐漸有序。
今天我們待排序的數組是 {6,8,4,3,5,9,11,7}
在排序之前我先說下大致思路:
1. 選擇一個數字作爲支點;
2. 把小於支點的放到支點的左面,大於支點的放到右面;
3. 分別對支點的左右兩部分,再對每一部分分別做#1和#2。那什麼時候結束呢?我們試着討論一下。
下面我具體說說到底這個算法是如何執行的。
我還是想打斷一下,因爲第一步是選擇一個“支點”, 這個支點的選擇通常有3種方法:
1. 第一個元素
2. 中間的元素
3. 最後一個元素
我們討論前兩種,算法大同小異,只是執行過程還是有細微的不同。
好吧開始,我們選擇第一個元素作爲支點。
我們會分幾趟來完成整個排序,第一趟的目標是把數組以支點爲中心,左右兩邊分開。是這樣的:
1. 支點選擇爲6;
2. 我們會從左右兩邊同時開始比較;
3. 比較最右面的元素,7, 7 大於 6,故他理所當然應該在右面,沒有問題;
4. 繼續從右面往左走,11 也大於 6,不用理會;
5. 9 大於 6,繼續;
6. 5, 5 大於 6嗎?否,故,停下來,我們需要記住這個右位置的元素 :5;
7. 這時我們再從左面開始,因爲我們選擇了6作爲支點,這個元素我們不考慮,直接看他下一個元素:8;
8. 8 小於 6嗎?(注意,這裏的條件變成了小於,前面是大於!原因很簡單,左面的應該比支點小,右面的應該比支點大)否,故停下來,我們也記住這個左位置的元素: 8!
9. 兩邊都停下來了,這時怎麼辦?交換!
10. 好的,在停下來的地方,我們交換他們,左面的 8 和 右面的 5 進行交換,交換後數組變爲:{6,5,4,3,8,9,11,7};
11. 不要太高興,這一趟還沒完呢,我們要繼續從剛纔右位置開始往左走,這個很簡單了,下一個元素是 3, 3 大於 6嗎?否,故再次停下來;
12. 從左面繼續,左位置下一個元素是4, 4 小於 6嗎?是的,繼續;
13. 下一個元素是3, 3 小於 6 嗎?是的,... 咦?剛纔我們不是已經比較過3跟6的關係了嗎?“嗯,是的,在第#11步的時候,我們比較過,但是因爲 3 大於 6不成立,我們停了下來。”;
14. 對!這就是我們這一趟排序停止的一個條件:從左右兩邊同時往中間走,在某個位置“碰頭”了(左位置已經等於右位置 或者 左位置已經大於右位置,這兩種都算碰頭吧),這時我們就可以停下來這一趟排序了;
15. 但我們需要做一件事情,那就是把支點跟這個停下來的位置做交換,交換後我們得到的數組爲{3,5,4,6,8,9,11,7}。
停下來觀察一下,我們選擇了 6 爲支點,此時左面爲 3, 5, 4; 右面爲 8, 9, 11, 7
很明顯,左面 < 支點 < 右面,這個關係成立了。
我們離目標近了一步!
下面呢,分別對左右兩部分再進行上面的步驟。
先說左面吧:
我們要排序的數組爲: 3,5,4
我們還選擇第一個元素:3 作爲支點。
1. 從右面開始,4 大於 3嗎?是的,繼續;
2, 5 大於 3 嗎?是的,繼續;
3, 3 大於 3 嗎?否,停下來(我們說這種情況的停位置在元素: 3,下面會再提到“停位置”);
4. 從左面開始,5 大於 3 ?。。。 啊哦!左位置(此時指向元素5)已經大於右位置(此時指向元素3)了,這一趟又停了。
5. 所以我們得到了:3,5,4, 支點右面的元素 5, 4 都是大於支點的,而左面沒有元素了,所以我們只需要考慮右面部分的排序就行了;
繼續對右面部分 5, 4 進行排序:
1. 選擇5作爲支點;
2. 從右面開始往左走,4大於5嗎?否,停下來(我們說這個停位置在:4,下面還會提到“停位置”);
3. 從左面往右走,4?噢!又碰頭了,這一趟又結束了;
4. 我們將支點跟停下來的位置進行交換,得到 4, 5
所以整個左面部分就排序完成了:3, 4, 5。
停!
我想你一定不滿意了,你會說:你好像沒有說清楚什麼時候交換什麼時候不交換啊!
是的!
你注意到這一點了,很好!
分兩種情況:
一種情況是:
尚未碰頭的時候,從右往左遍歷時發現了有比支點小的值,而從左往右遍歷時發現了有比支點大的值,而此時還沒有碰頭,是一定要交換的!
另一種情況:
我們在對整個數組做一趟快速排序的時候,我們在最後做了支點和停位置(6 和 3 交換了)元素的交換;
在對左面部分做快速排序時,我們沒有對支點和停位置(3 和 3 沒有交換),呵呵 也不需要交換對吧,因爲他們指向了同一個元素;
在最後一趟快速排序的時候,也做了支點和停位置(5 和 4 交換了)的交換。
嗯?有什麼規律嗎?
其實這個規律不太容易被發現,但稍微悉心就能看到:如果從右位置停下來的時候跟支點重合,那就不做交換,如果停下來的位置還沒到支點位置,那就交換。
再回頭看看,是不是呢?
對右面部分8, 9, 11, 7的排序我就不囉嗦了,想必你一定很清楚了。
我們來說說Java算法實現吧!
public class QuickSort {
public static void quickSort(int[] sort, int start, int end) {
// 這個條件很容易理解吧,當只有一個元素的時候 start == end,
// 而我們知道 一個元素的數組就是有序的不需要排序
if (end > start) {
// 找到支點,一分爲二
int p = partition(sort, start, end);
// 快排左半部分
quickSort(sort, start, p - 1);
// 快排右半部分
quickSort(sort, p + 1, end);
}
}
/**
* 找到分割點
* @return
*/
public static int partition(int[] sort, int start, int end) {
int p = start;
int pValue = sort[p];
int left = start;
int right = end + 1;
for (;;) {
// 從右面往左走,直到某個值小於或等於支點停止
while (sort [--right] > pValue) {
if (right <= left) break;
}
// 從左面往右走,直到某個值大於或等於支點停止
while (sort[++left] < pValue ) {
if (left >= right) break;
}
// 左位置大於等於右位置說明已經碰頭了
// 如果還沒有碰頭,
//從右往左遍歷時發現了有比支點小的值,
// 而從左往右遍歷時發現了有比支點大的值,而此時還沒有碰頭,是一定要交換的!
if (left >= right)
break;
else
// 沒碰頭時的交換
swap(sort, left, right);
}
// 如果右面的停位置等於支點位置,則不交換
if (right == p) {
return right;
}
else {
// 右面的停位置不等於支點位置,則需要交換
swap(sort, p, right);
return right;
}
}
// 交換
private static void swap(int[] sort, int left, int right) {
int tmp = 0;
tmp = sort[left];
sort[left] = sort[right];
sort[right] = tmp;
}
// For test
public static void main(String[] args) {
int[] sort = {6,8,4,3,5,9,11,7};
quickSort(sort, 0, sort.length - 1);
for (int i : sort) {
System.out.print(i + ", ");
}
}
}
複雜度:
時間複雜度:O(n*logn)
空間複雜度:O(1)
在下一篇中,我們繼續討論下,如果選擇數組的中間元素爲支點,是怎麼樣的情況。