排序 | 青训营笔记

76 阅读4分钟

什么是最快的排序算法

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总是表现不佳,以及一些黑客攻击情况

image.png