排序算法丨青训营笔记

146 阅读5分钟

各个算法解析

选择排序

  选择排序算法是不稳定的,它是先遍历一次数组,找到一个最小值,和已排好序的边界后一个元素交换。这个交换的步骤就让选择排序变得不稳定。如果交换过去的数,在交换的过程中有相同的值,就会不稳定。

冒泡排序

冒泡排序是稳定的,因为它不涉及大范围交换,当然得有一个注意的地方,就是当不断比较的时候,当发现相等的时候不能交换。也就是说他可以不稳定也可以稳定,但是何必让它不稳定呢?只不过是相等的时候不交换罢了

插入排序

插入排序是稳定的,就像冒泡排序一样,只要在比较到想的的时候不要交换就行。我们何必要它不稳定呢?

归并排序

归并排序是稳定的,它有点类似上面两个排序,在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总是表现不佳,以及一些黑客攻击情况