这是我参与「第三届青训营 -后端场」笔记创作活动的第2篇笔记
几种算法的 Benchmark
元素排列情况划分
-
完全随机 random
-
有序/逆序 sorted/reverse
-
元素重复度较高的情况 mod8
- mod8:就是把每一个元素都 mod8
在此基础上还需要根据序列长度进行划分(短16/中128/长1024)
实际表现
随机情况
短序列:插入 > 堆排 ≥ 快排
中序列:快排 > 堆排 > 插入
长序列:快排 > 堆排 > 插入
有序情况
序列:插入 > 堆排 > 快排
结论
在所有短序列和有序情况下,插入排序性能最好
在大部分情况下,快速排序有较好的综合性能
几乎在任何情况下,堆排序的表现都比较稳定
从零开始打造 pdqsort
pdqsort(pattern-defeating-quicksort)
- 不稳定
- 混合
- 对常见序列类型做了特殊优化
- 应用在 C++ BOOST、Rust、Go 1.19 中
version1
-
对短序列使用插排
-
其他情况使用快排
-
快排表现不佳时使用堆排,保证最坏情况下时间复杂度仍为 O(n*logn)
-
何为短序列?
12 ~ 32,在不同语言和场景中有所不同,在泛型版本根据测试选定为 24
-
如何得知快排表现不佳?
当最终 pivot 的位置离两端很接近(距离小于 length/8 时)判定为表现不佳,当这种情况的次数达到 limit(即 bits.Len(length),长度所需的比特位)时,切换到堆排
-
如何使 pdqsort 速度更快?
- 改进 pivot 的选择,使其更接近中位数
- 改进 partition,使其速度更快
version2
优化 pivot 的选择
需要平衡寻找 pivot 的开销和 pivot 带来的性能优化
最终解决方案:寻找近似的中位数
- 短序列(≤8),选择固定元素
- 中序列(≤50),采样 3 个元素,median of three
- 长序列(>50),采样 9 个元素,median of medians
pivot 的采样方式也提供了探知元素当前状态的能力
-
采样的元素都是逆序 → 序列可能已经逆序 → 翻转整个序列
-
采样的元素都是顺序 → 排序可能已经有序 → 使用插入排序
- 插入排序实际使用 partialInsertionSort,即有限制次数的插入排序(如果插入多次序列还未变成有序则放弃使用)
final version
如何优化重复元素很多的情况?
- 采样 pivot 的时候检测重复度?效果不好,因为采样数量有限,不一定能采样到相同元素
解决方案:如果两次 partition 生成的 pivot 相同,即 partition 进行了无效分割,此时认为 pivot 的值为重复元素
-
优化:重复元素较多的情况
当检测到 pivot 和上次相同时(发生在 leftSubArray),使用 **partitionEqual(一种特殊算法)**将相同元素排在一起,减少重复元素对于 pivot 选择的干扰
-
当 pivot 选择策略表现不佳时,随机交换元素
避免一些极端情况,和黑客攻击的情况,导致快排总是表现不佳