快排思想
快速排序算法(Quicksort),我们习惯性把它简称为“快排”。快排利用的也是分治思想
。乍看起来,它有点像归并排序,但是思路其实完全不一样。
快排的思想是这样的:如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r 之间的任意一个数据作为 pivot(分区点),通常是区间的最后一个数据 - array[r]。我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。
根据分治、递归的处理思想,我们可以用递归排序下标从 p 到 q-1 之间的数据和下标从 q+1 到 r 之间的数据,直到区间缩小为 1,就说明所有的数据都有序了。
原地排序
原地快排的核心是 原地分区函数
,此函数的实现思路非常巧妙
这里的处理有点类似选择排序。我们通过游标 i 把 array[p…r-1] 分成两部分。array[p…i-1] 的元素都是小于 pivot 的,我们暂且叫它 已处理区间
,array[i…r-1] 是 未处理区间
。我们每次都从未处理的区间 array[i…r-1] 中取一个元素 array[j],与 pivot 对比,如果小于 pivot,则将其加入到已处理区间的尾部,也就是 array[i]的位置。
怎样实现 “加入到已处理区间的尾部” ?
- 通过交换 array[j] 与 array[i],并把 i += 1 来实现
- 只需要将 array[i] 与 array[j] 交换,就可以在 O(1) 时间复杂度内将 array[j] 放到下标为 i 的位置。
原地排序, 分界点选取最后一个元素
/// 原地分区函数(array: 待分区数组, p: 起始下标, r: 结束下标)
private func partition(_ array: inout [Int], _ p: Int, _ r: Int) -> Int {
let pivot = array[r]
var i = p
for j in p...r-1 {
if array[j] < pivot {
// 交换数组下标为 i、j 的元素
array.swapAt(i, j)
i += 1
}
}
array.swapAt(i, r)
// 返回的是分区点的下标,遍于调用者继续递归排序
return i
}
根据上面的 partition 实现最终的快排
/// 快速排序算法
public func qucikSort(_ array: inout [Int], _ p: Int, _ r: Int) -> [Int] {
// 需要排序的区间只包含一个数字,则不需要重排数组,直接返回
if p >= r { return array }
let i = partition(&array, p, r)
qucikSort(&array, p, i-1)
qucikSort(&array, i+1, r)
return array
}
拓展
原地排序, 分界点选取第一个元素
/// 原地分区函数(array: 待分区数组, p: 起始下标, r: 结束下标)
private func partition(_ array: inout [Int], _ p: Int, _ r: Int) -> Int {
let pivot = array[p]
var i = p, j = r
while i != j {
// 先从右向左找小于pivot的数(order is important)
while array[j] >= pivot && i < j {
j -= 1
}
// 再从左往右找大于pivot的数
while array[i] <= pivot && i < j {
i += 1
}
if i < j {
array.swapAt(i, j);
}
}
// 找到分界点在数组中的位置, 并与"雀占鸠巢"者交换
array.swapAt(p, i);
return i
}