pdqsort 是什么
pdqsort(pattern-defeating-quicksort)是一种不稳定的混合排序算法,它的不同版本被应用在C++ BOOST、Rust以及Go 1.19中。它对常见的序列类型做了特殊的优化,使得在不同条件下都拥有不错的性能。
pdqsort 实现 version1
结合三种排序方法的优点
- 对于短序列(小于一定长度)使用插入排序
- 其他情况,使用快速排序来保证整体性能
- 当快速排序表现不佳时,使用堆排序来保证最坏情况下时间复杂度仍然为O(n*logn)
产生的问题
- 短序列的具体长度是多少
- 12~32,不同语言和场景中会有不同,在泛型版本根据测试选定24
- 如何知道快速排序的表现会不佳
- 当最终pivot的位置离序列两端很接近时(距离小于length/8)判定其表现不佳【也可以理解为每次分的太少】
- 什么时候切换到堆排序
- 当快排表现不佳的次数达到limit(即bits.Len(length))时,切换到堆排序
流程解释
- 对于短序列(<= 24) 使用插入排序
- 其他情况,先使用快速排序(选择首个元素作为pivot)来保证整体性能,并设定初始的limit
- 当快速排序表现不佳时(limit一直减,减到0),使用堆排序来保证最坏情况下时间复杂度仍然为O(n*logn)
pdqsort 实现 version2
version1的缺点
- pivot选择可以优化。
- 使用首个元素作为pivot,实现简单,但是往往效果不好,例如在sorted情况下性能很差。每次就确定一个
如何选择pivot
- 除了上面的用首个元素外,还可以遍历数组,寻找真正的中位数,但是遍历比对的代价很高,性能不好
- 选择pivot的标准是寻找pivot所需要的开销和pivot带来的性能优化达到一个平衡。
- 由此可得,可以尝试找近似的中位数
优化后的pivot选择方式
- 短序列(<= 8),选择固定元素(其实使用插入排序)
- 中序列(<= 50),采样三个元素,取三个元素的中间元素作为pivot
- 长序列(> 50),采样九个元素,取九个元素的中间元素。
pivot的采样方式使得我们有探知序列当前状态的能力
流程解释
- 升级pivot选择策略(近似中位数)
- 发现序列可能逆序,则翻转序列
- 发现序列可能有序,则使用有限插入排序
pdqsort 实现 final version
如何优化重复元素很多的情况
- 采样pivot检测重复度
- 不是很好,因为采样数量有限,不一定能采样到相同元素
- 解决方案:如果两次partition生成的pivot相同,即partition进行了无效分割,此时认为pivot的值为重复元素。
优化
- 重复元素较多的情况
- 当检测到此时的pivot和上次相同时(发生在leftSubArray),使用partitionEqual将重复元素排列在一起,减少重复元素对于pivot选择的干扰
- 当pivot选择策略表现不佳时,随机交换元素
- 避免一些极端情况使得quicksort总是表现不佳,以及一些黑客攻击情况
- 黄色表示可能发生可能不发生