Go的新排序算法:pdqsort

125 阅读4分钟

[

Codejitsu

](medium.com/@code-jitsu…)

编码术

关注

6月7日

-

4分钟阅读

[

拯救

](medium.com/m/signin?ac…)

Go的新排序算法:pdqsort

照片由 Markus Spiske 毫无悬念

我一直认为Go的标准库非常容易阅读。标准库的部分内容包括一些自成一体的概念,不需要太多事先的背景就可以深入了解。当我读到Go的排序算法已经改变¹为一个叫做 "pdqsort "的东西时,我想应该去看一看并了解一下它。

在这篇文章中,我们将通过研究以前的排序算法和新的排序算法来做到这一点。

到目前为止,Go在其标准库中依靠的是Quicksort。你已经可以在这里去查看它的实现了,但我也会在我们前进的过程中提供相关链接。

在其核心中,Quicksort算法的工作方式如下。

  1. 选择一个支点²
  2. 将小于支点的项目安排在支点的 "左边",大于支点的项目安排在 "右边"
  3. 对于支点的两边,从#1开始递归应用同样的逻辑。

经过这些步骤,我们最终应该得到一个排序的集合。这在很多数据集中都很好用,我们得到了一个平均复杂度为O(n logn)⁴的漂亮的排序算法。然而,在有些情况下,它的表现并不理想,最终有一个二次复杂度。这意味着它的最坏情况是O(n ^ 2)⁵的性能*。*这些Quicksort表现不佳的情况大多与已排序或几乎已排序的⁶数据集有关。

虽然我提到Go最初使用了Quicksort,但Go标准库采用了一些技巧来避免陷入这些情况,在这些情况下,Quicksort会产生其最差的性能,或达到相当的水平。

阅读源代码,其中一些是这样的。

  • 如果集合的大小 "足够小"⁷,使用⁸Shellsort
  • 如果递归的深度太大,就改用⁹Heapsort
  • 尝试选择一个好的支点¹⁰

你可能已经注意到了,Go的Quicksort实现并不是算法的简单形式,实际上是一种叫做Introsort的混合算法。

现在,我们来谈谈新的排序算法 "pdqsort" - Pattern Defeating Quicksort¹。读了这个名字,听起来好像还是Quicksort。对这一变化的描述概述了在一些最坏的情况下,它比以前的版本产生了更好的性能¹²。

让我们从这里开始,看看哪些地方改变了,哪些地方保留了。

当集合大小较小时,我们仍然使用 插入式排序¹³,在某些情况下,则退回到堆栈式排序。用于选择枢轴的方法也与以前的实现类似。

与之前的实现不同,pdqsort试图通过在遇到不同模式时移动东西来避免最坏的情况。

  • 如果我们处理的分区不能以平衡的方式分区¹⁴,我们就会移动东西以避免将来出现这种不平衡的分区。此外,与其纯粹基于递归深度而回落到Heapsort,不如用不平衡分区的数量来切换到回落。
  • 处理集合中可能已经被排序或反向排序的部分
  • 处理与支点相等的元素,以避免将来不必要的交换

在介绍pdqsort算法的文章中,还有很多内容。这些是我通过阅读源代码和文章可以发现的主要区别。新算法的实现似乎有更多的活动部件,尽管代码的组织方式使其易于理解。

这对我了解哪些地方发生了变化是有帮助的。我希望这能很好地概述这一变化以及它所试图解决的问题。

1.截止到Go 1.18,这仍然没有发布,预计会在未来的版本中登陆

2.枢轴是指在给定的集合中排列其他项目时要保持原位的项目。

3.排序的概念可能根据被排序的项目而有所不同。这些可能是整数或更复杂的数据结构。

4.见 对时间复杂性的解释

5.n个方块的顺序。

6.这可能包含许多重复的元素--见"荷兰国旗问题"

7.小于13

8.请看相关的代码 这里

9.请看 此处 如何计算切换到Heapsort的递归深度?

10.请看 这个 解释了选择一个好的枢轴的困难和不同的方法。这些大部分也可以在Go的源代码中找到。 避免 整数溢出。 挑选 的""作为支点

11.参见相关文章 这里 对该算法进行了更深入的解释。

12.该修改的作者 包括 的一项比较表明,它比以前的算法在排序片上快10倍左右,而且从来没有明显地比以前的版本慢过。

13.虽然之前的算法使用 贝壳分类,它们是非常相似的算法。

14.这是通过检查任何一边来完成的(见 1 & 2),看我们是否最终得到了两个分区,其中一个是 小得多 比另一个