# Lecture 06 - Quick sort > 課程內容 : 中興大學資工系 范耀中教授 (112 學年度第 2 學期) > > 其他科目內容請參見 [[Here]](https://hackmd.io/@ChenZE/By4SOO6Jyl) 快速排序法(Quick sort)與 Merge sort 類似,都是使用 divide and conquer 的概念,將大的陣列分成小問題後逐一解決。但 Quick sort 引入了分割(partition)的概念來分割小問題 ## 1. Quick sort 演進 ### 1.1 Idea (1): Partition items 分割 > Procedure > 1. 選擇一個基準(pivot) > 2. 從左開始掃描,比基準小的放左側,比基準大的放右側 > 3. 結束後該基準就會是在正確的位置,且左右分別有比基準小/大的子陣列 > 4. 對左右的子陣列繼續做上述動作 假設陣列 `[4, 2, 6, 1, 5, 7, 3]` 並以 `4` 作為基準 | Scan | Comparison | Left array | Right array | | :-: | :- | :- | :- | | 4 | Nothing | `[ ]` | `[ ]` | | 2 | Less than | `[2]` | `[ ]` | | 6 | Greater than | `[2]` | `[6]` | | 1 | Less than | `[2, 1]` | `[6]` | | 5 | Greater than | `[2, 1]` | `[6, 5]` | | 7 | Greater than | `[2, 1]` | `[6, 5, 7]` | | 3 | Less than | `[2, 1, 3]` | `[6, 5, 7]` | 最後結果為 `[2, 1, 3, 4, 6, 5, 7]`,可以發現基準元素 `4` 已經排好,且左邊是比 `4` 小的陣列,右邊是比 `4` 大的陣列。再繼續對左邊及右邊做排序 ![S__2203650](https://hackmd.io/_uploads/S1hnk5640.jpg) :::info 複雜度 : $N \cdot \log N$ 缺點 : 浪費兩倍的記憶體空間 ::: ![S__2203651](https://hackmd.io/_uploads/HJcFg9TNR.jpg) ### 1.2 Solution : In-place > Procedure > 1. 同樣選擇一個基準(pivot)元素 > 2. 由左至右開始掃描整個陣列 > 3. 若遇到比基準值大的元素,就從陣列尾端**依序**拿一個元素做交換 > 4. 若遇到比基準值小的元素,就直接與基準值交換 > 5. 直到所有元素都被交換過,則該基準值已被排好 > 6. 同樣可以得到兩個子陣列,依序做上述操作 假設陣列 $[4,\ 5,\ 3,\ 1,\ 6,\ 7,\ 8]$ 並以 $4$ 作為基準 | Scan | Comparison | Result | | :-: | :-: | :-: | | 4 | Nothing | $[4,\ 5,\ 3,\ 1,\ 6,\ 7,\ 8]$ | | 5 | $>$ | $[4,\ 8,\ 3,\ 1,\ 6,\ 7,\ \bf{5}]$ | | 8 | $>$ | $[4,\ 7,\ 3,\ 1,\ 6,\ \bf{8},\ \bf{5}]$ | | 7 | $>$ | $[4,\ 6,\ 3,\ 1,\ \bf{7},\ \bf{8},\ \bf{5}]$ | | 6 | $>$ | $[4,\ 1,\ 3,\ \bf{6},\ \bf{7},\ \bf{8},\ \bf{5}]$ | | 1 | $<$ | $[1,\ 4,\ 3,\ \bf{6},\ \bf{7},\ \bf{8},\ \bf{5}]$ | | 3 | $<$ | $[1,\ 3,\ 4,\ \bf{6},\ \bf{7},\ \bf{8},\ \bf{5}]$ | **註 : 粗體字表示已經換過位置** 到最後因為 $6$ 已經交換過位置,因此排序結束且基準元素 $4$ 已經排好位置,左右兩側分別是比 $4$ 小/大的子陣列,將左右子陣列依序做上述操作 ![S__2203652](https://hackmd.io/_uploads/BypQu9pEA.jpg) :::info 優點 : 只需要一個 Array 的空間 缺點 : 會產生不必要的搬移,例如一開始明明 $5>4$ 但卻換了一個更大的 $8$ 來到前面 ::: ### 1.3 Solution : 避免不必要的搬移 > Procedure > 1. 同樣選定一個基準值(pivot) > 2. 從陣列左右兩側同時做掃瞄 > 3. 左側(Left scan pointer)尋找比基準值大的元素 > 4. 右側(Right scan pointer)尋找比基準值小的元素 > 5. 找到以後做交換 > 6. 當左右交錯後,掃描停止,並取當前基準值與 Right scan pointer 交換 假設陣列 $[4,\ 5,\ 3,\ 1,\ 6,\ 7,\ 2,]$ 並以 $4$ 作為基準值 | Left pointer | Array | Right pointer | | :-: | :-: | :-: | | 5 | $[4,\ \overline 5,\ 3,\ 1,\ 6,\ 7,\ \mathbf{2},]$ | 2 | | 3 | $[4,\ 2,\ \overline 3,\ 1,\ 6,\ \mathbf{7},\ 5,]$ | 7 | | 1 | $[4,\ 2,\ 3,\ \overline 1,\ \mathbf{6},\ 7,\ 5,]$ | 6 | | 6 | $[4,\ 2,\ 3,\ \mathbf{1},\ \overline 6,\ 7,\ 5,]$ | 1 | | | $[\mathbf{1},\ 2,\ 3,\ 4,\ \overline 6,\ 7,\ 5,]$ | | 基準值 $4$ 已經排好,左右兩側分別為比 $4$ 小與大的子陣列,兩側繼續做上述操作 ![S__2203654](https://hackmd.io/_uploads/BJVu3oaNR.jpg) 以上改進後的版本就是 Quick sort 的核心內容 #### 缺點 考慮遞減排序的陣列 $[7,\ 6,\ 5,\ 4,\ 3,\ 2,\ 1]$ 並以 $7$ 作為基準值 | Left pointer | Array | Right pointer | | :-: | :-: | :-: | | 6 | $[7,\ \overline 6,\ 5,\ 4,\ 3,\ 2,\ \mathbf{1}]$ | 1 | | 5 | $[7,\ 6,\ \overline 5,\ 4,\ 3,\ 2,\ \mathbf{1}]$ | 1 | | 4 | $[7,\ 6,\ 5,\ \overline 4,\ 3,\ 2,\ \mathbf{1}]$ | 1 | | 3 | $[7,\ 6,\ 5,\ 4,\ \overline 3,\ 2,\ \mathbf{1}]$ | 1 | | 2 | $[7,\ 6,\ 5,\ 4,\ 3,\ \overline 2,\ \mathbf{1}]$ | 1 | | 1 | $[7,\ 6,\ 5,\ 4,\ 3,\ 2,\ \overline{\mathbf{1}}]$ | 1 | | | $[\overline{\mathbf{1}},\ 6,\ 5,\ 4,\ 3,\ 2,\ 7]$ | | 從上述會發現排完後的陣列是 $[1,\ 6,\ 5,\ 4,\ 3,\ 2,\ 7]$,但尚未排序完成 ## 2. Quick sort analysis ### 2.1 Best and worse case 由上述最後的例子可以發現 quick sort 的 worse case 以及 best case 所對應的情況。當挑選到最大或最小值作為基準值時,就是 quick sort 的 worse case,因為第一次 scan 後的結果這個基準值會在最左側或是最右側,此時的樹狀圖會是斜的 (如下圖),複雜度為 $(N-1)+(N-2)+...=\cfrac{N^2}{2}$ ![未命名](https://hackmd.io/_uploads/S1jUSA3ykx.png) 而 quick sort 的 best case ,是當選到的基準值是中位數,此時做完第一次 scan 後基準值會在中間,兩側的 sub-tree 會是平均分配的,複雜度是 $N \times \log N$。 ### 2.2 Average case 假設 N 個元素的 array 做 quick sort 的複雜度是 $C_N$ $$ C_N = (N+1) + \frac{C_0+C_{N-1}}{N} + \frac{C_1+C_{N-2}}{N} + ... + \frac{C_{N-1}+C_{0}}{N} $$ 其中, * $N+1$ 表示 left scan 與 right scan 掃過整個 array 並交錯的次數 * $\cfrac{C_0+C_{N-1}}{N}$ 中,$C_0+C_{N-1}$ 表示選到最小值 $C_0$ 所需要付出的成本,$\cfrac{1}{N}$ 表示選到最小值的機率 * 同理,$\cfrac{C_1+C_{N-2}}{N}$ 中,$C_1+C_{N-2}$ 表示選到第二小值 $C_1$ 所需要付出的成本,$\cfrac{1}{N}$ 表示選到最小值的機率 將上式兩側同 $\times N$ 後可得 $$ NC_N = N(N+1) + 2(C_0+C_1+...+C_{N-1}) $$ 且由 $C_N$ 的算法可推得 $C_{N-1}$ 的複雜度如下 : $$ (N-1)C_{N-1} = (N-1)N + 2(C_0+C_1+...+C_{N-2}) $$ 將 $C_N$ 與 $C_{N-1}$ 兩式相減後可得 : $$ NC_N - (N-1)C_{N-1} = 2N + 2C_{N-1} $$ 整理後可得 : $$ \cfrac{C_N}{N+1} = \cfrac{C_{N-1}}{N} + \cfrac{2}{{N+1}} $$ 重複以上的式子做遞迴,可將上式化簡為 : $$ \begin{align} \cfrac{C_N}{N+1} &= \cfrac{C_{N-1}}{N} + \cfrac{2}{{N+1}}\\ &= \cfrac{C_{N-2}}{N-1} + \frac{2}{N} + \frac{2}{N+1}\\ &= \cfrac{C_{N-3}}{N-2} + \frac{2}{N-1} + \frac{2}{N} + \frac{2}{N+1}\\ &= \frac{2}{3} + \frac{2}{4} + \frac{2}{5} + ... + \frac{2}{N+1} \end{align} $$ :::info 為何最後沒有 $\cfrac{2}{1} + \cfrac{2}{2}$ ? 因為拆到最後的 $\cfrac{C_1}{2}$ 表示只剩下一個元素,沒有必要拆解 ::: 將上移項後可得 : $$ \begin{align} C_N &= 2(N+1)(\frac{1}{3} + \frac{1}{4} + \frac{1}{5} + \frac{1}{N+1})\\ &= 2(N+1) \int_3^{N+1}\frac{1}{x}dx\\ &=2(N+1)\ln N \end{align} $$ ### 2.3 Summary of performance 因為 quick sort 的複雜度會依據所選定的基準值不同而有不同的結果,為了避免基準值選到最大或最小值,在排序前會先打亂 array 的順序。 然而,再沒有隨機打亂的情況下,該如何避免選到極值 ?可以嘗試挑選 3 個數並取其中位數,就可保證不是最大或最小值。