这是我参与「第三届青训营 -后端场」笔记创作活动的的第一篇笔记。
数据结构与算法
为什么要学习数据结构与算法
- 几乎存在于程序开发中的所有地方 例子:抖音直播排行榜(在某个时间段内,直播间里无数TOP10房间获得奖励,需要在每个房间展示排行榜)
解决方案:
礼物数量存储在Redis-zert中,使用skiplist使得元素整体有序 使用 Redis集群,避免单机压力过大,使用主从算法,分片算法 保证集群原信息的稳定,使用一致性算法 端使用缓存算法(LRU)降低Redis压力。展示房间排行榜
经典排序算法
-
插入排序:将元素不断插入已经排好的array中,最好的时间复杂度为O(n) ,在有序的情况下发生,最坏为O(n^2) ,在倒序的情况下发生,avg O(n^2) ,缺点:时间复杂度很高
-
快排:基于分治的思想,不断的分割序列,分为比轴点大和小的两个序列,最好的时间复杂度为**O(nlogn)**选择的pivot是中位数,avg O(nlogn) ,最坏情况O(n^2)选择的pivot位于最开始的位置。
-
堆排序:最好的时间复杂度为O(nlogn) ,最坏的时间复杂度也为O(nlogn) 。
根据序列元素排列情况进行划分:
- 完全随机的情况:插入排序在短序列中最快,其他情况下,快排最快,但是堆排序和其差距不是特别大
- 有序/逆序的情况:在完全有序的情况下,插入排序是最快的,快排是最慢的,堆排序表现比较稳定。
- 元素重复度较高的情况
在此基础上根据序列的长度进行划分(eg:随机情况)
- 短序列:16的情况,插入最快
- 中序列:128的情况,快排最快,堆排序和其差距并不大
- 长序列:1024的情况,快排最快,堆排序和其差距并不大
插入排序-->单车
快速排序-->汽车
堆排序-->地铁
总结:在短序列和元素有序的情况下,插入排序的性能最好,在大部分情况下,快速排序有较好的综合性能,几乎在任何情况下,堆排序的表现都有比较稳定。
从零开始打造pdqsort
是一种不稳定的混合排序算法它的不同版本被应用在C++ BOOST Rust以及GO1.19中。 它对常见的序列类型做了特殊的优化,使得在不同条件下都拥有不错的性能。
version1
对于短序列(小于一定长度)我们使用插入排序 其他情况,使用快速排序来保证整体性能 当排序表现不佳时,使用堆排序来保证最坏情况下时间复杂度仍然是O(n*logn)
何时切换到堆排序
当最终的pivot的位置离序列两端很接近的时候(距离小于length/8),判定其表现不佳,当这种情况的次数达到limit的时候(bits.len(length)),切换到堆排序。
改进
-
改进choosePivot
- 使用首个元素作为pivot的时候,有序的情况下性能很差;
- 遍历数组寻找真正的中位数,遍历比对代价很高,性能也不好
- 因此在这两个中间找一个平衡,寻找近似的中位数。 根据序列长度不同,来决定选择策略,短序列的情况选择一个固定的元素,中序列,采样三个元素,长序列,采样九个元素,在这几个元素中找到中位数作为pivot。
-
改进partition,在Go语言中表现不佳。
version2
pivot的采样方式让我们对序列当前状态有一定的掌握情况。如果也许有序,使用有限制的插入排序,如果超过了次数的时候,还是要使用快速排序。
优化总结:
- 升级pivot的选择策略
- 发现序列可能是逆序,使用翻转
- 若可能有序,使用有限的插入排序。
改进:对于重复度较高的情况下
如果两次partition生成的pivot相同,即partition进行了无效分隔,此时认为pivot的值为重复元素。使用partitionEqul将重复元素排列在一起,减少重复元素对于pivot选择的干扰
version3
不建议使用采样处理,因为采样数量有限,不一定能采样到重复元素。
解决方案: 如果两次partition生成的pivot相同,即Partition进行了无效分割,此时认为pivot是重复元素
当检测到此时的pivot与上次相同,将重复元素排列在一起,减少重复元素对pivot选择的干扰。