计数排序--时间复杂度为线性的排序算法

    我们知道基于比较的排序算法的最好的情况的时间复杂度是O(nlgn),然而存在一种神奇的排序算法,不是基于比较的,而是空间换时间,使得时间复杂度能够达到线性O(n+k),这种算法就是本文将要介绍的计数排序


一、适用情况

    这个算法在n个输入元素中每一个都是0到k的范围的整数,其中k也是整数。当k = O(n)时,排序的时间复杂度为Θ(n)。它的性质也就决定了它的运用范围比较窄,但是对于一个待排序的有负数的数组,我们可以将整个数组的整体加上一个整数,使得整个数组的最小值为0,然后就可以使用这个排序算法了。而且这个算法是稳定的


二、基本思想

    对一个一个待排序的元素x,我们可以确定小于等于x的个数i,根据这个个数i,我们就可以把x放到索引i处。那么如何确定小于等于x的个数i呢?我们可以专门开辟一个数组c[],然后遍历数组,确定数组a中每个元素中出现的频率,然后就可以确定对于a中每个元素x,小于等于这个元素的个数。然后就可以把元素x放到对应位置了。当然元素x的大小是可能重复的,这样就需要我们对数组c的值访问之后减1,保证和x一样大的元素能放在其前面。


三、运行过程


    1、对于数组A,我们首先统计每个值的个数,将A的值作为C元素索引,值的个数作为C数组的值。比如对于数组A中的元素2,在数组A中出现了2次,所以c[2] = 2,而元素5出现了以此,所以c[5] = 1。

    2、至此为止,数组C中已经统计了各个元素的出现次数,那么我们就可以根据各个元素的出现次数,累加出比该元素小的元素个数,更新到数组C中。比如a图中,C[0]=2表示出现0的次数为2,C[1]=0表示出现1的次数为0,那么小于等于1的元素个数为C[0]+C[1]=2,我们把C[1]更新为2,同理C[2]=2表示出现2的次数为2,那么小于等于2的元素个数为C[1]+C[2]=4,继续把C[2]更新为4,以此类推...

    3、到这里,我们得到了存储小于等于元素的个数的数组C。现在我们开始从尾部到头部遍历数组A,比如首先我们看A[7] = 3,然后查找C[3],发现C[3] = 7,说明有7个元素小于等于3。我们首先需要做一步C[3] = C[3] - 1,因为这里虽然有7个元素小于等于3,但是B的索引是从0开始的,而且这样减一可以保证下次再找到一个3,可以放在这个3的前面。然后B[C[3]] = 3,就把第一个3放到了对的位置。后面以此类推,直到遍历完数组B。

    4、截至到这,我们就获得了一个有序的数组B。


四、代码实现

    我们使用Java来实现这个算法:

    public static void countingSort(int[] a, int[] b, int k){
        int[] c = new int[k+1];//存放0~k
        for(int i = 0; i<a.length; i++)
            c[a[i]] += 1;
        for(int i = 1; i<=k; i++)
            c[i] += c[i-1];
        for(int i = a.length-1; i >= 0; i--){
            c[a[i]] --;
            b[c[a[i]]] = a[i];
        }
    }

    简简单单几行代码就实现了计数排序,其中参数a数组表示待排序的数组,b数组表示排序之后的存储数组,k表示a数组中最大的值。


五、时间复杂度分析和时间比较

    开头我们就说过了,这个算法不是基于比较的排序算法,因此它的下界可以优于Ω(nlgn),甚至这个算法都没有出现比较元素的操作。这个算法很明显是稳定的,也就是说具有相同值得元素在输出数组中的相对次序和他们在输入数组中的相对次序相同。算法中的循环时间代价都是线性的,还有一个常数k,因此时间复杂度是Θ(n+k)。当k=O(n)时,我们采用计数排序就很好,总的时间复杂度为Θ(n)。

    下面我们来和快速排序这个时间复杂度为Θ(2nlnn)的算法在时间上进行比较,关于快速排序的算法之前写过,请点击此处,测试代码如下:

package acm;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Random;

public class CountingRank {
    public static void main(String[] args) throws NumberFormatException, IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int length ;
        int maxElement;
        System.out.println("请输入待测试数组长度和数组的最大元素:");
        String input[] = br.readLine().split(" ");
        length = Integer.parseInt(input[0]);
        maxElement = Integer.parseInt(input[1]);
        br.close();
        int[] a = new int[length];
        int[] b = new int[length];
        Random random = new Random(System.currentTimeMillis());
        for(int i = 0;i<a.length; i++){
            a[i] = random.nextInt(maxElement+1);//产生[0,maxElement]的随机数
        }
        double start1 = System.currentTimeMillis();
        countingSort(a, b, maxElement);
        double end1 = System.currentTimeMillis();
        System.out.println(end1-start1);
        double start2 = System.currentTimeMillis();
        quickRank(a, 0, a.length-1);
        double end2 = System.currentTimeMillis();
        System.out.println(end2-start2);
    }
    private static int quickRankDivide(int[] arr, int left , int right){
        int shaft = arr[left];//定义一个轴
        int start = left++;//记录轴元素位置 left++ 减少一次轴元素与自己的无用比较 
        while(left<right){//只要left在right的右边 就持续进行
            while(left<right&&arr[left]<=shaft){//查找到不满足轴摆放要求的元素就暂停
                left++;//移动left指针
            }//循环结束时找到了不满足轴摆放要求的元素(大于轴元素的元素)
            while(left<right&&arr[right]>shaft){
                right--;//移动right指针
            }//循环结束时找到了不满足轴摆放要求的元素(小于轴元素的元素)
            swap(arr, left , right);//交换left right指针的元素
            left++;
            right--;//交换完之后再次移动指针 减少无用比较次数
        }
        swap(arr,start,right);//摆放轴元素到准确的位置
        return right;//返回轴元素的位置
    }
    private static void swap(int[] arr , int location1 , int location2){
        int temp = arr[location1];
        arr[location1] = arr[location2];
        arr[location2] = temp;
    }
    public static void quickRank(int[] arr, int left, int right){//分治 递归
        if(left<right){
            int location = quickRankDivide(arr, left, right);//找到轴元素的位置
            quickRank(arr,left,location-1);
            quickRank(arr,location+1,right);//递归调用
        }
    }
    public static void countingSort(int[] a, int[] b, int k){
        int[] c = new int[k+1];//存放0~k
        for(int i = 0; i<a.length; i++)
            c[a[i]] += 1;
        for(int i = 1; i<=k; i++)
            c[i] += c[i-1];
        for(int i = a.length-1; i >= 0; i--){
            c[a[i]] --;
            b[c[a[i]]] = a[i];
        }
    }
}

    我们首先测试元素大小范围为[0,10]的10000容量的数组排序:


    可以看到计数排序只花了1ms,而快排花了11ms,当数组最大元素较小时,这个优势是很明显的。

   

    接下来测试元素大小范围为[0,10000]的10000容量的数组排序:


    当最大元素的大小达到10000的时候,这两者的差距就很微弱了:一个是2ms,一个是3ms。


    如果我们继续增大数组元素的最大值,达到1000000:


    当元素最大值比较大的时候,计数排序就比不过快速排序了。


六、总结

    计数排序是复杂度为O(n+k)的稳定的排序算法,k是待排序列最大值,适用在对最大值不是很大的整型元素序列进行排序的情况下(整型元素可以有负数,我们可以把待排序列整体加上一个整数,使得待排序列的最小元素为0,然后执行计数排序,完成之后再变回来。这个操作是线性的,所以计数这样做计数排序的复杂度仍然是O(n+k))。本质上是一种空间换时间的算法,如果k比较小,计数排序的效率优势是很明显的,当k变得很大的时候,这个算法可能就不如其他优秀的排序算法(比如我们上面说的快速排序)。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值