排序算法课堂笔记|青训营笔记

124 阅读3分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第2篇笔记

几种算法的 Benchmark

元素排列情况划分

  • 完全随机 random

  • 有序/逆序 sorted/reverse

  • 元素重复度较高的情况 mod8

    • mod8:就是把每一个元素都 mod8

在此基础上还需要根据序列长度进行划分(短16/中128/长1024)

实际表现

随机情况

短序列:插入 > 堆排 ≥ 快排

中序列:快排 > 堆排 > 插入

长序列:快排 > 堆排 > 插入

有序情况

序列:插入 > 堆排 > 快排

结论

在所有短序列和有序情况下,插入排序性能最好

在大部分情况下,快速排序有较好的综合性能

几乎在任何情况下,堆排序的表现都比较稳定

从零开始打造 pdqsort

pdqsort(pattern-defeating-quicksort)

  • 不稳定
  • 混合
  • 对常见序列类型做了特殊优化
  • 应用在 C++ BOOST、Rust、Go 1.19 中

version1

  • 对短序列使用插排

  • 其他情况使用快排

  • 快排表现不佳时使用堆排,保证最坏情况下时间复杂度仍为 O(n*logn)

  • 何为短序列?

    12 ~ 32,在不同语言和场景中有所不同,在泛型版本根据测试选定为 24

  • 如何得知快排表现不佳?

    当最终 pivot 的位置离两端很接近(距离小于 length/8 时)判定为表现不佳,当这种情况的次数达到 limit(即 bits.Len(length),长度所需的比特位)时,切换到堆排

  • 如何使 pdqsort 速度更快?

    • 改进 pivot 的选择,使其更接近中位数
    • 改进 partition,使其速度更快

version2

优化 pivot 的选择

需要平衡寻找 pivot 的开销和 pivot 带来的性能优化

最终解决方案:寻找近似的中位数

  • 短序列(≤8),选择固定元素
  • 中序列(≤50),采样 3 个元素,median of three
  • 长序列(>50),采样 9 个元素,median of medians

pivot 的采样方式也提供了探知元素当前状态的能力

  • 采样的元素都是逆序 → 序列可能已经逆序 → 翻转整个序列

  • 采样的元素都是顺序 → 排序可能已经有序 → 使用插入排序

    • 插入排序实际使用 partialInsertionSort,即有限制次数的插入排序(如果插入多次序列还未变成有序则放弃使用)

final version

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

  • 采样 pivot 的时候检测重复度?效果不好,因为采样数量有限,不一定能采样到相同元素

解决方案:如果两次 partition 生成的 pivot 相同,即 partition 进行了无效分割,此时认为 pivot 的值为重复元素

  • 优化:重复元素较多的情况

    当检测到 pivot 和上次相同时(发生在 leftSubArray),使用 **partitionEqual(一种特殊算法)**将相同元素排在一起,减少重复元素对于 pivot 选择的干扰

  • 当 pivot 选择策略表现不佳时,随机交换元素

    避免一些极端情况,和黑客攻击的情况,导致快排总是表现不佳