各个算法解析
选择排序
选择排序算法是不稳定的,它是先遍历一次数组,找到一个最小值,和已排好序的边界后一个元素交换。这个交换的步骤就让选择排序变得不稳定。如果交换过去的数,在交换的过程中有相同的值,就会不稳定。
冒泡排序
冒泡排序是稳定的,因为它不涉及大范围交换,当然得有一个注意的地方,就是当不断比较的时候,当发现相等的时候不能交换。也就是说他可以不稳定也可以稳定,但是何必让它不稳定呢?只不过是相等的时候不交换罢了
插入排序
插入排序是稳定的,就像冒泡排序一样,只要在比较到想的的时候不要交换就行。我们何必要它不稳定呢?
归并排序
归并排序是稳定的,它有点类似上面两个排序,在merge的时候,也就是归并的时候,两个部分,当发现相等的时候,先让左边的赋值到辅助空间就行,要注意的是,小和问题不是稳定的,它是先让后面部分进行赋值。
快速排序
快速排序不是稳定,关键在partition的过程,也就是左右移动找到区分值的时候,一旦找到就得进行大范围的交换,一旦交换的距离中有相等的数值,就会出现稳定性不稳定的问题
堆排序
堆排序显然不可能是稳定的,甚至还没有排序,在建立堆的过程中就不稳定了,他将实现大范围的交换,并且还是跳跃式的,一旦中间有相等值,就会出现稳定性不稳定的问题
桶排序思想的算法
稳定,所有桶排序的算法都是稳定的,因为它本身定义了遍历顺序,还有桶本身是有稳定性问题的
经典排序算法
插入排序
将元素不断插入已经排序好的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(nlogn) Avg:O(nlogn) 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总是表现不佳,以及一些黑客攻击情况