標籤:排序演算法 快速排序
快速排序是東尼·霍爾在1962提出的劃分交換排序,並採用一種分治的策略。在這,我們先總結一下:快速排序 = 劃分交換排序 + 分治。然後我們在一一介紹他。
劃分交換排序
在討論它時,感覺有種看了崔顥《黃鶴樓》之後,再去寫黃鶴樓的感覺,因為MoreWindows寫 得白話經典演算法系列之六 快速排序 快速搞定已經足夠出色了。我在這隻是進行簡單的複述,你需要瞭解更多請看他的博文。
先來看看劃分交換排序的具體演算法描述:
1.從數列中選出一個數作為基準
2.從數組中選出比它大的數放右邊,比它小的數放左邊
3.重複以上操作,直至只剩最後一個數
看了演算法描述呢,感覺還是有點晦澀,我們可以換過一種思考方式:挖坑填數。
我們先來看看具體執行個體:
給出一個數組,並將第一個數作為基準
|
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
|
72 |
6 |
57 |
88 |
60 |
42 |
83 |
73 |
48 |
85
|
初始化時,i = 0, j = 9; X = array[0] = 72
由於array[0] 已經儲存到X中,所以,可以理解已經在i = 0處,挖好了一個坑,可以將其他的資料填充到這個坑中。
接下來充由j位置從後往前進行尋找小於等於基準的值,在j = 8處48符合條件,這時我們將它挖出,並填到上一個坑處,即array[0] = array[8];i++;此時,我們填好了一個坑,但是又重建了一個新坑,該怎麼辦呢。我們依舊需要找一個其他的值將它填上,接下來我們在i的位置從前往後找,直至找到大於等於基準的值。當i = 3;時,符合條件,將array[8] = array[3];j--;數組便變成了:
|
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
|
48 |
6 |
57 |
88 |
60 |
42 |
83 |
73 |
88 |
85
|
此時i = 3, j = 7
重複以上操作,先從後往前找,在從前往後找。
從j開始往前找,在j = 5處的42小於基準值,將它填到i = 3處,array[3] = array[5]; i++;
從i = 4處開始從前往後找,當i = 5時依舊沒有合格值,且(i<j)不滿足,所以應該跳出迴圈,退出。
此時 i=5,且j = 5,整個數組剩下一個坑就是i = 5時,因為第一次的劃分已經完畢,所以講基準值填回坑中,即array[i]=X;
數組變成:
|
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
|
48 |
6 |
57 |
42 |
60 |
72 |
83 |
73 |
88 |
85
|
可以看出,i=5之前的值全部小於或等於array[5],之後的值大於等於array[5]。
請注意,不是每次的劃分都會是這麼理想,當所以怎麼選擇基準會成為影響效率的一大原因,其次因為每次是找出大於等於或小於等於的值進行填坑,所以該演算法是不穩定的。
接下來,我們將數組分為兩個集合p1 = array[0...4], p2 = array[6...9],那麼我們先來看看這段思想化成的代碼吧。
public static void sort(int[] array, int left, int right){ int i = left; int j = right; if (i >= j){ return; } int pivot = array[i]; while (i < j){ while ((i < j) && (array[j] > pivot)){ j--; } if (i < j){ array[i] = array[j]; } while ((i < j) && (array[i] < pivot)){ i++; } if (i < j){ array[j] = array[i]; } } array[i] = pivot; }
然後再劃分好之後繼續如此,直至集合中元素小於等於1個,好好想想,這個有沒有像什麼,沒錯,樹,小的放左邊,大的放右邊,依照方法想,我們可以想到遞迴。好了下來,就是我們的分治了。
分治法
在這,不做詳細的介紹,假如有興趣,推薦部落格 五大常用演算法之一:分治演算法。
分治演算法最簡單的解釋就是:將一個問題分解成多個小問題,再遞迴解決所有的問題,最後將解合并,產生最終解。
他的演算法模式:
Divide-and-Conquer(P) 1. if |P|≤n0 2. then return(ADHOC(P)) 3. 將P分解為較小的子問題 P1 ,P2 ,...,Pk 4. for i←1 to k 5. do yi ← Divide-and-Conquer(Pi) △ 遞迴解決Pi 6. T ← MERGE(y1,y2,...,yk) △ 合并子問題 7. return(T)
分治在快排中的具體思想:
在一個給定的一個數組R[left....right]中,先從數組中選取一個元素作為基準pivort,根據基準,可以將數組分為兩個子數組R[left....pivortpos-1]和R[pivortpost+1....right]。根據定義,可以知道,左邊的元素全部小於等於基準pivort,右邊的元素全部大於等於pivort,所以pivort已經在正確的位置上了,在接下來的遞迴中,不需要在對它進行劃分。
劃分的關鍵是求出基準所在的位置pivortpos,劃分的結果可以簡單表示為pivort = R[pivortpos];換分的結果可以簡單表示為:
R[left...pivortpos-1] <= pivort <= R[pivortpos+1....right]
left <= pivortpos <= right
所以在我們可以寫出分治的部分代碼
public static void sort1(int[] array, int left, int right){ if (left >= right){ return; } int i = left; int j = right; int pivort = array[i]; while(i < j){ //劃分交換 int pivortpos = Partition(R, left, right); sort(array, left, pivortpos-1); sort(array,pivortpos+1, right); } }
所以,我們可以將兩個代碼結合,得到完整的代碼:
public static void sort(int[] array, int left, int right){ int i = left; int j = right; if (i >= j){ return; } int pivot = array[i]; while (i < j){ while ((i < j) && (array[j] > pivot)){ j--; } if (i < j){ array[i] = array[j]; } while ((i < j) && (array[i] < pivot)){ i++; } if (i < j){ array[j] = array[i]; } } array[i] = pivot; sort(array, left, i-1); sort(array, i+1, right); } 說完了,但是可能還是感覺有點不清楚,那我們繼續來看一下他的執行過程:
首先可以大家可以先看個動畫示範,是關於劃分交換的,當然假如你覺得自己足夠清楚,可以跳過;
假如我們存在以下集合:
在這我們可以看到,他並不是穩定的排序演算法,並且他的效率和基準的選擇有很大的關係。
總結:
快速排序的最壞時間複雜度是O(n^2),最好時間複雜度為O(nlogn),平均時間複雜度為O(nlogn),他是基於關鍵字比較的內部排序演算法中速度最快的。
空間複雜度:因為是遞迴樹的高度O(logn),所以需要棧空間O(logn),最差時是O(n)。
常用演算法之排序演算法三【快速排序】