什么是最快的排序算法
python-timsort C++-introsort Rust-pdqsort
Go(<=1.18)-introsort
Go1.19在某些场景中比之前算法快约10被
Go1.19排序算法是如何设计的?
生产环境中使用的排序算法和课本上的排序算法有什么区别
经典排序算法
插入排序
将元素不断插入已经排序好的Array中
Best:O(n) Avg:O(x^2) Worst:O(n^2)
快速排序
分治思想,不断分割序列直到序列整体有序
- 选定一个Pivot轴点
- 使用Pivot分割序列,分成元素比pivot大和元素比pivot小的两个序列
Best:O(nlogn) Avg:O(nlogn) Worst:O(n^2)
Heap Sort堆排序
利用堆的性质形成的排序算法
- 构成一个大顶堆
- 将根节点(最大节点)交换到最后一个位置,调整整个堆,如此反复。
Best:O(n*logn) Avg:O(n*logn) Worst:O(n*logn)
Benchmark
根据序列元素排序情况划分
- 完全随机的情况
- 有序/逆序的情况
- 元素重复度较高的情况
- 在此基础上,还需要根据序列长度的划分
random场景:
- 短序列插入排序较快
- 中序列快速排序比较快
- 长序列快速排序比较快
sorted场景
- 短序列插入排序较快
- 中序列插入排序较快
- 长序列插入排序较快
所有短序列和元素有序情况下,插入排序性能最好 在大部分情况下,快速排序有较好的综合性能 在任何情况下,堆排序的表现都比较稳定
从零打造pdqsort
是一种不稳定的混合排序算法,它的不同版本被应用到C++ Boost Rust Go1.19 对常见的序列做了进一步优化。
version1:
结合三种排算法的优点:
- 对于短序列(小于一定的长度),使用插入排序
- 其他情况,使用快速排序来保证整体性能
- 当快速排序表现不佳时,使用堆排序来保证最坏情况下时间复杂度仍然为O(n*logn)
Q&A 短序列的具体长度是多少?12-32,在不同的语言场景中,在泛型版本根据测试选定24
如何得知快速排序表现不佳,以及何时切换到堆排序?当最终pivot的位置离序列两端很接近时,距离小于length/8,判定其表现不佳,当这种情况的次数到达limit(bits.Len(Length))时,切换到堆排序。
如何让pdqsort速度更快?
- 尽量使得QuickSort的pivot为序列的中位数->改进choose pivot
- Partition速度更快->改进partition,但是此优化在Go表现不好
关于pivot的选择,需要平衡寻找pivot所需要的开销和pivot带来的性能优化
version2
根据序列长度的不同,来决定选择策略
优化pivot选择:
- 短序列<=8,选择固定元素
- 中序列<=50,采样三个元素,median of tree
- 长序列>50,采样9个元素,median of medians
pivot的采样方式使得我们有探知序列当前状态的能力
- 采样的元素都是逆序排列->序列可能已经逆序->反转整个序列
- 采样的元素都是顺序排寻->序列可能已经有序->使用插入排序
- (插入排序实际使用partiallnsertionSort,即有限次数的插入排序)
优化总结:
- 升级pivot的选择策略(近似中位数)
- 发现序列可能逆序,则反转序列,应对reverse场景
- 发现序列可能有序,使用有限插入排序,应对sorted场景
还有什么场景没有优化?
如何优化重复元素很多的情况
- 采样pivot的时候检测重复度?
- 不是很好,因为采样数量有限,不一定能采样到相同元素
- 解决方案:如果两次partition生成的pivot相同,即partition进行了无效分割,此时认为pivot的值为重复元素
final version
当检测到此时的pivot和上次相同时(发生在LeftSubArray),使用partiitonEqual将重复元素排列在一起,减少重复元素对于pivot选择的干扰
当pivot选择策略表现不佳时,随机交换元素。 避免一些极端情况是的Quicksort总是表现不佳,以及一些黑客攻击情况