数据结构与算法 | 青训营笔记
这是我参与「第三届青训营 -后端场」笔记创作活动的的第12篇笔记
一、为什么要学习数据结构和算法
比如-抖音直播排行榜功用
规则:某个时刻段内,直播间礼物数TOP10房间获得奖赏,需要在每个房间展示排行榜
解决方案
- 礼物数量存储在Redis-zset中,运用skiplist 使得元素全体有序
- 运用Redis集群,防止单机压力过大,运用主从算法、分片算法
- 确保集群原信息的安稳,运用一-致性算法
- 后端运用缓存算法(LRU)降低Redis压力,展示房间排行榜
问题
什么是最快的排序算法?
Python -timesort C++ -introsort Rust – pdqsort
go的排序算法有没有提升空间?
Go(≤ 1.1.8)-introsort
从头完成了Go的排序算法,在某些场景中比之前的算法块 ~10倍,成为Go 1.19中的默认算法
二、经典排序
1. 刺进排序 Insertion Sort
将元素不断刺进到现已排好的Array中
- 其实只有一个元素5,其自身是一个有序序列
- 后续元素刺进有序序列中,即不断产生交流,直到找到第一个比其小的元素
最快时刻复杂度:O(N) 最差:O(N2) 空间复杂度:O(1)
缺陷 :
- 最坏时刻复杂度高达 O(n2)
长处:
- 最好状况下时刻复杂度 O(n)
2. 快速排序 Quick Sort
分治思想,不断切割序列直到序列全体有序
- 选定一个pivot(轴点)
- 运用pivot切割序列,分成元素比pivot大 和 元素比pivot小的两个序列
最快时刻复杂度:O(N*logN) 最差:O(N2) 空间复杂度:O(1)
缺陷 :
- 最坏时刻复杂度高达 O(n2)
长处:
- 最好状况下时刻复杂度 O(N*logN)
3. 堆排序 Heap Sort
运用对的性质形成的排序算法
- 构造一个大顶堆
- 将根节点(最大元素)交流到最后一个方位,调整整个堆,如此重复
最快时刻复杂度:O(NlogN) 最差:O(NlogN) 空间复杂度:O(logN)
缺陷 :
- 最好时刻复杂度高达 O(N*logN)
长处:
- 最坏状况下时刻复杂度 O(N*logN)
4. 经典算法理论形象
排序 | Best | Avg | Worst |
---|---|---|---|
刺进排序 | O(N) | O(N2) | O(N2) |
快速排序 | O(N*logN) | O(N*logN) | O(N2) |
堆排序 | O(N*logN) | O(N*logN) | O(N*logN) |
- 刺进排序均匀和最坏状况时刻复杂度都是O(n2),功能欠好
- 快速排序全体功能处于中间层次
- 堆排序功能安稳,“众生平等”
5. 实践场景
依据序列元素摆放状况区分
- 完全随机的状况(random)
- 有序/逆序的状况(sorted/reverse)
- 元素重复度较高的状况(mod8)
在此基础上,还需要依据序列长度的区分(16/128/1024)
状况 | 最快排序 |
---|---|
短序列(长度为16) | 刺进排序 |
中序列(长度为128) | 快速排序 |
长序列(长度为1024) | 快速排序 |
- 刺进排序在短序列中速度最快
- 快速排序在其他状况中速度最快
- 堆排序速度于最快算法差距不大
假如序列自身就是有序的
状况 | 最快排序 |
---|---|
短序列(长度为16) | 刺进排序 |
中序列(长度为128) | 刺进排序 |
长序列(长度为1024) | 刺进排序 |
- 刺进排序在现已有序的状况下速度最快
定论
- 所有短序列和元素有序状况下,刺进排序功能最好
- 在大部分的状况下,快速排序有较好的归纳功能
- 简直在任何状况下,堆排序的体现都比较安稳
三、从零开始打造pdqsort
pdqsort (pattern- defeating-quicksort)
是一种不安稳的混合排序算法,它的不同版别被应用在C++ BOOST、Rust以及Go 1.19中。它对常见的序列类型做了特别的优化,使得在不同条件下都具有不错的功能
pdqsort – version1
结合三种排序方法的长处
- 关于短序列(小于一定长度)咱们运用刺进排序
- 其他状况,运用快速排序来确保全体功能
- 当快速排序体现欠安时,运用堆排序来确保最坏状况下时刻复杂度依然为O(n*logn)
Q&A
-
1.短序列的具体长度是多少呢? 12 ~ 32, 在不同语言和场景中会有不同,在泛型版别依据测验选定24
-
怎么得知快速排序体现欠安,以及何时切换到堆排序?
当最终pivot的方位离序列两端很挨近时(间隔小于length/8)断定其体现欠安,当这种状况的次数达到limit (即bits.Len(length))时,切换到堆排序
pdqqsort – version1
- 关于短序列(≤24)咱们运用刺进排序
- 其他状况,运用快速排序(挑选收割元素作为pivot)来确保全体功能
- 当快速排序体现欠安时(limit==0),运用堆排序来确保最坏状况下时刻复杂度依然为O(n*logn)
问题:
怎么让pdqsort速度更快?
- 尽量使得QuickSort的pivot为序列的中位数 -> 改进choose pivot
- Partition速度更快 -> 改进partition,可是此优化在Go体现欠好,略
pdqsort – version2
考虑关于pivot的挑选
-
运用首个元素作为pivot (最简单的方案 )
完成简单,可是作用欠好,例如在sorted状况下功能很差
-
遍历数组,寻觅真实的中位数
遍历比对价值很高,功能欠好
寻觅近似
-
依据序列长度不同没来决议挑选战略
-
优化 – Pivot的挑选
- 短序列(≤8)挑选固定元素
- 中序列(≤50)采样三个元素,寻觅中位数
- 长序列(>50)采样九个元素,寻觅中位数
-
pivot采样方法是咱们有探知不知道序列当时状况的能力!
- 采样的元素都是逆序摆放 → 序列或许现已逆序 → 回转整个序列
- 采样的元素都是顺序摆放 → 序列或许现已有序 → 运用刺进排序(刺进排序实践运用
partiallnsertionSort
,即有限次数的刺进排序)
Version1升级到version2优化总结
- 升级pivot挑选战略(近似中位数)
- 发现序列或许逆序,则翻转序列 → 应对reverse场景
- 发现序列或许有序,运用有限刺进排序 → 应对sorted场景
还有什么场景咱们没有优化?
-
短序列状况
- 运用刺进排序(v1)
-
极点状况
- 运用堆排序确保算法的可行性(v1)
-
完全随机的状况(random)
- 更好的pivot挑选战略(v2)
-
有序/逆序的状况(sorted/reverse)
- 依据序列状况 翻转或者刺进排序(v2)
-
元素重复度较高的状况(mod8) → ?
怎么优化重复元素许多的状况?
-
采样pivot的时候检测重复度?
不是很好因为采样数量有限,不一定能采样到相同元素
-
解决方案:
- 假如两次partition 生成的pivot相同,即partition进行了无效切割,此刻认为pivot的值为重复元素(相比上一种有更高的采样率)
pdqsort – final version
-
优化 – 重复元素较多的状况(partitionEqual)
- 当检测到此刻的
pivot
和上次相一起(产生在leftSubArray
),运用partitionEqual
将重复元素摆放在一起,削减重复元素关于pivot
挑选的搅扰
- 当检测到此刻的
-
优化-当pivot挑选战略体现欠安时,随机交流元素
- 防止一些极点状况使得
QuickSort
总是体现欠安,以及一些黑客进犯状况
- 防止一些极点状况使得
排序 | Best | Avg | Worst |
---|---|---|---|
InsertionSert | O(N) | O(N2) | O(N2) |
QuickSort | O(N*logN) | O(N*logN) | O(N2) |
HeapSort | O(N*logN) | O(N*logN) | O(N*logN) |
pdqSort | O(N) | O(N*logN) | O(N*logN) |
四、问题
高功能排序算法是怎么规划的
依据不同状况挑选不同战略,扬长避短
生产环境中运用的的排序算法和课本上的排序算法有什么区别?
理论算法注重理论功能,例如时刻、空间复杂度等。生产环境中的算法需要面对不同的实践场景,愈加注重实践功能
Go语言(<= 1.18)的排序算法是快速排序么?
实践一直是混合排序算法,主体是快速排序。Go ≤ 1.18时的算法也是根据快速排序,和pdqsort的区别在于fallback时机、pivot 挑选战略、是否有针对不同pattern优化等
\