三种线性排序算法: 计数排序、桶排序与基数排序

[非基于比较的排序]

在计算机科学中,排序是一门基础的算法技术,许多算法都要以此作为基础,不同的排序算法有着不同的时间开销和空间开销。排序算法有非常多种,如我们最常用的快速排序和堆排序等算法,这些算法需要对序列中的数据进行比较,因为被称为基于比较的排序。

而非基于比较的排序,如计数排序,桶排序,和在此基础上的基数排序,则可以突破O(NlogN)时间下限。但要注意的是,非基于比较的排序算法的使用都是有条件限制的,例如元素的大小限制,相反,基于比较的排序则没有这种限制(在一定范围内)。但并非因为有条件限制就会使非基于比较的排序算法变得无用,对于特定场合有着特殊的性质数据,非基于比较的排序算法则能够非常巧妙地解决。

[计数排序]

首先从计数排序(Counting Sort)开始介绍起,假设我们有一个待排序的整数序列A,其中元素的最小值不小于0,最大值不超过K。建立一个长度为K的线性表C,用来记录不大于每个值的元素的个数。

算法思路如下:
1. 扫描序列A,以A中的每个元素的值为索引,把出现的个数填入C中。此时C[i]可以表示A中值为i的元素的个数。
2. 对于C从头开始累加,使C[i]=C[i]+C[i-1]。这样,C[i]就表示A中值不大于i的元素的个数。
3. 从尾部往头扫描,则A[i]应该放在B[count[A[i]]-1]处,(减1因为C[i]也包括了自己),然后count[A[i]]–;

#include<iostream>
#include<vector>
using namespace std;


void countingSort(vector<int> &A, int maxKey){
    vector<int> count(maxKey + 1, 0);

    for (int i = 0; i < A.size(); i++){
        count[A[i]]++;
    }

    for (int i = 1; i < count.size(); i++){
        count[i] += count[i - 1];
    }

    vector<int> B(A.size());

    /*下面的操作是为了让排序是稳定的,否则不需要*/
    for (int i = A.size() - 1; i >= 0; i--){
        B[count[A[i]] - 1] = A[i];
        count[A[i]]--;
    }

    A = B;
}


int main(){
    vector<int> A = { 3, 5, 8, 9, 1, 2 };

    countingSort(A, 9);

    for (int n : A){
        cout << n << " ";
    }

    cout << endl;

    return 0;
}

可以发现计数是一种稳定排序算法;

计数排序的时间复杂度为O(N+K),空间复杂度为O(N+K)。当K不是很大时,这是一个很有效的线性排序算法。

[桶排序]

同计数排序一样,桶排序也对待排序序列作了假设,桶排序假设序列由一个随机过程产生,该过程将元素均匀而独立地分布在区间[0,1)上。基本思想是:把区间[0,1)划分成M个相同大小的子区间,称为桶。将n个记录分布到各个桶中去。如果有多于一个记录分到同一个桶中,需要进行桶内排序。最后依次把各个桶中的记录列出来记得到有序序列。

当然,也不需要一定要在[0,1)之间。如果元素的最小值不小于0,最大值不超过K。假设我们有M个桶,第i个桶Bucket[i]存储iK/M至(i+1)K/M之间的数。

注意:只要数据和桶之间有一个映射函数即可,不一定非要上面的形式。

参考:http://hxraid.iteye.com/blog/647759

#include<iostream>
#include<vector>
using namespace std;

struct node{
    int val;
    node* next;
    node(int val) :val(val), next(NULL){}
};

void bucketSort(vector<int> &A, int maxKey,int numOfBucket){
    vector<node*> B(numOfBucket);
    for (int i = 0; i < numOfBucket; i++){
        B[i] = new node(0);
    }

    for (int i = 0; i < A.size(); i++){
        node* u = new node(A[i]);

        int key = A[i] / numOfBucket;

        if (B[key]->next == NULL){
            B[key]->next = u;
        }
        else{
            //插入排序
            node* p = B[key];
            //要用p->next
            while (p->next!=NULL && p->next->val < u->val){
                p = p->next;
            }

            node* tmp = p->next;
            p->next = u;
            u->next = tmp;
        }
    }

    int i = 0;
    for (int j = 0; j < numOfBucket; j++){
        node *p = B[j]->next;
        while (p){
            A[i++] = p->val;
            p = p->next;
        }
    }

}


int main(){
    vector<int> A = { 3, 5, 8, 9, 1, 2 };

    bucketSort(A, 9, 4);

    for (int n : A){
        cout << n << " ";
    }

    cout << endl;

    return 0;
}

当记录在桶中分布均匀时,即每个桶只有一个元素,此时时间复杂度o(n)。空间复杂度也是O(n)。
算法导论证明只要就算不是均匀分布,只要桶数目的平方根元素个数成比例,时间复杂度就是O(n)。

桶排序在海量数据中的应用

1.

一年的全国高考考生人数为500 万,分数使用标准分,最低100 ,最高900 ,没有小数,你把这500 万元素的数组排个序。

分析:对500W数据排序,如果基于比较的先进排序,平均比较次数为O(5000000*log5000000)≈1.112亿。但是我们发现,这些数据都有特殊的条件: 100=<score<=900。那么我们就可以考虑桶排序这样一个“投机取巧”的办法、让其在毫秒级别就完成500万排序。

方法:创建801(900-100)个桶。将每个考生的分数丢进f(score)=score-100的桶中。这个过程从头到尾遍历一遍数据只需要500W次。然后根据桶号大小依次将桶中数值输出,即可以得到一个有序的序列。而且可以很容易的得到100分有?人,501分有?人。

2.

题目:在一个文件中有 10G 个整数,乱序排列,要求找出中位数。内存限制为 2G。只写出思路即可(内存限制为 2G的意思就是,可以使用2G的空间来运行程序,而不考虑这台机器上的其他软件的占用内存)。

思想:将整形的每1byte作为一个关键字,也就是说一个整形可以拆成4个keys,而且最高位的keys越大,整数越大。如果高位keys相同,则比较次高位的keys。整个比较过程类似于字符串的字典序。
假设是unsigned int.

  1. 第一步:把10G整数每2G/4读入一次内存,然后一次遍历这些个数据。每个数据用位运算”>>”取出最高8位(31-24)。这8bits(0-255)最多表示256个桶,那么可以根据8bit的值来确定丢入第几个桶。最后把每个桶写入一个磁盘文件中,同时在内存中统计每个桶内数据的数量。

  2. 第二步:根据内存中256个桶内的数量,计算中位数在第几个桶中。

  3. 第三步:继续以内存中的整数的次高8bit进行桶排序(23-16)。过程和第一步相同,也是256个桶。

  4. 第四步:一直下去,直到最低字节(7-0bit)的桶排序结束。我相信这个时候完全可以在内存中使用一次快排就可以了。

[基数排序]

上述的基数排序和桶排序都只是在研究一个关键字的排序,现在我们来讨论有多个关键字的排序问题。

假设我们有一些二元组(a,b),要对它们进行以a为首要关键字,b的次要关键字的排序。我们可以先把它们先按照首要关键字排序,分成首要关键字相同的若干堆。然后,在按照次要关键值分别对每一堆进行单独排序。最后再把这些堆串连到一起,使首要关键字较小的一堆排在上面。按这种方式的基数排序称为MSD(Most Significant Dight)排序。

第二种方式是从最低有效关键字开始排序,称为LSD(Least Significant Dight)排序。首先对所有的数据按照次要关键字排序,然后对所有的数据按照首要关键字排序。要注意的是,使用的排序算法必须是稳定的,否则就会取消前一次排序的结果。由于不需要分堆对每堆单独排序,LSD方法往往比MSD简单而开销小。下文介绍的方法全部是基于LSD的。

常用于整数排序:

将所有待比较数值(注意,必须是正整数)统一为同样的数位长度,数位较短的数前面补零.
然后, 从最低位开始, 依次进行一次稳定排序(计数排序算法, 因为每个位可能的取值范围是固定的从0到9).
这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列.

#include<iostream>
#include<vector>
#include<cmath>
using namespace std;

void countingSort(vector<int> &A, int maxKey, int d){
    vector<int> count(maxKey + 1, 0);

    vector<int> dVal(A.size());

    for (int i = 0; i < A.size(); i++){
        dVal[i] = (A[i] / int(pow(10, d - 1))) % 10;
    }

    for (int i = 0; i < A.size(); i++){
        count[dVal[i]]++;
    }

    for (int i = 1; i < count.size(); i++){
        count[i] += count[i - 1];
    }

    vector<int> B(A.size());

    /*下面的操作是为了让排序是稳定的,否则不需要*/
    for (int i = A.size() - 1; i >= 0; i--){
        B[count[dVal[i]] - 1] = A[i];
        count[dVal[i]]--;
    }

    A = B;
}

void radixSort(vector<int> &A, int maxD){
    for (int d = 1; d < maxD; d++){
        countingSort(A, 9, d);
    }
}


int main(){
    vector<int> A = { 114, 118, 152, 114, 111, 132 };

    radixSort(A, 3);

    for (int n : A){
        cout << n << " ";
    }

    cout << endl;

    return 0;
}

参考:https://www.byvoid.com/blog/sort-radix

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值