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

187 阅读4分钟

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

1. 为什么要学习数据结构和算法

例子-抖音直排行榜功能

规则:某个时间段内,直播间礼物数top10房间展示排行榜。

解决方案:

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

数据结构和算法几乎存在程序开发的所有地方。

当前最快的排序算法:

  • python:timsort
  • C++:introsort
  • rust:pdqsort

2. 经典排序算法

插入排序快速排序堆排序
bestO(n)O(nlogn)O(nlogn)
avgO(n^2)O(nlogn)O(nlogn)
worstO(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即可