每日算法计数&基数&桶&位图排序-简介

Posted jiange_zh

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了每日算法计数&基数&桶&位图排序-简介相关的知识,希望对你有一定的参考价值。

在前面的文章中,我们介绍的都是基于比较的排序。

对于比较排序,对含n个元素的序列进行排序,在最坏情况下都要用O(n logn)次比较(归并排序和堆排序是渐近最优的)。

本文将继续介绍以线性时间运行的排序算法,他们使用的是非比较排序,因此下界O(n logn)对它们不适用。

计数排序

想象下面这种情况:

一个班有k个人,需要排成一条纵队,地面上已经用粉笔按从小到大的顺序标明了1到k个号码,要求按身高从低到高排列,也就是说,最高的站在标号为k的位置,最矮的站在标号为1的位置。

那么对于每个人,如何知道自己的位置呢?

假如每个人都知道所有人的身高,那么他只需要数一数比自己矮的有多少个,比如10个,那么他就可以很自觉地站到标号为11的位置了。

注意:如果有身高相等的情况,我们需要改变下策略:每个人数一数比自己矮的以及和自己身高一样的人数(包括自己),假如有2个人身高相等,一开始他们得到的计数为8,于是一个人开始站到标号为8的位置,那么另一个人应该站到标号为7的位置(也就是说他的计数值应该减1)。

当每个人都站好之后,整个队就按身高从低到高排好序了。

计数排序假设n个输入元素都是0到k之间的整数,基本思想是对每个元素x,确定出小于x的元素个数,之后便可以将x直接放到合适的位置。(需注意元素相等的情况)

为完成计数排序,我们需要三个数组:输入数组a[0…n-1],排序结果数组b[0…n-1],计数数组c[0…k]。

算法步骤如下:

  1. 初始化c[0…k]为0;
  2. 对于每个元素a[i], c[ a[i] ]++,此时c记录a中各个元素出现的次数,比如{1, 1, 3}中,c[1] = 2, c[2] = 0, c[3] = 1;
  3. 对于i=1 to k, c[i] = c[i] + c[i-1],此时c记录小于等于i的元素的个数,c[1] = 2, c[2] = 2, c[3] = 3;
  4. 按照计数结果存放元素到b中:对i = n-1 to 0,b[ c[ a[i] ] ] = a[i], c[ a[i] ]–;

第四步有点绕,我们一步步看: a[i]表示第i个待排元素, c[ a[i] ]表示小于等于第i个待排元素的元素个数,也就是说,第i个待排元素应该放置的位置。之后c[ a[i] ]–,因为a[i]已经放到正确的位置上了,这一个语句使得下一个值等于a[i]的待排元素放置到a[i]的前一个位置,这就解决了相等元素的问题。(同时注意到i是逆序的,它保证了计数排序的稳定性,假如是正序的,以{1,1,3}为例,那么第一个1将放置到第二个1后面,不稳定)

代码实现:

void countingSort(int a[], int b[], int size, int k)
{
    if (NULL == a || size <= 0)
        return ;
    int *c = new int[k+1];
    if (NULL == c)
    {
        cerr << "new error" << endl;
        return ;
    }
    memset(c, sizeof(c), 0);
    for (int i = 0; i < size; ++i)
        ++c[ a[i] ];
    for (int i = 1; i <= k; ++i)
        c[i] += c[i-1];
    for (int i = size-1; i >=0; --i)
        b[ c[ a[i] ]-- ] = a[i];
}

计数排序花费的总的时间为O(k+n),当k = O(n)时,运行时间为O(n)。

可以看到,计数排序的效率是很高的,但是其局限性也高:排序的元素必须是【正整数】,且由于需要辅助数组c[0…k],所以元素的范围不能太大。

基数排序

基数排序就很有意思了,它最初是用在老式穿卡机上的算法。

先举个扑克牌的例子……

我们要对一副扑克牌排序,可以这样做:先不管牌的大小,先对花色(比如红桃、黑桃、方块、梅花)排序,之后再根据牌的大小排序,于是一副牌就按花色、大小排好了。

对于序列:{329,457,657,839,436,720,355},基数排序的工作原理如下:

先按最低位排序:

720
355
436
457
657
329
839

再按次低位排序:

720
329
436
839
355
457
657

最后按最高位排序:

329
355
436
457
657
720
839

需要强调的一点:每一次按位排序都要是稳定的。

伪代码:

void radixSort(int arr[], int d) //d为最高位,1为最低位
{
    for (int i = 1; i <= d; ++i)
        use a stable sort to sort array on digit i;
}

以上的基数排序是最低位优先(Least Significant Digit first)的,简称LSD法,同样还有最高位优先(Most Significant Digit first)法,简称MSD法,它从最高位开始排序。

**以上代码考虑的是正整数,在实际应用中,可能需要处理负数的情况。

基数排序的时间复杂度取决于选择哪种稳定的中间排序算法。

桶排序

桶排序就很好理解了,其思想是:假设所有元素均匀分布在区间[0,1)上,将该区间划分成n个相同大小的子区间(称为桶),之后,将相应的元素放到对应范围的桶里面,对各个桶里的元素进行排序,最后按次序把各个桶里的元素列出来即可。

桶排序的期望运行时间为O(n)。

以上只是最基本的桶排序思想,在实际应用中,考虑到实际数据的情况,需要做相应的调整。(比如桶的个数、桶之间是否平均分配等等),有兴趣的读者可以自行深入学习一下(对于并行算法感兴趣的朋友,可以看一看基于桶排序的并行化算法Sample Sort)。

位图排序

位图排序通常并不作为一种排序算法列出来,因为它的局限性很强。不过在这里我仍然想提一下这个优秀的想法,因为没有哪一个算法是全能的,更多的时候我们应该根据具体的问题来选择最适合的算法,而不是一味地追求所谓的最快or最通用的算法。

关于位图排序,我之前写过一遍博文介绍过,这里不赘述,请有兴趣的读者移步:

【编程珠玑-读书笔记】用位图解决排序问题–仔细分析问题的重要性

到此为止,我们的排序算法将告一段落了,后面我将就排序算法写一篇文总结一下,除了目前为止我提到的算法,其实还有一些其他的排序算法,特别是一些并行化的排序算法,下篇文我会顺带介绍一下~

参考文献:《算法导论》 第八章 线性时间排序


每天进步一点点,Come on!

(●’◡’●)

本人水平有限,如文章内容有错漏之处,敬请各位读者指出,谢谢!

以上是关于每日算法计数&基数&桶&位图排序-简介的主要内容,如果未能解决你的问题,请参考以下文章

常用排序算法基数排序桶排序以及计数排序

排序算法总结

基数排序-LSD&MSD

希尔&计数&基数排序

非比较排序 (计数排序 && 基数排序)

非比较排序 (计数排序 && 基数排序)