08 pdqsort | 青训营笔记

179 阅读2分钟

pdqsort 是什么

pdqsort(pattern-defeating-quicksort)是一种不稳定的混合排序算法,它的不同版本被应用在C++ BOOST、Rust以及Go 1.19中。它对常见的序列类型做了特殊的优化,使得在不同条件下都拥有不错的性能。

pdqsort 实现 version1

结合三种排序方法的优点

  • 对于短序列(小于一定长度)使用插入排序
  • 其他情况,使用快速排序来保证整体性能
  • 当快速排序表现不佳时,使用堆排序来保证最坏情况下时间复杂度仍然为O(n*logn)

产生的问题

  1. 短序列的具体长度是多少
    • 12~32,不同语言和场景中会有不同,在泛型版本根据测试选定24
  2. 如何知道快速排序的表现会不佳
    • 最终pivot的位置离序列两端很接近时(距离小于length/8)判定其表现不佳【也可以理解为每次分的太少】
  3. 什么时候切换到堆排序
    • 当快排表现不佳的次数达到limit(即bits.Len(length))时,切换到堆排序

image.png

流程解释

  • 对于短序列(<= 24) 使用插入排序
  • 其他情况,先使用快速排序(选择首个元素作为pivot)来保证整体性能,并设定初始的limit
  • 当快速排序表现不佳时(limit一直减,减到0),使用堆排序来保证最坏情况下时间复杂度仍然为O(n*logn)

pdqsort 实现 version2

version1的缺点

  • pivot选择可以优化。
  • 使用首个元素作为pivot,实现简单,但是往往效果不好,例如在sorted情况下性能很差。每次就确定一个

如何选择pivot

  • 除了上面的用首个元素外,还可以遍历数组,寻找真正的中位数,但是遍历比对的代价很高,性能不好
  • 选择pivot的标准是寻找pivot所需要的开销和pivot带来的性能优化达到一个平衡。
  • 由此可得,可以尝试找近似的中位数

优化后的pivot选择方式

  • 短序列(<= 8),选择固定元素(其实使用插入排序)
  • 中序列(<= 50),采样三个元素,取三个元素的中间元素作为pivot
  • 长序列(> 50),采样九个元素,取九个元素的中间元素。

pivot的采样方式使得我们有探知序列当前状态的能力

image.png

image.png

流程解释

  • 升级pivot选择策略(近似中位数)
  • 发现序列可能逆序,则翻转序列
  • 发现序列可能有序,则使用有限插入排序

pdqsort 实现 final version

如何优化重复元素很多的情况

  • 采样pivot检测重复度
    • 不是很好,因为采样数量有限,不一定能采样到相同元素
    • 解决方案:如果两次partition生成的pivot相同,即partition进行了无效分割,此时认为pivot的值为重复元素。

优化

  • 重复元素较多的情况
    • 当检测到此时的pivot和上次相同时(发生在leftSubArray),使用partitionEqual将重复元素排列在一起,减少重复元素对于pivot选择的干扰
  • 当pivot选择策略表现不佳时,随机交换元素
    • 避免一些极端情况使得quicksort总是表现不佳,以及一些黑客攻击情况

image.png

  • 黄色表示可能发生可能不发生