Go 1.19 排序算法实现 | 青训营笔记

139 阅读4分钟

写在前面

排序算法可以算是我学习算法的开始,但是通过这节课的学习,我了解到了工业界数据结构与算法,也感受到了它们的相同与不同。

排序算法

经典排序算法有很多,像插入排序、快速排序、堆排序、选择排序等等。这些算法应该是数据结构与算法的入门内容,不在此赘述。

这里想简单介绍一下,Python 的 TimSort,C++ 的 introsort 和 Rust 的 pdqsort 排序算法。

TimSort 算法是 Python 标准库中的排序算法,其中结合了归并排序和插入排序的优点,并考虑现实世界中待排序数据的分布情况,具有较好的性能表现和稳定性。在最坏的情况下,时间复杂度为 O(n log n),但在平均情况下表现更好,时间复杂度为 O(n),空间复杂度为 O(n)。

introsort 算法是 C++ 标准库中的排序算法,结合了快速排序和堆排序的优点,在选择排序算法的过程中自适应选择算法,可以在平均情况下具有较好的性能表现。在最坏情况下,时间复杂度为 O(n log n),但大多数情况下表现更好,时间复杂度为 O(n log n),空间复杂度为 O(log n)。

pdqsort 算法是 Rust 社区中的排序算法,由原来的 quicksort 代码库经过优化后得到,具有较好的性能和稳定性。在大多数情况下,时间复杂度为 O(n log n),空间复杂度为 O(log n),并不需要使用额外的内存进行排序。

pdqsort

通过本次课的学习,我了解到了 pdqsort 这个排序算法,也让我对排序算法的实现有了新的认识——融合。pdqsort 是一种不稳定的混合排序算法,它的不同版本被应用在 C++ BOOST、Rust 以及 Go 1.19 中。它对常见的序列类型做了特殊的优化,使得在不同条件下都拥有不错的性能。

插入排序、快速排序、堆排序在不同的情况下性能有所差异,有它们各自适合的序列长度、序列性质。在单独使用的时候,会因为实际要排序的序列不同,性能有所差异。那么,能不能将它们的优势综合起来,使得不论待排序的序列性质如何,算法都有一个较优的表现呢?pdqsort 就解决了这个问题,通过对待排序队列性质的实时监测,选取当前情况下最优的排序算法,最终使得整体性能最优。

下面对 pdqsort 算法的实现做简要介绍。

  1. 对于一个待排序队列,先判断序列长度,如果长度小于 24,则使用插入排序算法对其排序。
  2. 如果当前 limit = 0,则使用堆排序算法对其进行排序。这里的 limit 是一个限制,具体代表什么在后文解释。
  3. 否则,使用快速排序算法。

看了上面的过程,可能会有很多疑惑,通过下面的问题,你会对这个算法为什么这样实现有一个简单的认识。

  1. 为什么长度小于 24 使用插入排序?

    通过测试,插入排序在短序列的情况下表现最好,这个短序列的长度区间为 12~32。在泛型版本中,根据测试选择 24 作为界限。

  2. 什么是 limit ?

    快速排序在最坏的情况下复杂度高达 O(n2)O(n^2),那么是不是可以实时监控快速排序的运行状况,如果较差,那就弃用快速排序,而选择其他的排序。limit 就是用来支持这个切换操作的。当快速排序最终 pivot 的位置离序列两端很近时(距离小于 length / 8)判定其表现不佳,当这种情况的次数达到 limit (即bits.Len(length)bits.Len(length))时,切换到堆排序。

  3. 快速排序部分还有哪些优化?

    ① 让 pivot 的选择更加合理——尽量选择中位数。通过使用 median of three 或 median of medians 采样方法,使得选取的 pivot 更加接近中位数。

    ② 在上面采样的过程中,如果采样得到的元素都是逆序排列,那么猜测序列可能是逆序的,此时翻转整个序列;如果采样的元素都是顺序排列,那么猜测序列可能已经排好序,此时直接使用有限制次数的插入排序。

    ③ 对于重复元素很多的情况,当检测到此时的 pivot 和上次相同时,使用 partitionEqual 将重复的元素排列在一起,减少重复元素对 pivot 选择的干扰。

    ④ 当 pivot 选择策略表现不佳时,随机交换序列中的元素。

pdqsort 算法的大致内容已经介绍完毕。这个算法让我对业界实际使用的排序算法有了新的认识,也拓宽了我的思维方式。