这是我参与「第三届青训营-后端场」笔记创作活动的第12篇笔记。
1. 为什么要学习数据结构和算法
例子-抖音直排行榜功能
规则:某个时间段内,直播间礼物数top10房间展示排行榜。
解决方案:
- 礼物数量存储在Redis-zest中,使用skiplist使得元素整体有序。
- 使用Redis集群,避免单击压力过大,使用主从算法、分片算法。
- 保证集群原信息的稳定,使用一致性算法。
- 后端使用LRU缓存算法降低Redis压力,展示房间排行榜。
数据结构和算法几乎存在程序开发的所有地方。
当前最快的排序算法:
- python:timsort
- C++:introsort
- rust:pdqsort
2. 经典排序算法
| 插入排序 | 快速排序 | 堆排序 | |
|---|---|---|---|
| best | O(n) | O(nlogn) | O(nlogn) |
| avg | O(n^2) | O(nlogn) | O(nlogn) |
| worst | O(n^2) | O(n^2) | O(nlogn) |
- 插入排序平均和最坏情况时间复杂度都是O(n^2),性能不好
- 快速排序整体性能处于中间层次
- 堆排序稳定,一直保持O(nlogn)的复杂度
根据序列元素的排列情况划分:
- 完全随机的情况(random)
- 有序/逆序的情况(sorted/reverse)
- 元素重复度比较高的情况(mod8) 再次基础上,还需要根据序列长度划分(16/128/1024)
随机顺序的数组根据长短划分,我们有以下结论:
- 短序列插入排序最快
- 快速排序在其他情况中速度最快
- 对排序速度与最快算法差距不大
有序数组插入排序最快。
3. 从零开始打造pdqsort
version1
pdqsort(pattern-defeating-quicksort)是一种不稳定的混合排序算法。他的不同版本在C++、BOOST、Rust以及Go(>=1.19)中。他对常见的序列类型做了特殊的优化,使得在不同条件下都拥有不错的性能。
实现方法:
- 对于短序列(一般是小于24)使用插入排序。
- 其他情况使用快排保证整体性能。
- 当快排表现不佳时(设置limit,每次划分出的两个子序列长度差距大,有一边小于整体长度的1/8则limit--,limit==0时认为表现不佳)使用对排序保证最坏情况下也有O(nlogn)的复杂度。
如何优化?
- 尽量使快排划分的子序列长度相等->改进选择划分基准的方法
- 优化划分速度->改进划分函数(后续略)
version2
如何选择划分基准?
- 使用首个元素作为划分(最简单方案):实现简单,效果往往不好,在有序情况下表现很差。
- 遍历数组,寻找中位数:遍历代价高导致性能不好。
方案:寻找近似中位数
- 短序列(<=8),选择固定位置元素
- 中序列(<=50),采样3个元素取中位数
- 长序列(>50),采样9个元素
采样之后如果采样的元素都是逆序排列,可以假设整体逆序,反转整个序列。如果都是顺序可以假设已经有序,直接使用插入排序。使用插入排序时如果发现交换次数达到某一阈值则停止插入,重新使用快排。
如何优化?
- 短序列直接插入排序(v1)
- 极端情况使用堆排序保证可行性(v1)
- 完全随机情况更好的划分(v2)
- 有序/逆序情况处理(v2)
- 元素重复度高的情况?
final version
如何优化重复元素很多的情况?
采样时检测重复效果不好,因为采样数量有限,不一定能采样到相同的元素
解决方法:如果两次huafen生成的pivot相同,即partition进行了无效分割,此时认为pivot的值为重复元素(采样率较高)
优化-重复元素多的情况:当检测到此时的pivot和上次相同时(发生在左子序列)将重复的元素排列到一起,减少重复元素对于pivot选择的干扰。
优化-当pivot选择策略表现不佳时,随机交换元素避免极端情况或黑客攻击的情况。
4.作业
实现pdqsort的v1版本(C++代码)
#include<iostream>
using namespace std;
template<typename T>
void insert_sort(T* elems, int begin, int end){
for(int i = begin; i < end; i++){
for(int j = i; j > begin; j--){
if(elems[j] < elems[j - 1]){
T tmp = elems[j];
elems[j] = elems[j - 1];
elems[j - 1] = tmp;
}
}
}
}
template<typename T>
void heap_init(T* elems, int begin, int end){
int num = end - begin;
for(int i = num-1; i > -1; i--){
if(2 * i + 1 < num && elems[begin + i] < elems[begin + 2 * i + 1]){
T tmp = elems[begin + 2 * i + 1];
elems[begin + 2 * i + 1] = elems[begin + i];
elems[begin + i] = tmp;
}
if(2 * i + 2 < num && elems[begin + i] < elems[begin + 2 * i + 2]){
T tmp = elems[begin + 2 * i + 2];
elems[begin + 2 * i + 2] = elems[begin + i];
elems[begin + i] = tmp;
}
}
}
template<typename T>
void heap_sort(T* elems, int begin, int end){
for(int i = end; i > begin; i--){
heap_init(elems, begin, i);
T tmp = elems[i - 1];
elems[i - 1] = elems[begin];
elems[begin] = tmp;
}
}
template<typename T>
void pdqsort_v1(T* elems, int begin, int end, int limit){
if(end - begin <= 24){
insert_sort(elems, begin, end);
return;
}
if(limit == 0){
heap_sort(elems, begin, end);
return;
}
int l = begin, r = end - 1;
T pivot = elems[begin];
while(l<r){
while(l<r && elems[r]>=pivot) --r;
if(l<r) elems[l] = elems[r];
while(l<r && elems[l]<=pivot) ++l;
if(l<r) elems[r] = elems[l];
}
elems[l] = pivot;
int oneOFeight = (end-begin)>>3;
if(l < begin + oneOFeight || l > end - oneOFeight) --limit;
pdqsort_v1(elems, begin, l, limit);
pdqsort_v1(elems, l + 1, end, limit);
}
int main(){
int e[100];
for(int i = 0; i < 100; i++){
e[i] = rand()%1000;
}
// for(auto i:e){
// cout<<i<<endl;
// }
// cout<<endl;
pdqsort_v1(e,0,100,3);
for(int i=1;i<100;++i){
if(e[i]<e[i-1]) cout<<"error"<<endl;
}
return 0;
}
文件保存为test.cpp,终端运行g++ test.cpp -o pdq,./pdq即可