模板:排序(五)

这大概是最玄学的了……所以咱们来认真讲一下?(前面几张点击量不高啊……也许大家真的对简单排序太习以为常了吧……我还是太蒟蒻了)虽然真正OI的时候应该只能用到的最快的基数排序吧。
提前说明,参考资料:算法导论,百度百科,基数排序的性能优化
htt(和谐)p://bl(和谐)og.csdn.net/(和谐)yutianzuijin/art(和谐)icle/details/22876017
————————分割线————————
故事在那之后,就没了后文,听说后来勇者真的得到了一个int的G……硬盘!
然后勇者就一脸赌气的走了。
到这里应该结束了,但是民间有传,这个故事还有后文……

————————分割线————————

桶排序

Sp厂子:遥远的地球订购了70亿的钢材,所幸的是,地球人只喜欢钢材长度为0-100。所以你有足够的时间来排序……前提是他们中的人能够活着等到货物送过来。你有好几个操场那么大的地方可供你开桶。
————————分割线————————
由于勇者的脑细胞不足以完成sp厂子了,所以就由路由器来解释一下吧。
这里咱们来说一下咱们刚开始对于(一)中桶排的神奇的另一种形式。
接下来,你会看到真正的桶排。

#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<iostream>
using namespace std;
const int MAXN=1000;//别忘记根据实际情况修改MAXN 
const int INF=99999999;
int N=10000;//这里面看情况取N 
int b[10001][22001]={0},c[10001]={0},a[10000010];
bool cmp(int a,int b){
    if(a>b)return 1;
    return 0;
}
int main(){
    int n,maxn=-INF,minn=INF;
    scanf("%d",&n);//保证n<=MAXN 
    if(n<N)N=n;
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);//保证0<=a[i]<=MAXN
        int k=a[i]*N/MAXN;
        if(k>maxn)maxn=k;
        if(k<minn)minn=k;
        c[k]++;
        b[k][c[k]]=a[i];
    }
    for(int i=maxn;i>=minn;i--){
        sort(b[i]+1,b[i]+c[i]+1,cmp);
        for(int j=1;j<=c[i];j++){
            printf("%d ",b[i][j]);
        }
    }
    return 0;
}

其中的取值范围为0-MAXN
实际上,从代码中,已经很容易明白了这个桶排的实质:将数组分成若干份,对每份sort再输出。
然而事实上,这样做的速度,n<=10000000时,经过测试其实和快排的速度不相上下。
所以,这个算法看看就好……真正能用到这个算法的是这道题。
当我们知道了MAXN的数值以及当MAXN的数值十分的小的时候且数据量巨大的时候,桶排序就有了极大的优越性……然而我没有办法证明,因为我的电脑承受不了这样大的数据量。

————————分割线————————

计数排序

然后我们来看一个线性排序,那就是计数排序。

计数排序的思想,就是找到一个数,然后知道比这个数小的有几个,假设是n个吧,那么这个数就应该被放在n+1的位置上。
这个思想适用于没有重复数据的基础上,但是如果有呢?

也很简单,我们统计出重复数据的最后一个数据应该放在那里,然后按顺序依次放下。
至于代码实现,我们扫一遍数组,然后将数组的名字看做数字,例如我们有一个数n,那么c[n]++,这样我们就先统计出了所有的重复数据。

然后,我们有这样的递推式。
c[i]+=c[i-1];

这样,我们就能知道每一个数字应该放的位置了。(想一想,蛮简单知道为什么的吧!)
好吧我还是解释一下,假设我们有c[0]=2,c[1]=0,c[2]=1

然后我们就知道了0只能放在1,2位置上,虽然c[1]在那之后=2但是在我们扫的时候没有1所以不用管,之后c[2]=3,知道2应该放在3的位置。以此类推……
好的,上代码。

#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<iostream>
using namespace std;
int a[100000],b[100000]={0},c[100000]={0}; 
int main(){
    int n,MAXN=-1;
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);//保证大于等于0 
        c[a[i]]++;
        if(a[i]>MAXN)MAXN=a[i];
    }
    for(int i=1;i<=MAXN;i++){
        c[i]+=c[i-1];
    }
    for(int i=n;i>=1;i--){
        b[c[a[i]]]=a[i];
        c[a[i]]--;
    }
    for(int i=n;i>=1;i--)printf("%d ",b[i]);
    return 0;
}

————————分割线————————

好的,开胃菜结束,桶排唯一要记的,恐怕也只有其思想与(一)那个简单桶排了,计数排序也很少见,如果认为桶排占空间的话……额这个耗空间也蛮大的。

接下来,我们要见证几件神奇的排序,见证神奇的WC2017神奇的排序中所基于的一种线性排序(其实说实话,我还真不知道WC2017那个神奇的排序的实现是什么,不过看有些大牛的博客说好像是基数排序+归并……等等我好像剧透了什么)

是的是

基数排序

解释

基数排序的原理,请查看百度百科或者其他博客,因为这玩应只有看图片才能好好理解……

好吧我还是来文字表述了(要不然多没意思)。
有一组数据
28
26
24
49
37
39
91
我们先按照个位数排序得到(完全不用管十位数)(注意,如果遇到数相同的,我们按照上一步排序的顺序来排先后)
91
24
26
37
28
49
39
然后再根据十位数排序(同理)我们惊奇的得到了
24
26
28
37
39
49
91
完全有序了。
这个算法需要桶排序的思想,所以说我们率先交代了桶排序算法。好的废话不多说上代码吧。

没经过改进的代码

#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<iostream>
using namespace std; 
int pai[10][10000]={0},b[10]={0},ji[7]={1,1,10,100,1000,10000,100000}; 
int a[100000];
int fen(int n,int wei){
    return (n/ji[wei])%10;
}
int main(){
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
    }
    for(int i=1;i<=6;i++){//循环次数视情况而定 
        memset(pai,0,sizeof(pai));
        memset(b,0,sizeof(b));
        for(int j=1;j<=n;j++){
            int p=fen(a[j],i);
            b[p]++;
            pai[p][b[p]]=a[j];
        }
        int l=0;
        for(int j=0;j<=9;j++){
            for(int k=1;k<=b[j];k++){
                l++;
                a[l]=pai[j][k];
            }
        }
    }
    for(int i=1;i<=n;i++){
        printf("%d ",a[i]);
    }
    return 0;
}

然而排序的速度相比较于快排来说速度仍然不相上下啊,这可怎么办?
————————分割线————————

优化

不要着急,还记得我们神奇的常数优化吗?因为这个整个运算当中,fen这个函数调用的次数最多,所以我们把fen优化了,就等于减少时间。
(注意,以下讲的优化方面只是对上面我所提到并参考的博客的剪取加精炼加解释,所以如果真的想要让这个算法的速度快到天际,请看一下那个博客)
一.我们运用了ji数组,我们将基数提前求出来,减少了大量的乘法运算,这个在上面的代码里已经提到了。

二.然后呢?我们的基数取得不是很好。10的话我们的循环次数就会变多,而如果我们的基数变成1000,以空间换时间,那么我们就能够减少循环次数来减少时间。
然而,速度还不够快:虽然已经在长度很大的时候我们的速度已经大于快排了。

三.接下来,我们再分析一下fen的函数,我们发现,我们还有一个除法运算没有优化。
这个也很好解决,我们同样预处理,将1/ ji[wei]提前求出来,然后调用。
(因为改动不多,我就不贴代码了)

至此,我们的速度已经大幅度提升,然而,我们还有一个最关键的运算符没有优化,那就是%%%%%%%%%%%%%%%%%%%%%%%
怎么优化呢???????????
路由器参考了那个博客,然后猛拍一脑袋!
其实刚才的优化都没有必要。

四.我们只需要让2的幂来作基数不就可以了吗?(当然实际上我们用的是1024,原因同上)
我们完全可以用位运算与&来算fen,从而真正达到优化常数的目的!

我们知道,一个数除以2的m次方可以用N>>m表示。
而且(我也不知道为什么)我们有n%a(n一定为正整数)=n&(a-1)
这是最神奇的。

那么我们套上去吧就行了。
再然后,我们可以考虑一下减少空间使用。我们一定得用桶解决吗?这个问题,完全可以计数排序搭配。如果我们用计数排序的话……也就是说,将基数相同的数看成一个数,用一遍计数排序,然后加上刚开始我们排序的种种规则(也就是正序放倒序出),同样也能得到我们想要的结果。

此时我们的速度已经远远大于了神奇的快速排序,独领风骚了。

————————————新更新分界线——————————————————————
然而事实上我们所常用的基数不是1024,。
一般排序为65536.
松排为256.
原因与L1缓存有关。
具体是为什么也就不讲了。
毕竟和编程就没关系了。

优化代码

附上代码(256松排基数排序)

#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<iostream>
using namespace std;
const int rdi=256;//基数 
const int maxn=100001; 
int ji[5]={0,8,16,24,32}; //2^ji[1]=rdi,ji[i]=ji[i-1]*2;
inline int fen(int n,int wei){
    return (n>>ji[wei])&(rdi-1);
}
int a[maxn];
int b[maxn],c[rdi];
void radix_sort(int n){
    for(int i=0;i<=3;i++){//循环次数视情况而定 
        for(int j=0;j<rdi;j++)c[j]=0;
        for(int j=0;j<n;j++)c[fen(a[j],i)]++;
        for(int j=1;j<rdi;j++)c[j]+=c[j-1];
        for(int j=n-1;j>=0;j--)b[--c[fen(a[j],i)]]=a[j];
        for(int j=0;j<n;j++)a[j]=b[j];
    }
    for(int i=0;i<n;i++){
        printf("%d ",a[i]);
    }
    return;
}
int main(){
    int n;
    scanf("%d",&n);
    for(int i=0;i<n;i++){
        scanf("%d",&a[i]);
    }
    radix_sort(n);
    return 0;
}

至此,很多排序算法已经讲完,还有神奇的堆排序与希尔排序了……然而其实这篇文章内的东西都是最近几天自学的……这两个算法也还没有看呢,但是我打算先告一段落了。等到哪天看完,你们可能会看到(六)哦!

posted @ 2017-05-10 21:38  luyouqi233  阅读(160)  评论(0编辑  收藏  举报