演算法基礎--快速排序

本文只是自己的筆記,並不具備過多的指導意義。
為了理解很多都使用了遞迴,而不是自己通過while進行壓棧處理。
程式碼的初衷是便於理解,網上大神優化過的程式碼很多,也不建議在專案中copy本文程式碼。
ofollow,noindex">目錄
- 快速排序的基本思想
- 單次遍歷,確定一個值在陣列中的最終位置
- 將小於等於給定值num的數放在數 組的左邊,大於num的數放在陣列的右
- 將陣列中某個元素的值作為基準值num,確定其在最終排序陣列中的位置
- 經典快排
- 在每段小陣列上確定最後元素的最終位置
- 將大陣列拆分成小陣列
- 經典快排的動圖
- 經典快排的改進
- 雙路快排
- 三路快排
- 基準值對快排時間複雜度的影響
- 隨機快排
- 將中位數作為基準值
快速排序的基本思想
每次排序,確定一個任意值在陣列中的最終位置
具體操作上:
- 以陣列中某個值作為基準值
- 遍歷陣列,將小於基準值的放在左側,大於他的放在右側
- 最終,確定該元素在陣列中的位置。
對於經典快排
- 繼續在該元素左側與右側重複1,2,3步驟。每次確定一個元素的位置最終確定整個陣列的所有元素。
單次遍歷,確定一個值在陣列中的最終位置。
遍歷陣列,某個元素作為基準值,將小於基準值的放在左側,大於他的放在右側。最終,確定該元素在陣列中的位置。
-
將小於等於給定值num的數放在數 組的左邊,大於num的數放在陣列的右邊

/// 給定一個數組arr,把小於等於給定值num的數放在數 組的左邊,大於num的數放在陣列的右邊。並返回其位置 /// /// - Parameters: ///- arr: 陣列 ///- num: 劃分值 /// - Returns: 最終位置 func partition0(arr: inout [Int] ,num:Int) -> Int { if arr.count<2 { return 0 } var p = 0-1//小於等於區域結束位置。 for i in 0..<arr.count { //遍歷整個陣列 if arr[i]<=num { //如果小於給定的num,則擴大小於等於區域,並將其交換進該區域末尾 p=p+1 arr.swapAt(p, i) } } //最終,p左側為小於等於區域,右側為大於區域 return p }
需要注意小於等於區域的初始值為-1,因為最初並沒有任何元素被確定小於num。
-
將陣列中某個元素的值作為基準值num,確定其在最終排序陣列中的位置
既然其左側必然小於等於他,右側必然大於他。那麼他的位置一定不變。
那麼,我們只需要對上述方法進行一些小改動。比如將陣列中最後一位的值作為基準,這樣每次就能確定最後一位的最終位置。
/// 給定一個數組arr,把小於等於末尾值num的數放在陣列的左邊,大於num的數放在陣列的右邊。並返回其位置 /// /// - Parameters: ///- arr: 陣列 /// - Returns: 最終位置 func partition(arr: inout [Int]) -> Int { if arr.count<2 { return 0 } let num = arr[arr.count-1] var p = 0-1//小於等於區域結束位置。 for i in 0..<arr.count { //遍歷整個陣列 if arr[i]<=num { //如果小於給定的num,則擴大小於等於區域,並將其交換進該區域末尾 p=p+1 arr.swapAt(p, i) } } //最終,p左側為小於等於區域,右側為大於區域 return p }
let num = arr[arr.count-1]
所做的,就是上述 將陣列中最後一位的值作為基準
的操作。
對於單次遍歷,可以完成下面的結果

經典快排
-
在每段小陣列上確定最後元素的最終位置
很簡單,值需要將上面的方法新增left,right引數。在大陣列中確定小陣列的左右邊界即可。
/// 在一個數組的left,right範圍內。確定最後一個元素的最終位置 /// /// - Parameters: ///- arr: 陣列 ///- left: 左邊界 ///- right: 右邊界 /// - Returns: 基準元素的最終位置 func partition(arr:inout [Int] ,left:Int ,right:Int) ->Int { var l = left - 1//小於等於區域末端位置 var p = left//遍歷的起始位置,從最左端開始 while p < right { //保證不越界,並且遍歷的範圍不包含右側邊界位置。 if arr[p] <= arr[right] { l+=1 //滿足小於等於,擴大小於等於區域 arr.swapAt(l, p) //並將其交換進該區域末尾 } p+=1 } arr.swapAt(right, l+1) //最後,將右側邊界位置與大於區域首位置(p+1)交換 return l+1; //返回最後一個值的最終位置 }
-
將大陣列拆分成小陣列
以 partition
劃分時找出的最終位置作為再次劃分成左側與右側,要求兩個小陣列繼續進行處理。
/// 快速排序 /// /// - Parameter arr: 陣列 func quickSort(arr:inout [Int]) { quickSortProcess(arr: &arr, left: 0, right: arr.count-1) } /// 快速排序遞迴方法 /// /// - Parameters: ///- arr: 大陣列 ///- left: 左邊界 ///- right: 右邊界 func quickSortProcess (arr:inout [Int] ,left:Int ,right:Int) { if left<right { //如果右側小於左側,說明陣列只有一個元素 let p = partition(arr: &arr, left: left, right: right) quickSortProcess(arr: &arr, left: left, right: p-1) quickSortProcess(arr: &arr, left: p+1, right: right) } }
需要注意的時再次劃分時,左側 (left~p-1)
與右側 (p+1~right)
已經將p位置排除在外了,因為其的位置已經確定。
-
經典快排的動圖
每次劃分都只確定最後一個元素的最終位置,重複進行直到整個陣列有序。

經典快排的改進
-
雙路快排
不管是當條件是大於等於還是小於等於v,當陣列中重複元素非常多的時候,等於v的元素太多,那麼就將陣列分成了極度不平衡的兩個部分,因為等於v的部分總是集中在陣列的某一邊,導致分割不均。
雙路快排當遇到重複元素的時候,也能近乎將他們平分開來。
簡而言之是前後兩個指標:
指標i,表示小於等於基準的區域。
指標j,表示大於等於基準的區域。
遍歷暫停的時機:
當i遇到大於等於基準的值時暫停,j遇到小於等於基準的時暫停。

此時交換arr[i]與arr[j],這是雙路快排最核心的思想。
- 若交換位置不處於連續重複元素區間。正好,將正確的元素放到了正確的位置。
- 若交換一段正好處於連續重複元素區間
交換後另一端被交換後還會繼續遍歷,直到下一個暫停的時機,此時原本連續的重複元素之間將會穿插很多其他的值。
舉個例子:
有一個綠色的藍色的4和一個綠色的4,與基準值相同

此時交換橙色位置的4,以及黃色位置的2。
然後橙色指標繼續向右移動,被卡在7的位置。
黃色指標也繼續向左移動,被卡在綠色4的位置。

繼續交換...

綠色的4和藍色的4已經被分散到陣列兩端。
這樣便保證不會出現由於經典快排中<=的邊界導致陣列劃分不均的情況了。
-
三路快排
經典快排值劃分的小於等於,大於區域,中間用一個基準值進行區分。
類似荷蘭國旗問題,我們可以將基準值以及與其相等的值劃分成一整個區域。
如此。每次將不止再確定一個值,而是幾個值的位置了。
具體操作上:
改進的地方在於,右側新增了一個指標,指向大於基準值區域的首位。
最終生成一個小於區域,大於區域,剩下的差值便是等於區域。

/// 快速排序 /// /// - Parameter arr: 陣列 func quickSort(arr:inout [Int]) { quickSortProcess(arr: &arr, left: 0, right: arr.count-1) } func quickSortProcess (arr:inout [Int] ,left:Int ,right:Int) { if left<right { let p = partition(arr: &arr, left: left, right: right) quickSortProcess(arr: &arr, left: left, right: p[0]-1) //右邊界為小於區域首位再向前一個 quickSortProcess(arr: &arr, left: p[1]+1, right: right)//左邊界為大於區域首位再向後一個 } } /// 三路快排 /// /// - Parameters: ///- arr: 陣列 ///- left: 左邊界 ///- right: 右邊界 /// - Returns: 陣列型別,[等於區域左邊界,等於區域右邊界] func partition(arr:inout [Int] ,left:Int ,right:Int) ->[Int] { var l = left - 1//小於區域,預設不在範圍內(上次的基準值) var r = right + 1//大於區域,預設不在範圍內(上次的基準值) var p = left//遍歷指標首位置 let num = arr[right]//目標值 while p < r { //注意這裡的r不是右邊界right,p==r則已經遍歷到大於區域了 if arr[p] < num { //小於目標值,將小於區域擴大並將該值交換進小於區域。 l+=1 arr.swapAt(p, l) p+=1 }else if arr[p] == num {//等於目標值,不動,繼續向下遍歷 p+=1 }else if arr[p] > num {//大於目標值,將大於區域擴大並將該值交換進大於區域。 r-=1 arr.swapAt(p, r) //需要注意這裡遍歷指標p沒有繼續右移,因為當前p位置已經交換成了待定區域的某個值。需要再次判定 } } //此時l為小於區域末尾,r為大於區域首部 //等於區域位於小於區域之後一位,到大於區域之前一位 return [l+1,r-1] }
基準值對快排時間複雜度的影響
快速排序法事應用最廣泛的排序演算法之一,最佳情況下時間複雜度是 O(nlogn)。但是最壞情況下(陣列本身已經有序的情況下),每次基準值都會處於陣列邊界處,時間複雜度將劣化到O(n^2)。
-
隨機快排
將基準值位置通過隨機數的方式獲取,將複雜度的表示式轉化為概率表示式。最終的表示式也會趨近於O(nlogn)。
這種方式與經典快排在隨機陣列的情況下相差無幾,甚至由於獲取隨機數的成本速度略低於經典快排。但在出現有序陣列的情況下,速度遠優於經典快排。
《快速排序與隨機化快排執行速度實驗比較》隨機快排應該是目前最流行的快速排序。
-
將中位數作為基準值
這種能將快排的時間複雜度確定在O(n^2),但是取中位數的過程究竟有多大的影響我也不確定。(目前我只會用堆來求,不妄加評論)。