go 1.19 发行版内置排序算法的底层原理 | 青训营笔记

318 阅读5分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第1篇笔记 .
听过青训营老师讲述的go语言内置的排序算法,本文章对于go 1.19发行版内置算法 的由来做了一些总结和归纳: 首先,我们可以对序列元素的排序情况做几个大分类:

1.完全随机的情况
2.有序、逆序的情况
3.序列重复度高的情况
4.还需要根据长度划分

常用的几个排序算法:插入排序,快速排序,堆排序。哪种方法的时间复杂度最小呢? 从理论上分析:

插入排序:Best:O(n)   Avg: O(n^2)  worst: O(n^2)

快速排序:Best:O(nlogn) Avg:O(nlogn)  worst:O(n^2)                            

堆排序:  Best:O(nlogn)  Avg:O(nlogn)  worst: O(nlogn)

从时间复杂度方面分析,似乎堆排序的综合时间复杂度最小,那么最优的排序是堆排序吗? 在实际测试中,出现了以下结果:

benchmark-random(随机序列)

短序列中,插入排序最快

中序列中,快速排序最快

长序列中,快速排序最快

benchmark-sorted:(有序序列)

短序列中。插入排序最快

中序列中,插入排序最为快

长序列中,插入排序也是最快

由实际测试结果测试出:
在短序列和元素有序的情况下,插入排序的性能最好。在大部分情况下,快速排序有较好的综合性能。 在任何情况下,堆排序的表现最为稳定。 我们可以将这三种算法类比为三种常见的交通工具:

插入排序:单车

快速排序:汽车

堆排序:地铁

二、如何设计一个跟好的算法 如何将三种算法结合实现最优的算法?go 1.19的算法又是基于什么呢?

go 1.19的排序算法为pdqsort混合排序算法。 在pdqsort算法中,其实是对以上三种排序算法做了整合和取优:

pdqsort混合排序算法:

对于短序列使用插入排序,其他情况使用快速排序保证其稳定性,当快速排序表现不佳时,使用堆排序来保证最坏情况下时间复杂度为O(nlogn)

那么有以下问题:

1.短序列的具体长度是多少呢?

12~32,在不同语言和场景中使用,泛型版本为选定24

2.如何得知快速排序表现不佳,合适切换为堆排序?

可以从快排本身的行为来发现,当最终的pivot离序列两端很近时(距离小于length/8)判定其表现不佳,当这种情况的次数达到limit时,切换到堆排序。(limit==0,发现前几次排序效果不是很好,则limit-1,limit的初始值会根据长度计算出一个值)

由此诞生了:

    pdqsort version1版本

但是version1版本有可以改进的地方,例如:

如何使快排更快呢?

1.选取pivot为中位数 -> 改进 choose pivot

2.pation速度更快 -> 改进partion,但此优化在go中并不好

因此有了pdqsort version2的出现: version1版本中使用首个元素缺点pivot,如果有序就会达到最坏情况 实现简单,但效果不好,在sorted下性能很差

解决方法:

遍历数组寻找中位数:遍历数组代价很高

寻找pivot所需要的开销,并平衡pivot的性能优化,寻找近似的中位数

根据序列长度的不同,来决定选择的策略

优化:

pivot选择:
    短序列,选择固定元素
    中序列,采样三个元素,选择中位数
    长序列,采用九个元素,选择中位数

pivot采样方式也可以是我们有探知序列当前状态的能力:

如果采样的元素都是逆序,很可能序列已经逆序,所以先反转整个序列

如果采用元素都是顺序,序列恒可能有序,使用插入排序算法

(注意:这里的插入排序为partiallnsertionSort,即尝试三次的插入排序,若任然没有拍成一个顺序的序列,则放弃使用插入排序)

version2的优化总结:

 1.选择了一个近似的中位数(采样三个点,先猜测,失败后考虑快排)
 2.发现序列可能有逆序,先使用reverse
 3.发现序列可能有序,使用有限插入排序
 

还有什么地方没有实现性能优化呢?

 短序列情况:使用插入排序

 极端情况:使用堆排序保证算法的可行性

 完全随机的情况:跟好的pivot选择(version2的优化)

 有序或逆序根据序列状态反转或者插入排序

对于元素重复度较高的情况,pivot采样的时候可以检测重复度吗?

 不可行。因为采样数量有限,不一定能采样到相同的元素

解决防范:

 如果两次partition生成的pivot相同,即partition进行了无效分割,此时认为pivot的值为重复元素
 优化重复元素较多的情况:
 

重复元素较多:

  当检测到此时pivot和上次相同时(发生在leftsubarray),使用partitionEqual将重复元素排列在一起,减少重复元素对于pivot选择的干扰
  当选择策略表现不佳,随机交换元素:
  可以防止一些黑客的攻击

pdqsort-final version(最终pdqsort算法的版本)

最终pdqsort的排序测试结果:

最好情况下o(n)
平均情况下o(nlogn)
最坏其概况下o(nlogn)

实际性能测试:

在有序和无需情况下提升了十倍
在其他情况下有10~50%的提升

总结:

高性能排序算法是如何设计的:

根据不同情况选择不同策略,取长补短

生产环境中的排序算法和课本上的排序算法有什么不同?

理论算法注重理论,例如时间空间复杂度等。生产算法中的算法需要对不同的实践场景,更加注重实际性能