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

121 阅读3分钟

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

经典排序算法

插入排序

将元素不断插入已经排好序的array中

1653056338(1).png

时间复杂度
最好情况平均情况最坏情况
O(n)O(n^2)O(n^2)

快速排序

分治思想,不断分割序列直到序列整体有序

  • 选定一个pivot(轴点)
  • 使用pivot分割序列,分成元素比pivot大和元素比pivot小的两个序列 | | 时间复杂度 | | | --- | --- |---| | 最好情况 |平均情况|最坏情况 | | O(nlogn) |O(nlogn)|O(n^2) |

堆排序

利用堆的性质形成的排序算法

  • 构造一个大顶堆
  • 将根节点(最大元素)交换到最后一个位置,调整整个堆,如此反复 | | 时间复杂度 | | | --- | --- |---| | 最好情况 |平均情况|最坏情况 | | O(nlogn) |O(nlogn)|O(n*logn) |

三种算法的总结

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

实际场景

根据序列元素排序情况划分

  • 完全随机的情况(random)
  • 有序/逆序的情况(sorted/reverse)
  • 元素重复度较高的情况(mod8) 在此基础上还需根据序列长度划分(16/128/1024)

实际场景benchmark结论

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

pdqsort(pattern-defeating-quicksort)

是一种不稳定的混合排序算法 不同版本被应用在C++ BOOST、Rust以及Go 1.19中,对常见的序列类型做了特殊的优化,使得在不同条件下都拥有不错的性能

pdqsort - version1

  • 对于短序列(在泛型版本根据测试选定24),我们使用插入排序
  • 其他情况,使用快速排序来保证整体性能
  • 当快排表现不佳时,使用堆排序来保证最坏情况下时间复杂度仍然为O(n*logn) 表现不佳:当最终pivot的位置离序列两端很接近时(距离小于length/8)判定其表现不佳,当这种情况次数达到limit(bits.Len(length))时,切换到堆排序

version1.png

pdqsort - version2

优化了Pivot的选择

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

同时Pivot的采样方式也使得我们有探知序列当前状态的能力

  • 采样的元素是逆序的,则序列可能已经逆序,翻转整个序列
  • 采样的元素是顺序的,则序列可能已经有序,使用插入排序 (注:插入排序实际使用partialinsertionSort,即有限制次数的插入排序)

version2.png

pdqsort - final version

优化了重复元素很多的情况

解决方案:如果两次partition生成的pivot相同,即partition进行了无效分割,此时认为pivot的值为重复元素

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

优化2:当pivot选择策略不佳时,随机交换元素

final.png

时间复杂度
最好情况平均情况最坏情况
O(n)O(n*logn)O(n*logn)