这是我参与「第三届青训营 -后端场」笔记创作活动的第3篇笔记
经典排序算法
插入排序
将元素不断插入已经排好序的array中
| 时间复杂度 | ||
|---|---|---|
| 最好情况 | 平均情况 | 最坏情况 |
| O(n) | O(n^2) | O(n^2) |
快速排序
分治思想,不断分割序列直到序列整体有序
- 选定一个pivot(轴点)
- 使用pivot分割序列,分成元素比pivot大和元素比pivot小的两个序列 | | 时间复杂度 | | | --- | --- |---| | 最好情况 |平均情况|最坏情况 | | O(nlogn) |O(nlogn)|O(n^2) |
堆排序
利用堆的性质形成的排序算法
- 构造一个大顶堆
- 将根节点(最大元素)交换到最后一个位置,调整整个堆,如此反复 | | 时间复杂度 | | | --- | --- |---| | 最好情况 |平均情况|最坏情况 | | O(nlogn) |O(nlogn)|O(n*logn) |
三种算法的总结
- 插入排序平均和最坏情况时间复杂度都是O(n^2),性能不好
- 快速排序整体性能处于中间层次
- 堆排序性能稳定,“众生平等”
实际场景
根据序列元素排序情况划分
- 完全随机的情况(random)
- 有序/逆序的情况(sorted/reverse)
- 元素重复度较高的情况(mod8) 在此基础上还需根据序列长度划分(16/128/1024)
实际场景benchmark结论
- 所有短序列和元素有序情况下,插入排序性能最好
- 在大部分的情况下,快速排序有较好的综合性能
- 几乎在任何情况下,堆排序的表现都比较稳定
pdqsort(pattern-defeating-quicksort)
是一种不稳定的混合排序算法 不同版本被应用在C++ BOOST、Rust以及Go 1.19中,对常见的序列类型做了特殊的优化,使得在不同条件下都拥有不错的性能
pdqsort - version1
- 对于短序列(在泛型版本根据测试选定24),我们使用插入排序
- 其他情况,使用快速排序来保证整体性能
- 当快排表现不佳时,使用堆排序来保证最坏情况下时间复杂度仍然为O(n*logn) 表现不佳:当最终pivot的位置离序列两端很接近时(距离小于length/8)判定其表现不佳,当这种情况次数达到limit(bits.Len(length))时,切换到堆排序
pdqsort - version2
优化了Pivot的选择
- 短序列(<=8),选择固定元素
- 中序列(<=50),采样三个元素,选其中的中位数
- 长序列(>50),采样九个元素,选其中的中位数
同时Pivot的采样方式也使得我们有探知序列当前状态的能力
- 采样的元素是逆序的,则序列可能已经逆序,翻转整个序列
- 采样的元素是顺序的,则序列可能已经有序,使用插入排序 (注:插入排序实际使用partialinsertionSort,即有限制次数的插入排序)
pdqsort - final version
优化了重复元素很多的情况
解决方案:如果两次partition生成的pivot相同,即partition进行了无效分割,此时认为pivot的值为重复元素
优化1:当检测到此时的pivot和上次相同,使用partitionEqual将重复元素排列在一起,减少重复元素对于pivot选择的干扰
优化2:当pivot选择策略不佳时,随机交换元素
| 时间复杂度 | ||
|---|---|---|
| 最好情况 | 平均情况 | 最坏情况 |
| O(n) | O(n*logn) | O(n*logn) |