pdqSort 算法
经典排序算法对比
算法 | 最好时间复杂度 | 平均时间复杂度 | 最坏时间复杂度 |
---|---|---|---|
插入排序 | |||
快速排序 | |||
堆排序 |
可以看出其中插入排序可以在最好时间复杂度达到理论上的最快速排序,堆排序无论在任何情况下的时间复杂度都是一样的,是一个非常稳定的排序算法。
实际应用场景的表现
根据实际应用场景,设计以下三种元素排列情况
- 序列完全随机
- 序列呈现有序/逆序情况
- 元素中重复元素多
在此基础上,再划分出 16 / 128 / 1024 三种序列长度。
Benchmark - random
- 可以看出在短序列的情况下,插入排序的速度是最快的,中序列和长序列情况下,快排的速度是最快的,堆排序一直保持和快排差不多的速度。
Benchmark - sorted
- 可以看出在原数组已经有序的情况下,插入排序在短、中、长三种序列下都是最快的。且远远快于其他两种排序。
总结
- 在短序列和元素初始呈现有序的情况下,插入排序性能最好
- 大部分情况下,快速排序耗时较少
- 堆排序的耗时很稳定。
于是思考,能否设计一种算法结合以上三种算法的优点,使得该算法在 最好的情况下达到插入排序的性能,在 最坏情况下能保持堆排序的稳定性。
pdqsort 算法
pdqSort是(pattern - defeating - quicksort)的简称,是一种不稳定的 混合 排序算法,它对常见的序列类型做了特殊的优化,会根据排序的序列类型自动选择合适的排序算法,使得在不同条件下都拥有不错的性能。
version1
结合三种排序方法的优点:
- 对于短序列(经过测试认为小于 为短序列)使用 插入排序
- 对于其他情况,使用 快速排序。这里默认快排中选择的基准点为序列中的第一个元素。
- 当快速排序表现不佳的时候,使用 堆排序 来保证最坏情况下,时间复杂度仍然为
当最终
pivot
所在的位置离序列两端很近(小于length / 8
)的时候,判定快排表现不佳,而当表现不佳的次数达到limit
,即bits.Len(length)
时,切换到堆排序。程序中首先设置limit
的初值,每次判定不佳后让limit --
, 当limit==0
切换到堆排序。
优化方向: 快排中 pivot
的选择很重要,尽量让 privot
分割出的左右子序列能平衡。
- 随机选择一个数作为
privot
: 生成随机数耗时,且不一定能产生很好的效果 - 选择中位数:遍历数组寻找中位数的过程耗时巨大。
version2
根据序列的长度进行 pivot
的采样!
- 短序列( ),选择固定元素
- 中序列( ),采样三个元素
- 长序列,采样九个元素
根据采样的元素,选择采样的中位数来作为 pivot
,这样选出的 pivot
是近似数组中位数,且不会在寻找的过程中消耗太多时间。除此之外,采样可以探知序列当前的状态。
-
采样的元素都是逆序排列 -> 序列可能已经逆序 -> 翻转整个序列
-
采样的元素都是顺序排列 -> 序列可能已经有序 -> 使用插入排序
final version
优化方向: 当序列中重复元素较多的情况,则两次选取的 pivot
有可能是同样的一个数,那么快排中就会进行无效的左右子序列分割。
当检测到此时 pivot
和上一次相同时,使用 partitionEqual
将重复元素排在一起,减少重复元素对于 pivot
选择的干扰。
优化方向: 当随机采样选择出的 pivot
仍然使得快排表现不佳
此时随机交换序列中的一些元素,避免极端情况以及一些黑客的攻击。
经过测试,总体下来,pdqsort算法比原来的算法有着 的提升
课后作业 - pdqsort算法version1的实现
// 快排
func QuickSort(arr []int, l, r int, limit int) int {
fmt.Println("进入快速排序")
arrLen := len(arr)
if r <= l {
return limit
}
pivot := arr[l]
if limit == 0 {
return 0
}
i, j := l - 1, r + 1
for i < j {
for {
i ++
if arr[i] >= pivot {
break
}
}
for {
j --
if arr[j] <= pivot {
break
}
}
leftArrLen, rightArrLen := len(arr[:j]), len(arr[j + 1:])
//进行快排表现优劣的评估
if leftArrLen < arrLen / 8 || rightArrLen < arrLen / 8 {
limit --
fmt.Println("l:", leftArrLen)
fmt.Println("r:", rightArrLen)
}
if i < j {
arr[i], arr[j] = arr[j], arr[i]
}
}
limit = QuickSort(arr, l, j, limit)
limit = QuickSort(arr, j + 1, r, limit)
return limit
}
// 堆排序
func HeapSort(arr[] int) {
fmt.Println("进入堆排序")
Heapify(arr, len(arr))
for i := len(arr) - 1; i > 0; i -- {
arr[0], arr[i] = arr[i], arr[0]
Heapify(arr, i)
}
}
func Heapify(arr []int, heapSize int) {
for i := len(arr) / 2 - 1; i >= 0; i -- {
leftIndex := 2 * i + 1
if leftIndex < heapSize && arr[i] < arr[leftIndex] {
arr[i], arr[leftIndex] = arr[leftIndex], arr[i]
}
rightIndex := 2 * i + 2
if rightIndex < heapSize && arr[i] < arr[rightIndex] {
arr[i], arr[rightIndex] = arr[rightIndex], arr[i]
}
}
}
// 插入排序
func InsertionSort(arr []int) {
fmt.Println("进入插入排序排序")
for i := 1; i < len(arr); i ++{
num:= arr[i]
j := i - 1
for j >= 0 && arr[j] > num {
arr[j + 1] = arr[j]
j --
}
arr[j + 1] = num
}
}
func pqdSort(arr []int) {
limit := bits.Len(uint(len(arr)))
var maxInsertion = 24 // 短序列判断标准
for {
arrLength := len(arr)
if arrLength <= maxInsertion {
InsertionSort(arr)
return
}
limit = QuickSort(arr, 0, len(arr) - 1, limit)
// fmt.Println("limt", limit)
// 快排表现不佳则使用堆排序
if limit == 0 {
HeapSort(arr)
return
} else {
return
}
}