如何使得快速排序性能更优? | 青训营笔记

215 阅读2分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第2篇笔记。在学校期间,数据结构与算法课程少不了对排序算法的介绍,例如冒泡排序、选择排序、插入排序、快速排序、堆排序以及基数排序等等。 那么现在的编程语言的标准库中的排序算法是怎么样的呢?答案是一种自适应的快速排序算法。 学习了抖音青训营的数据结构与算法课程后,来学习学习pdqsort(pattern-defeating-quicksort)。

  • 在所有基准测试中,pdqsort 从未明显慢于以前的算法
  • 在常见模式中,pdqsort 通常更快(即在排序切片中快 10 倍)

下面来逐一介绍其原理:

  1. v1版本:结合三种排序方法的优点:
  • 对于短序列(小于一定的长度)我们使用插入排序

  • 其他情况,使用快速排序来保证整体性能

  • 当快速排序表现不佳时,使用堆排序来保证最坏情况下时间复杂度为O(nlogn)

  • 短序列的具体长度是多少呢? 12~32, 在不同语言和场景会不同,默认为24.

  • 如何得知快速排序表现不佳,以及何时切换到堆排序?

    当pivot的位置离序列两端很接近时(距离小于length/8) 判定其表现不佳,当这种情况的次数达到limit(即bits.Len(length))时, 切换到堆排序

下图展示了v1 的整个过程:

image.png

如何继续优化? 思考关于privot的选择

  • 使用首个元素作为pivot(最简单)

    实现简单,但是往往效果不好,例如在sorted情况下 性能很差

  • 遍历数组,寻找真正的中位数

    遍历代价很高,性能不好

于是寻找近似中位数: 据序列长度的不同,来决定选择策略

  • 短序列 (<=8) , 选择固定元素
  • 中序列 (<=50), 采样三个元素,选择三个元素中的中位数
  • 长序列(>50),采样九个元素,中位数中的中位数。 pivot 的采样方式使得我们有探知序列当前状态的能力!

image.png

整合下来就诞生v2版本:

image.png

还能优化? 是的! 当检测到此时的pivot和上次相同时,认为重复元素较多,使用pritionEqual将元素排列在一起,减少重复元素对于privot的干扰。 最终版本的pdqsort是这样的:

image.png