[
6月7日
-
4分钟阅读
[
拯救
Go的新排序算法:pdqsort
照片由 Markus Spiske 于 毫无悬念
我一直认为Go的标准库非常容易阅读。标准库的部分内容包括一些自成一体的概念,不需要太多事先的背景就可以深入了解。当我读到Go的排序算法已经改变¹为一个叫做 "pdqsort "的东西时,我想应该去看一看并了解一下它。
在这篇文章中,我们将通过研究以前的排序算法和新的排序算法来做到这一点。
到目前为止,Go在其标准库中依靠的是Quicksort。你已经可以在这里去查看它的实现了,但我也会在我们前进的过程中提供相关链接。
在其核心中,Quicksort算法的工作方式如下。
- 选择一个支点²
- 将小于支点的项目安排在支点的 "左边",大于支点的项目安排在 "右边"
- 对于支点的两边,从#1开始递归应用同样的逻辑。
经过这些步骤,我们最终应该得到一个排序的集合。这在很多数据集中都很好用,我们得到了一个平均复杂度为O(n logn)⁴的漂亮的排序算法。然而,在有些情况下,它的表现并不理想,最终有一个二次复杂度。这意味着它的最坏情况是O(n ^ 2)⁵的性能*。*这些Quicksort表现不佳的情况大多与已排序或几乎已排序的⁶数据集有关。
虽然我提到Go最初使用了Quicksort,但Go标准库采用了一些技巧来避免陷入这些情况,在这些情况下,Quicksort会产生其最差的性能,或达到相当的水平。
阅读源代码,其中一些是这样的。
你可能已经注意到了,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.虽然之前的算法使用 贝壳分类,它们是非常相似的算法。