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

130 阅读3分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天

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

场景:某个时间段内,直播间礼物数TOP10房间获得奖励,需要在每个房间展示排行榜

  • 礼物数量存储在Redis-zset中,使用skiplist 使得元素整体有序
  • 使用Redis集群,避免单机压力过大
  • 使用主从算法、分片算法
  • 保证集群原信息的稳定,使用一致性算法
  • 后端使用缓存算法(LRU)降低redis压力,展示房间排行榜

二、经典排序算法

1.插入排序

  • 概念:将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表
  • 时间复杂度
bestavgworst
O(n)O(n^2)O(n^2)
  • 对于少量元素的排序是一个有效的算法

2. 快速排序

  • 概念:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以 递归 进行,以此达到整个数据变成有序 序列
  • 时间复杂度
bestavgworst
O(nlogn)O(nlogn)O(n^2)
  • 主要是要选择正确的轴点

3.堆排序

  • 概念:构造一个大顶堆,利用堆进行排序
  • 时间复杂度
bestavgworst
O(nlogn)O(nlogn)O(nlogn)
  • 性能稳定

三、从零打造pdqsort

三种排序算法实际场景的表现?

  • 所有短序列和元素有序情况下,插入排序性能最好
  • 在大部分的情况下,快速排序有较好的综合性能
  • 几乎在任何情况下,堆排序的表现都比较稳定 能否综合设计一种更优秀的算法呢?

1.pdqsort简介(pattern- defeating-quicksort)

它是一种不稳定的混合排序算法,它的不同版本被应用在C++,BOOST、Rust以及Go 1.19中。它对常见的序列殊的优化使得在不同条件下都拥有不错的性能

结合三种排序方法的优点

  • 对于短序列(小于一 定长度)我们使用插入排序
  • 其他情况,使用快速排序来保证整体性能
  • 当快速排序表现不佳时,使用堆排序来保证最坏情况下时间复杂度仍然为O(n*logn)
  • 短序列的具体长度是多少呢? 12~32,在不同语言和场景中会有不同,在泛型版本根据测试选定24

  • 如何得知快速排序表现不佳,以及何时切换到堆排序? 当最终枢轴的位置离序列两端很接近时(距离小于length/8)判定其表现不佳,当这种情况的次数达到limit (即bits.Len(length)) 时,切换到堆排序

2.version1

pdsort-v1.drawio.png

  1. 对于短序列(len<=24) 我们使用插入排序
  2. 其他情况,使用快速排序(选择首个元素作为枢轴)来保证整体性能
  3. 当快速排序表现不佳时(limit==0) ,使用堆排序来保证最坏情况下时间复杂度仍然为O(n*logn)

3.version2

思考关于pivot的选择

  • 使用第一个数?实现简单,但是往往效果较差
  • 遍历寻找中位数? 遍历的代价较高,性能较差 重点是平衡寻找pivot的开销和pivot带来的效果优化!!

解决策略

  • 短序列(<=8),选择固定元素
  • 中序列(<=50),采样三个元素
  • 长序列(>50),采样九个元素 同时,采样的结果会带来另一种优化的方法
  • 采样的元素都是逆序排列,序列可能已经逆序,翻转整个序列
  • 采样的元素都是顺序排列,序列可能已经有序,使用插入排序

4.version3

考虑重复度较高的问题

当检测到此时的pivot和上次相同时(发生在leftSubArray),使用partitionEqual将重复元素排列在一起,减少重复元素对于pivot选择的干扰

快排性能不佳,随机交换元素

避免一些极端情况使得快速排序总是表现不佳,以及一些黑客攻击情况