目标
- 数据结构和算法学习重要性说明
- pdqsort
1 为什么要学习数据结构和算法?
- 由于项目需求,需要使用Redis数据库,为了保证Redis集群的稳定,需要使用到一些算法。因 此在后端使用某些算法可以降低Redis的压力,所以就需要数据结构和算法。
-
案例:某时间段,抖音排行榜top10
-
解决方案:
- 礼物数量在zset,用skiplist使得元素整体有序
- 通过主从算法、分片算法,使用Redis集群,避免单机压力过大
- 一致性算法保证集群信息的稳定
- LRU降低Redis压力,展示排行
-
总结:数据结构和算法几乎在于程序开发中的所有地方。
-
最快排序
- python-timsort
- C++-introsort
- Rust-pdqsort
- Go排序算法提升——introsort(<=1.8)
- 现重新实现,在常见常见比之前快大概10倍,成为默认排序算法
2 经典的排序算法
- 插入排序:从第二个元素开始与前驱元素进行一一对比,比较结果决定是否移动位置。最好的情况是有序排列的数组,时间复杂度为O(n),平均和最差为O(n^2)。
- 快速排序:选pivot,将数组递归分开,用两个指针,分别指向左右两边,一次比较大小,交替进行,时间复杂度最好和平均是O(nlogn),最坏都为O(n^2)。性能处于中间层次
- 堆排序:利用不断地调整堆排序,使堆保持最大或最小根堆。通过上滤或下滤的操作来实现调整堆。堆排序最好最坏都为O(nlogn)。
总结:不同场景下需要用不同的排序算法才能达到最好的效果,因此没有排序算法可以说是最快最好的。
根据序列元素排列情况划分:
-
完全随机的情况(random)
-
有序/逆序的情况(sorted/reverse)
-
元素重复度较高的情况(mod8)
-
在此基础上,还需要根据序列长度的划分(16/128/1024)
-
根据实际场景的benchmark结论:
- 在随机的短序列中,插入排序最快,在随机的长序列和中序列中,堆排序和快速排序相差不大。但快排在长序列和中序列中速度最快。
- 在序列有序中,插入排序在三种情况下都很快,而堆排序相差10倍,快速排序相差几十倍。
- 大部分情况下,快速排序效果比较好。在任何情况下,堆排序都比较稳定。在短序列情况下,插入排序性能最好。
插入-单车 快排-汽车 堆排序-地铁
3 从零开始打造 pdqsort
-
pdqsort(pattern-defeating-quicksort)
- 不稳定的混合排序
- 应用在 C++ Boost、rust、go 1.19
- 对常见的序列类型进行特殊优化
-
version1- 结合经典算法继续切换
- 短序列(12-32,泛型选24),插入
- 其他,快排
- 快排不佳则用堆排序,如何判别:看pivot位置选择
- 接近两端length/8,次数为limit(bits.Len(length)),切换
-
version2——改进pivot选择,改进partition(虎烈)
-
关于pivot的选择
- 使用首个元素作为pivot实现简单,但是往往效果不好
- 但是寻找中位数的方法又需要遍历数组,性能不好
-
根据序列长度的不同,来决定选择策略
- 短序列(<=8),选择固定元素
- 中序列(<=50)中,我们采集三种元素,寻找他的中位数。从前中后三个位置选取三种元素,选取三个元素中的中位数。
- 长序列(>50)中,我们采集九种元素,找到它们之间的中位数。
-
发现序列可能逆序,翻转
-
发现序列可能有序,有限的插入排序
-
-
final version
-
优化——重复元素很多的情况(partitonEqual)
- 采样时pivot的数量有限,所以不一定会采样到相同的元素
- 我们可以对两次partition生成的pivot进行比对,如果两个partition生成的pivot相同,则认为此次分割无效。
- 方案:检测到这次pivot和上次相同,使用partitonEqual将重复元素排列到一起,减少重复元素对pivot选择的干扰
-
优化-pivot选择策略不佳——随机交换元素
-大致10%-50%的提升
学习心得
- 高性能算法根据不同情况取长补短开发的
- 理论注重理论性能,时空复杂度,但是生产环境需要考虑实际场景的实际性能
- pdqsort做了fallback时机、pivot选择策略、是否对不同pattern优化方面工作
- 实际开发的优化一定要考虑实际场景
引用
ppt:数据结构与算法.pptx - 飞书云文档 (feishu.cn)