六. 经典排序算法
6.1 例子:抖音直播排行榜功能
规则:某个时间段内,直播间礼物数TOP10房间获得奖励,每个房间展示排行榜
解决方案:
- 礼物数量存储在Redis-zset中,使用skiplist使得元素整体有序
- 使用Redis集群,避免单机压力过大,使用主从算法、分片算法
- 保证集群原信息的稳定,使用一致性算法
- 后端使用缓存算法(LRU)降低Redis压力,展示房间排行榜
总结:数据结构和算法几乎存在于程序开发中的所有地方
6.2 经典排序算法
-
插入排序
将元素不断插入已排序好的array中,最好O(n),平均o(n^2),最差O(n^2)
缺点:平均和最差时间复杂度较高
优点:最好情况时间复杂度低
-
快速排序
最好情况是选中的轴点恰好是中位数,即刚好能平分数组,最好O(nlogn),平均O(nlogn),最差O(n^2)
-
堆排序
时间复杂度均为O(n*logn)
-
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的时候检测重复度?但概率有点低。
解决方案:
- 如果两次分割的pivot相同,即进行了无效分割,这时认为pivot的值为重复元素。
- 当pivot选择策略表现不佳,随机交换元素。
淡黄色:可能发生,可能不发生
最终时间复杂度:最好O(n),平均O(nlogn),最差O(nlogn)
6.4 总结
Q:高性能排序算法如何设计?
A:根据不同情况选择不同策略,取长补短
Q:生产环境中使用的排序算法和课本上的排序算法区别?
A:理论算法注重理论性能,如时间、空间复杂度。生产环境中算法需要面对不同实践场景,注重实践性能。
Q:Go(<=1.18)的排序算法是?
A:混合排序算法,主体是快速排序,和1.19后的pdqsort区别在于 fallback时机、pivot选择策略、是否针对不同pattern优化 等。