[ Go 1.19 排序算法实践 | 青训营笔记 ]

101 阅读3分钟

Go 1.19 排序算法实践 - 课程笔记

课程引入

课程以不同高级语言的默认排序算法作为引入点

Python - timsort

C++ - introsort

Rust - pdqsort

然后过渡到Go的排序算法 有没有提升空间?

Go(<=1.18)-introsort

Go(>=1.19)-pdqSort

三大经典排序算法

  1. Insertion Sort 插入排序

    遍历元素,并将元素插入到 已经有序的 区间

  2. Quick Sort 快速排序

    分治思想,寻找一个pivot轴点,然后将元素分为 pivot为依据的 一大一小 两个序列

  3. Heap Sort 堆排序

    利用堆的 堆序性。 新加入的元素放入 最后一个位置,然后上筛。

三种算法的 三种情况 的对比

​ 最好情况 平均情况 最坏情况

image-20230601221124041.png

结论

  • 插入排序平均和最坏情况时间复杂度都是 O(n2)O(n^2),性能不好
  • 快速排序整体性能处于 中间层次
  • 堆排序性能稳定, “众生平等”

提出疑问

能不能有这么一个算法, 能够结合三者的优点,

达到对于三种情况 都有着最优的性能?

从零开始打造 pdqSort

(pattern-defeating-quicksort)

v1

流程图如下:

image-20230601222017345.png

策略:

  • 对于短序列(<= 24)使用插入排序

  • 其他情况,使用快速排序(使用首个元素作为pivot)来保证整体性能

  • 当快速排序表现不佳(limit == 0),使用堆排序来保证 最坏情况 O(nlogn)O(n*logn)的时间复杂度

    limit是快排的表现效果,最开始的值根据序列的长度而定,每使用一次快排就 -1,到 limit = 0 时,说明使用了多次快排 且表现不佳

==需要改进的问题==: 改进choose pivot 因为默认是选取第一个元素,这肯定不妥。

v2 改进choose pivot

流程图如下:

image-20230601223421926.png

不使用首个元素作为 pivot (特别在有序情况下 性能不好)

遍历比对的代价高,性能不好。

所要做的:

​ 寻找pivot所需要的开销 <= ==平衡== => pivot带来的性能优化

​ ==寻找近似中位数==

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

选一个随机的 是否可行?

不行,一般来说在实际的表现不是 最优的

  1. 生成一个伪随机数,需要性能开销
  2. 随机的pivot不能真正代表实际情况

优化-Pivot 的选择

  • 短序列(<=8),选择固定元素
  • 中序列(<=50),采样三个元素, median of tree ,选中位数。
  • 长序列(>50),采样九个元素, median of medians

pivot的采样方式使得我们 有探知序列当前状态的能力!

采样的元素都是逆序排列 => 序列==可能==已经逆序 =》 翻转整个序列

采样的元素都是顺序排列 => 序列==可能==已经有序 =》 使用插入排序

注:插入排序 实际使用 partialInesertionSort, 即有限制次数的插入排序

还有什么场景没有优化

已经优化的

  • 短序列情况
    • 用插入排序(v1)
  • 极端情况
    • 使用堆排序保证算法的可行性(v1)
  • 完全随机的情况(random)
    • 更好的pivot选择策略(v2)
  • 有序/逆序的情况(sorted/reverse)
    • 根据序列状态翻转或者插入排序(v2)

未优化:

重复度较高

final version

流程图如下:

image-20230531225022661.png

对比

image-20230531225109920.png

end

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

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

生产环境中使用的的排序算法和课本上的排序算法有什么区别?

理论算法注重理论性能,例如时间、空间复杂度等。

生产环境中的算法需要面对不同的实践场景,更加注重实践性能

Go语言(<=1.18)的排序算法是快速排序么?

实际一直是混合排序算法,主体是快速排序。

Go <= 1.18时的算法也是基于快速排序,和pdqsort的区别在于fallback时机、pivot选择策略、是否有针对不同pattern优化等