数据结构与算法 | 青训营笔记

87 阅读4分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第一篇笔记。

数据结构与算法

为什么要学习数据结构与算法

  • 几乎存在于程序开发中的所有地方 例子:抖音直播排行榜(在某个时间段内,直播间里无数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)

根据序列元素排列情况进行划分:

  1. 完全随机的情况:插入排序在短序列中最快,其他情况下,快排最快,但是堆排序和其差距不是特别大
  2. 有序/逆序的情况:在完全有序的情况下,插入排序是最快的,快排是最慢的,堆排序表现比较稳定。
  3. 元素重复度较高的情况

在此基础上根据序列的长度进行划分(eg:随机情况)

  1. 短序列:16的情况,插入最快
  2. 中序列:128的情况,快排最快,堆排序和其差距并不大
  3. 长序列: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选择的干扰。

image.png

image.png