快速排序是個非常經典、高效、常用的排序演算法。很多語言標準庫裡的排序演算法都有用到它。
原理
快排原理其實比較簡單,就是將原本很大的數組拆成小數組去解決問題。
要拆就得找個拆的位置。如果吧這個位置稱為支點,那麼快速排序問題就變成了不斷的去找到拆分的支點元素位置。
通常找支點就是以某個元素為標準,分別從最右側元素向左找到比指定元素小的位置,再從最左側開始向右找比指定元素大的位置。如果兩個位置不相同就交換兩個位置,在繼續分表從兩頭相向尋找。找到合適的位置就是我們需要的支點。支點兩邊的元素再各自重複上面的操作,直到分拆出來的子數組只剩一個元素。分拆結束,順序也就拍好了。
那麼問題來了,以哪個元素為標準去比較呢?比如可以選第一個元素。
複雜度
理想情況下找到的支點可以把數組拆分成左右長度相近的子數組,此時時間複雜度為O(n*logn)
而最差情況則是每次找到的支點元素都在某一次,導致另一側完全浪費,尋找支點的過程也浪費。這個時候用時會達到O(n^2)。
由於會打亂相同元素原有的順序,所以快排也是一個不穩定排序。所以常用在普通類型資料的排序中。
代碼實現
package mainimport ( "time" "fmt" "math/rand")func main() { var length = 10 var list []int // 以時間戳記為種子產生隨機數,保證每次運行資料不重複 r := rand.New(rand.NewSource(time.Now().UnixNano())) for i := 0; i < length; i++ { list = append(list, int(r.Intn(50))) } fmt.Println(list) quickSort(list, 0, length-1) fmt.Println(list)}func quickSort(list []int, start, end int) { // 只剩一個元素時就返回了 if start >= end { return } // 標記最左側元素作為參考 tmp := list[start] // 兩個遊標分別從兩端相向移動,尋找合適的"支點" left := start right := end for left != right { // 右邊的遊標向左移動,直到找到比參考的元素值小的 for list[right] >= tmp && left < right { right-- } // 左側遊標向右移動,直到找到比參考元素值大的 for list[left] <= tmp && left < right { left++ } // 如果找到的兩個遊標位置不統一,就遊標位置元素的值,並繼續下一輪尋找 // 此時交換的左右位置的值,右側一定不大於左側。可能相等但也會交換位置,所以才叫不穩定的排序演算法 if left < right { list[left], list[right] = list[right], list[left] fmt.Println(list) } } // 這時的left位置已經是我們要找的支點了,交換位置 list[start], list[left] = list[left], tmp // 按支點位置吧原數列分成兩段,再各自逐步縮小範圍排序 quickSort(list, start, left-1) quickSort(list, left+1, end)}
運行結果
雜談
遇到最差情況時,上面演算法的效能就比較糟糕了,和普通的插入排序差不多。所以為了避免選了個糟糕的支點,可以選擇數組中間元素作為對比的標準,或是選3個元素,取中間大小的元素作為參考項。這時可以在一定程度上最佳化效能。不過最快情況的情境是在太少見,基本可以忽略掉。
還有個可最佳化的點,就是在資料量很少時,快排並不能體現速度優勢,反而在二十幾個元素以內的排序中比插入排序還慢。所以可以在遞迴迴圈裡加個判斷,如果一側的元素數量小於某個值(比如20)時直接使用插入排序。