数据结构与算法 | 青训营笔记

88 阅读4分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第5篇笔记

经典排序算法

插入排序

插入排序的主要想法是,每一次将一个待排序的元素插入到前方已经排序好的序列中,直到插入所有元素。尽管其平均时间复杂度高达 O(n^2),但是在 array 长度较短(这个值一般是 16 ~ 32)的情况下,在实际应用中拥有良好的性能表现。

BestAvgWorst
O(n)O(n^2)O(n^2)

快速排序

快速排序(quicksort) 主要采用了分治的思想,具体的过程是将一个 array 通过选定一个 pivot(锚点)分成不同的 sub-arrays,选定 pivot 后,使得这个 array 中位于 pivot 左边的元素都小于 pivot,位于 pivot 右边的元素都大于 pivot。由此,pivot 两边构成了两个 sub-arrays,然后对这些 sub-arrays 进行相同的操作(选定 pivot 然后切分)。当某个 sub-array 只有一个元素时,其本身有序,此时便可以退出循环。如此反复,最后得到整体的有序。

BestAvgWorst
O(nlogn)O(nlogn)O(n^2)

堆排序

堆排序是利用堆结构设计出来的一种排序算法。这个算法有一个非常重要的特性,其在最坏情况下的时间复杂度仍然为 O(n* logn)。故而很多混合排序算法利用了这一特性,将堆排序作为 fall back 的排序算法,使得混合排序算法在最坏情况下的理论时间复杂度仍然为 O(n* logn)。

BestAvgWorst
O(nlogn)O(nlogn)O(nlogn)

三个算法性能比较

  • 所有短序列和元素有序情况下,插入排序性能最好
  • 在大部分情况下,快速排序有较好的综合性能
  • 几乎在任何情况下,堆排序的表现都比较稳定

pdqsort

pdqsort(pattern-defeating-quicksort)是一种不稳定的混合排序算法,它的不同版本被应用在BOOST、Rust以及Go1.19中。它对常见的序列类型做了特殊的优化,使得在不同条件下都拥有不错的性能。

version1

  • 对于短序列(小于一定长度12-32,在泛型版本根据测试选定24)使用插入排序
  • 其他情况,使用快速排序保证整体性能
  • 当快速排序表现不佳时,使用堆排序来保证最坏情况下时间复杂度仍为O(n*logn)

存在问题:如何确定快速排序表现不佳,以及何时切换到堆排序

当最终pivot的位置离序列两端很接近时判定其表现不佳,当这种情况的次数达到limit时,切换到堆排序

优化方向:尽可能使得快排的pivot为序列的中位数

version2

关于pivot的选择

  1. 使用首个元素作为pivot ----实现简单,但效果不好,在sorted有序情况下性能很差
  2. 遍历数组,寻找真正的中位数 ----遍历存在一定开销

解决方案:寻找近似中位数

pivot 如果选定为中位数,则大部分情况下每次 partition 都会形成两个长度基本相同的 sub-arrays,我们只需要 logn 次 partition 就可以使得 array 完全有序,此时时间复杂度为 O(n* logn)。在最坏情况下,我们需要 n-1 次 partition (每次将长度为 L 的 array 分为长度为 1 和 L - 1 的两个 sub-arrays)才能使得 array 有序,此时时间复杂度为 O(n^2)。

en.wikipedia.org/wiki/Quicks…

pivot选择策略:

  • 短序列(<=8),选择固定元素
  • 中序列(<=50),采样三个元素
  • 长序列(>50),采样九个元素

这样的采样方式使得能够探知序列当前状态:

  1. 采样元素都是逆序排序 --- 序列可能已经有序 --- 翻转整个序列
  2. 采样元素都是顺序排列 --- 序列可能已经有序 --- 使用插入排序

final version

针对重复元素很多的情况?

认为两次partition生成的pivot相同,即partition进行了无效分割,pivot的值为重复元素

优化-重复元素较多的情况

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

优化-当pivot选择策略表现不佳时,随机交换元素

避免一些极端情况使得QuickSort总是表现不佳

640.png