参考链接
快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。
快速排序由于排序效率在同为O(N*logN)的几种排序方法中效率较高,因此经常被采用,再加上快速排序思想----分治法也确实实用
基本思想:
从数组中选择一个元素,称之为中轴元素(pivot),然后把数组中所有小于中轴元素的元素放在其左边,所有大于或等于中轴元素的元素放在其右边,显然,此时中轴元素所处的位置的是有序的。也就是说,我们无需再移动中轴元素的位置。
从中轴元素那里开始把大的数组切割成两个小的数组(两个数组都不包含中轴元素),接着我们通过递归的方式,让中轴元素左边的数组和右边的数组也重复同样的操作,直到数组的大小为1,此时每个元素都处于有序的位置。
2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3.再对左右区间重复第二步,直到各区间只有一个数。
虽然快速排序称为分治法,但分治法这三个字显然无法很好的概括快速排序的全部步骤。因此我的对快速排序作了进一步的说明:挖坑填数+分治法:
以一个数组作为示例,取区间第一个数为基准数。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
72 | 6 | 57 | 88 | 60 | 42 | 83 | 73 | 48 | 85 |
初始时,i = 0; j = 9; X = a[i] = 72
由于已经将a[0]中的数保存到X中,可以理解成在数组a[0]上挖了个坑,可以将其它数据填充到这来。
从j开始向前找一个比X小或等于X的数。当j=8,符合条件,将a[8]挖出再填到上一个坑a[0]中。
a[0]=a[8]; i++; 这样一个坑a[0]就被搞定了,但又形成了一个新坑a[8],这怎么办了?
简单,再找数字来填a[8]这个坑。这次从i开始向后找一个大于X的数,当i=3,符合条件,将a[3]挖出再填到上一个坑中a[8]=a[3]; j--;
数组变为:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
48 | 6 | 57 | 88 | 60 | 42 | 83 | 73 | 88 | 85 |
i = 3; j = 7; X=72
再重复上面的步骤,先从后向前找,再从前向后找。
从j开始向前找,当j=5,符合条件,将a[5]挖出填到上一个坑中,a[3] = a[5]; i++;
从i开始向后找,当i=5时,由于i==j退出。
此时,i = j = 5,而a[5]刚好又是上次挖的坑,因此将X填入a[5]。
数组变为:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
48 | 6 | 57 | 42 | 60 | 72 | 83 | 73 | 88 | 85 |
可以看出a[5]前面的数字都小于它,a[5]后面的数字都大于它。因此再对a[0…4]和a[6…9]这二个子区间重复上述步骤就可以了。
对挖坑填数进行总结
1.i =L; j = R; 将基准数挖出形成第一个坑a[i]。
2.j--由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。
3.i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。
4.再重复执行2,3二步,直到i==j,将基准数填入a[i]中。
算法的实现:
extension Array where Element: Comparable {
mutating func quickSort() {
func qsort(from left: Int, to right: Int) {
guard left < right else {
return
}
/// 取左中右,中间的值作为pivot,结果效果不大
// let mid = (left + right) / 2
// /// 保证self[left] <= self[mid] <= self[right],即mid是pivot
// if self[left] > self[mid] { swapAt(left, mid) }
// if self[left] > self[right] { swapAt(left, right) }
// if self[mid] > self[right] { swapAt(mid, right) }
// swapAt(mid, left)
/// left为pivot
let pivot = self[left]
var r = right
var l = left
while l < r {
while l < r, self[r] >= pivot {
r -= 1
}
self[l] = self[r]
while l < r, self[l] <= pivot {
l += 1
}
self[r] = self[l]
}
self[l] = pivot
qsort(from: left, to: l - 1)
qsort(from: l + 1, to: right)
}
qsort(from: 0, to: count - 1)
}
}
快速排序的改进
快速排序是通常被认为在同数量级(O(nlog2n))的排序方法中平均性能最好的。但若初始序列按关键码有序或基本有序时,快排序反而蜕化为冒泡排序。为改进之,通常以“三者取中法”来选取基准记录,即将排序区间的两个端点与中点三个记录关键码居中的调整为支点记录。快速排序是一个不稳定的排序方法。
在本改进算法中,只对长度大于k的子序列递归调用快速排序,让原序列基本有序,然后再对整个基本有序序列用插入排序算法排序。实践证明,改进后的算法时间复杂度有所降低,且当k取值为 8 左右时,改进算法的性能最佳,而且相对稳定一些。
extension Array where Element: Comparable {
mutating func quickSortBetter() {
func qsort(from left: Int, to right: Int) {
guard right - left > 7 else {
return
}
/// left为pivot
let pivot = self[left]
var r = right
var l = left
while l != r {
while l < r, self[r] >= pivot {
r -= 1
}
if l < r {
self[l] = self[r]
l += 1
} else {
break
}
while l < r, self[l] <= pivot {
l += 1
}
if l < r {
self[r] = self[l]
r -= 1
}
}
self[l] = pivot
qsort(from: left, to: l - 1)
qsort(from: l + 1, to: right)
}
/// 先调用改进算法,使之基本有序
qsort(from: 0, to: count - 1)
/// 再调用之前的插入排序
insertSort()
}
}