线性时间运行的排序算法

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

对于比较排序,对含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];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

计数排序花费的总的时间为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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

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

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

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

桶排序

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

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

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

位图排序

位图排序是一种效率极高(复杂度可达O(n))并且很节省空间的一种排序方法,但是这种排序方法对输入的数据是有比较严格的要求(数据不能重复,大致知道数据的范围,必须是整数,没有数据与整数记录相关联……)。位图排序即利用位图或者位向量来表示集合。举个例子,假如有一个集合{3,5,7,8,2,1},我们可以用一个8位的二进制向量set[1-8]来表示该集合,如果数据存在,则将set相对应的二进制位置1,否则置0.根据给出的集合得到的set为{1,1,1,0,1,0,1,1},然后再根据set集合的值输出对应的下标即可得到集合{3,5,7,8,2,1}的排序结果。这个就是位图排序的原理。

位图排序的应用:

      1.给40亿个不重复的unsigned int的整数,没有排过序,然后再给一个数,如果快速判断这个数是否在那40亿个数当中。

      因为unsigned int数据的最大范围在在40亿左右,40*10^8/1024*1024*8=476,因此只需申请512M的内存空间,每个bit位表示一个unsigned int。读入40亿个数,并设置相应的bit位为1.然后读取要查询的数,查看该bit是否为1,是1则存在,否则不存在。

      2.给40亿个unsigned int的整数,如何判断这40亿个数中哪些数重复?

      同理,可以申请512M的内存空间,然后读取40亿个整数,并且将相应的bit位置1。如果是第一次读取某个数据,则在将该bit位置1之前,此bit位必定是0;如果是第二次读取该数据,则可根据相应的bit位是否为1判断该数据是否重复。

 

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

《王道》

http://www.fx114.net/qa-97-116090.aspx

http://blog.csdn.net/jiange_zh/article/details/50703949

原文地址:https://www.cnblogs.com/shixisheng/p/6840763.html