参考链接
快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。
快速排序由于排序效率在同为O(N*logN)的几种排序方法中效率较高,因此经常被采用,再加上快速排序思想----分治法也确实实用
基本思想:
- 先从数列中取出一个数作为基准数。
- 分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
- 再对左右区间重复第二步,直到各区间只有一个数。
虽然快速排序称为分治法,但分治法这三个字显然无法很好的概括快速排序的全部步骤。因此我的对快速排序作了进一步的说明:挖坑填数+分治法:
先来看实例,以一个数组作为示例,取区间第一个数为基准数。
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 {
/**
## 基本思想:
1. 选择一个基准元素,通常选择第一个元素或者最后一个元素。
2. 通过一趟排序将待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小;另一部分记录的元素值均比基准值大。
3. 此时基准元素在其排好序后的正确位置
4. 然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序。
*/
// 将数组分割成独立的两部分
mutating func partition(lowIndex: Int, highIndex: Int) -> Int {
//基准元素
// let sentinel = middle(self[lowIndex], two: self[highIndex/2], three: self[highIndex])
let sentinel = self[lowIndex]
var low = lowIndex
var high = highIndex
// 从表的两端交替地向中间扫描
while low < high {
while low < high && self[high] >= sentinel {
high -= 1
}
// 从 high 所指位置向前搜索,至多到 low+1 位置。将比基准元素小的交换到前端
if low < high {
swap(&self[high], &self[low])
low += 1
}
while low < high && self[low] <= sentinel {
low += 1
}
// 从 low 所指位置向后搜索,至多到 high-1 位置。将比基准元素大的交换到后端
if low < high {
swap(&self[low], &self[high])
high -= 1
}
}
//print(self)
return low
}
mutating func quickSort(lowIndex: Int, highIndex: Int) {
if lowIndex < highIndex {
// 将表一分为二
let pivotIndex = partition(lowIndex, highIndex: highIndex)
// 递归对低子表递归排序
quickSort(lowIndex, highIndex: pivotIndex - 1)
// 递归对高子表递归排序
quickSort(pivotIndex + 1, highIndex: highIndex)
}
}
mutating func quickSort() {
if count < 2 {
return
}
quickSort(0, highIndex: count - 1)
}
}
let arr = ["6", "1", "5", "2", "3", "4"]
var demoArr = arr
demoArr.quickSort()
快速排序的改进
快速排序是通常被认为在同数量级(O(nlog2n))的排序方法中平均性能最好的。但若初始序列按关键码有序或基本有序时,快排序反而蜕化为冒泡排序。为改进之,通常以“三者取中法”来选取基准记录,即将排序区间的两个端点与中点三个记录关键码居中的调整为支点记录。快速排序是一个不稳定的排序方法。
在本改进算法中,只对长度大于k的子序列递归调用快速排序,让原序列基本有序,然后再对整个基本有序序列用插入排序算法排序。实践证明,改进后的算法时间复杂度有所降低,且当k取值为 8 左右时,改进算法的性能最佳。
mutating func quickSortImprove(lowIndex: Int, highIndex: Int, k: Int) {
// 长度大于k时递归,k为指定数
if highIndex - lowIndex > k {
// 将表一分为二
let pivotIndex = partition(lowIndex, highIndex: highIndex)
// 递归对低子表递归排序
quickSortImprove(lowIndex, highIndex: pivotIndex - 1, k: k)
// 递归对高子表递归排序
quickSortImprove(pivotIndex + 1, highIndex: highIndex, k: k)
}
}
mutating func quickSortBetter() {
if count < 2 {
return
}
// 先调用改进算法,使之基本有序
quickSortImprove(0, highIndex: count - 1, k: 8)
//实践证明,改进后的算法时间复杂度有所降低,且当k取值为 8 左右时,改进算法的性能最佳。
// 再用插入排序对基本有序序列排序
//每次取一个元素插入
for insert in 1 ..< count {
var current = insert
while current > 0{
let prev = current - 1
//如果插入元素小于前一个元素 就交换2者,直到找到合适插入位置
if self[current] < self[prev] {
swap(&self[current], &self[prev])
current -= 1
} else {
break
}
}
//print(self)
}
}
网友评论