部分排序算法|青训营笔记

33 阅读3分钟

六. 经典排序算法

6.1 例子:抖音直播排行榜功能

规则:某个时间段内,直播间礼物数TOP10房间获得奖励,每个房间展示排行榜

解决方案:

  1. 礼物数量存储在Redis-zset中,使用skiplist使得元素整体有序
  2. 使用Redis集群,避免单机压力过大,使用主从算法、分片算法
  3. 保证集群原信息的稳定,使用一致性算法
  4. 后端使用缓存算法(LRU)降低Redis压力,展示房间排行榜

总结:数据结构和算法几乎存在于程序开发中的所有地方

6.2 经典排序算法

  • 插入排序

    将元素不断插入已排序好的array中,最好O(n),平均o(n^2),最差O(n^2)

    缺点:平均和最差时间复杂度较高

    优点:最好情况时间复杂度低

  • 快速排序

    最好情况是选中的轴点恰好是中位数,即刚好能平分数组,最好O(nlogn),平均O(nlogn),最差O(n^2)

  • 堆排序

    时间复杂度均为O(n*logn)

  • random测试

    random序列测试

  • 有序测试

    有序序列测试

  • 结论

    短序列和元素有序的情况,插入排序性能最好(单车)

    大部分情况下,快速排序有较好的综合性能(汽车)

    任何情况下,堆排序表现稳定(地铁)

    因此,我们能否可以结合这几种 "交通工具",设计一个更好的算法?

6.3 从零开始打造pdqsort

作为一种不稳定的混合排序算法,pdqsort 的不同版本被应用在 C++ BOOST、Rust 以及 Go 1.19 中。它对常见的序列类型做了特殊的优化,使得在不同条件下都拥有不错的性能,本节课将详细介绍其实践步骤。

6.3.1 Version 1

结合三种排序方法的优点:

  • 对于短序列(小于一定长度)使用插入排序(在泛型版本中选定24)
  • 其他情况,使用快速排序保证整体性能(当最终pivot位置离序列两段很接近,距离小于length/8时,这种情况达到limit次,切换到堆排序)
  • 当快速排序表现不佳,使用堆排序保证最坏情况下时间复杂度仍为O(n*logn)

6.3.2 Version 2

继续针对快速排序部分优化,我们可以从pivot的选择入手:

  • 使用首个元素作为pivot(最简单方案)

  • 遍历数组,寻找中位数(代价高,性能不好)

  • 优化选择:

    短序列(小于等于8),选择固定元素;

    中序列(小于等于50),采样三个元素,取其中的中位数

    长序列(大于50),采样九个元素,取其中的中位数

  • 上面的采样方法使得我们可以探知序列当前状态:

    采样的元素是逆序 -> 序列可能 逆序 -> 翻转序列

    采样的元素是顺序 -> 序列可能 有序 -> 插入排序(限制次数)

6.3.3 Version 3

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

采样pivot的时候检测重复度?但概率有点低。

解决方案:

  1. 如果两次分割的pivot相同,即进行了无效分割,这时认为pivot的值为重复元素。
  2. 当pivot选择策略表现不佳,随机交换元素。

pdqsort

淡黄色:可能发生,可能不发生

最终时间复杂度:最好O(n),平均O(nlogn),最差O(nlogn)

6.4 总结

Q:高性能排序算法如何设计?

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

Q:生产环境中使用的排序算法和课本上的排序算法区别?

A:理论算法注重理论性能,如时间、空间复杂度。生产环境中算法需要面对不同实践场景,注重实践性能。

Q:Go(<=1.18)的排序算法是?

A:混合排序算法,主体是快速排序,和1.19后的pdqsort区别在于 fallback时机、pivot选择策略、是否针对不同pattern优化 等。