本文只是自己的筆記,並不具備過多的指導意義。
爲了理解很多都使用了遞歸,而不是自己通過while進行壓棧處理。
代碼的初衷是便於理解,網上大神優化過的代碼很多,也不建議在項目中copy本文代碼。
目錄
- 快速排序的基本思想
-
單次遍歷,確定一個值在數組中的最終位置
- 將小於等於給定值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),但是取中位數的過程究竟有多大的影響我也不確定。(目前我只會用堆來求,不妄加評論)。
參考資料
左神牛課網算法課
雙路快速排序法
經典排序之快排及其優化
快速排序與隨機化快排運行速度實驗比較
快速排序 改進快排的方法
十大經典排序算法(動圖演示)