排序算法(三):计数排序与桶排序

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/sunjinshengli/article/details/70738527

插入排序、堆排序、归并排序等排序方法,在排序的最终结果中,各个元素的次序依赖于他们之间的比较,我们把这一类的排序算法称为比较排序。在最坏情况下,任何比较排序算法都要经过 Omega(nlgn)次比较。因此堆排序和归并排序都是渐近最优的比较排序算法。
计数排序、基数排序和桶排序因为不采用比较排序方法,因此可以打破其下界。本文主要介绍计数排序和桶排序。

一、计数排序

计数排序假设n个输入元素中的每一个都是在0到k区间内的一个整数,其中k为某个整数。当k=O(n)时,计数排序的运行时间为\Seta(n)。

1. 计数排序的基本思想

对于一个输入数组中的一个元素x,只要我们知道了这个数组中比x小的元素的个数,那么我们就可以直接把x放到(x+1)的位置上。这就是计数排序的基本思想。
基于这个思想,计数排序的一个主要问题就是如何统计数组中元素的个数。再加上输入数组中的元素都是0-k区间的一个整数这个条件,那么就可以通过另外一个数组的地址表示输入元素的值,数组的值表示元素个数的方法来进行统计。

下面给出统计数组元素都是0-k区间的整数的数组中各个元素个数的方法。

/**
     * 统计数组元素都是0-k区间的整数的数组中各个元素个数
     * @param src 
     * @param k 元素分布区间
     */
    public static int[] getArrayCount(int[] src,int k) {
        int c[] = new int[k];
        for(int i : src)
             c[i]++;
        return c;
    }

2.计数排序实现

public class CountSort{
    public static void main(String args[]) {
        int k = 10;
        int test[] = com.sunpro.java.RandomGenerator.randGenerator(10,k);
        com.sunpro.java.Print.printArray(test);
        countSort(test, k);
        com.sunpro.java.Print.printArray(test);
    }

    public static void countSort(int[] A , int k){
        //初始化count数组
        int[] count = new int[k];
        //为输入数组中每个元素计数
        for(int i : A)
            count[i]++;
        //计算各个数之前元素的总和
        for(int i = 1; i < k; i++) 
            count[i] = count[i] + count[i-1];
        //初始化一个新的数组存放排序后的元素
        int[] B = new int[A.length];
        for(int j = A.length-1; j >= 0; j--){
            //把A[j]放到对应的位置
            B[count[A[j]]-1] = A[j];
            //计数器减一
            count[A[j]]--;
        }
        System.arraycopy(B,0, A, 0,A.length);
    }
}

计数排序的一个重要性质是稳定性,具有相同值的元素在输出数组中的相对次序与输入数组数组中的次序相同。这种稳定性在进行排序的数据电邮卫星数据的时候比较重要。

二、桶排序

桶排序(bucket sort)假设输入数据服从均匀分布。平均情况下他的时间代价是O(n)。计数排序假设输入数据分布于一个小区间的整数,而桶排序则假设输入是一个随机过程产生的,该过程将元素均匀独立地分布于[0,1)区间上。

1.桶排序的基本思想

桶排序将[0,1)区间划分为n个相同的大小的子区间,这些子区间被称为。然后将n个输入元素分别放入各自的桶中。因为输入时均匀独立的,所以一般不会有很多数同时落在一个桶中的情况。这样,我们想对各个桶中的数据进行排序,然后遍历每个桶,按照次序把各个桶中的元素列出来即可。
一个桶排序的示例如图:
桶排序示例
简单来说就是把数组 arr 划分为n个大小相同子区间(桶),每个子区间各自排序,最后合并。这样说是不是和分治法有点像了 啊!因为分治法就是分解 —— 解决 ——合并这样的套路。我认为这样想没毛病。可以理解为桶排序是一种特殊的分治法,特殊的地方主要体现在前两部分(这样说不知道对不对~)。具体来说如下:
分解部分,采用了计数排序类似的思想,通过分解,虽然没有比较,但是将数据基本按照大小划分了几个区间。针对输入数据均匀分布的特点,因此将数据分布的区间可以均匀分为n个子区间。那么就有

max - min = n * width; (1)

其中,max,min 是输入数据的最大值最小值,n是子区间个数,width是子区间宽度。
这样划分后,每个数据x对应的桶的编号(0-n-1)就是;

index = (x - min) / width = (x - min) / (max - min) * n;(2)

这样,当我们取n=Array.length时,相应的每个数据的存放的桶的编号也就确定了:

index = (x - min) / (max - min) * Array.length

如果我们取n= (max-min)/Array.length 时,就有:

index = (x - min) / (max - min) * (max-min) / Array.length = (x - min) / Array.length;

这样形式上看起来比较简单,在实际编程中也有一定的方便性。在性能上是不是有什么优缺点还没有什么发现。

2. 桶排序的实现

第一种分组方案:

public static void bucketSort(int[] A) {
        //1. 构造桶
        //1.1 确定桶的个数n
        int n = A.length;
        //1.2 声明并初始化一个List,存放链表;
        List<ArrayList<Integer>> Blist = new ArrayList<>(n);
        for(int i = 0; i < n; i++)
            Blist.add(new ArrayList<Integer>(5));
        //2.将数组中的元素放到桶中
        //2.1 确定元素的最值
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;
        for(int a : A){
            max = Math.max(max, a);
            min = Math.min(min, a);
        }
        //2.2 确定每个元素放入桶的编号并放进去
        for(int i : A){
            //2.2.1 确定桶的编号
            int len = A.length;
            //加1是为了保证inde< A.length,防止程序抛出IndexOutofBoundsEx;
            int index = (int)((i-min) / (max-min+1.0) * A.length); 
            //2.2.2 放入对应的桶中
            Blist.get(index).add(i);
        }
        //3.桶内排序
        for(int i = 0; i < Blist.size(); i++){
            java.util.Collections.sort(Blist.get(i));
        }
        //4.合并数据
        int j = 0;
        for(ArrayList<Integer> arr : Blist){
            for(int i : arr){
                A[j++] = i;
            }
        }
    }

第二种分组方案:

public static void bucketSort(int[] arr){

    int max = Integer.MIN_VALUE;
    int min = Integer.MAX_VALUE;
    for(int i = 0; i < arr.length; i++){
        max = Math.max(max, arr[i]);
        min = Math.min(min, arr[i]);
    }

    //桶数
    int bucketNum = (max - min) / arr.length + 1;
    ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
    for(int i = 0; i < bucketNum; i++){
        bucketArr.add(new ArrayList<Integer>());
    }

    //将每个元素放入桶
    for(int i = 0; i < arr.length; i++){
        int num = (arr[i] - min) / (arr.length);
        bucketArr.get(num).add(arr[i]);
    }

    //对每个桶进行排序
    for(int i = 0; i < bucketArr.size(); i++){
        Collections.sort(bucketArr.get(i));
    }

    System.out.println(bucketArr.toString());

}

3. 桶排序的运行复杂度分析

根据程序我们容易得到,最坏情况下,桶排序的时间复杂度为:
桶排序复杂度
平均情况下:
桶排序复杂度
桶排序的期望时间为:
桶排序的期望时间
这些具体的推导可以参考《算法导论》。总而言之一句话:

只要所有桶的大小的平方和与元素总数成线性关系,桶排序就能在线性时间能完成。

那么怎么才能算“所有桶的大小的平方和与元素总数成线性关系”呢?正好,我们通过上面给出两个方案来对比一下:

public static void main(String args[]) {
        //总数小,范围小
        int test1[] = com.sunpro.java.RandomGenerator.randGenerator(10,10);
        //总数小,范围大
        int test2[] = com.sunpro.java.RandomGenerator.randGenerator(10,1000);
        //总数大,范围小
        int test3[] = com.sunpro.java.RandomGenerator.randGenerator(100,10);
        //总数大,范围大
        int test4[] = com.sunpro.java.RandomGenerator.randGenerator(100,1000);
        //int test[] = {9, 99, 37, 41, 34, 33, 69, 92, 2, 18};
        //com.sunpro.java.Print.printArray(test);
        System.out.println("总数小,范围小:方案二:");
        bucketSort2(test1);
        System.out.println("方案一:");
        bucketSort(test1);
        System.out.println("总数小,范围大:方案二:");
        bucketSort2(test2);
        System.out.println("方案一:");
        bucketSort(test2);
        System.out.println("总数大,范围小:方案二:");
        bucketSort2(test3);
        System.out.println("方案一:");
        bucketSort(test3);
        System.out.println("总数大,范围大:方案二:");
        bucketSort2(test4);
        System.out.println("方案一:");
        bucketSort(test4);
        //com.sunpro.java.Print.printArray(test);

    }

运行结果:

桶排序>java BucketSort
总数小,范围小:
方案二:
[[0, 1, 1, 2, 4, 5, 6, 8, 8, 9]]
方案一:
[[0], [1, 1], [2], [], [4], [5], [6], [], [8, 8], [9]]
总数小,范围大:
方案二:
[[2], [], [], [], [], [], [], [], [], [], [], [117], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [320], [], [], [342]
, [], [], [], [], [], [408], [418], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [663], [], [684],
 [], [], [], [], [], [], [756], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [993]]
方案一:
[[2], [117], [], [320, 342], [408, 418], [], [663, 684], [756], [], [993]]
总数大,范围小:
方案二:
[[0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5,
5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9,
 9]]
方案一:
[[0, 0, 0, 0, 0, 0], [], [], [], [], [], [], [], [], [], [1, 1, 1, 1, 1, 1, 1], [], [], [], [], [], [], [], [], [], [2, 2, 2, 2, 2, 2, 2, 2, 2, 2], [
], [], [], [], [], [], [], [], [], [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], [], [], [], [], [], [], [], [], [], [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4], []
, [], [], [], [], [], [], [], [], [5, 5, 5, 5, 5, 5, 5, 5, 5], [], [], [], [], [], [], [], [], [], [6, 6, 6, 6, 6, 6, 6, 6, 6, 6], [], [], [], [], []
, [], [], [], [], [7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7], [], [], [], [], [], [], [], [], [], [8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8], [], [], [], [],
[], [], [], [], [], [9, 9, 9, 9, 9, 9, 9, 9, 9], [], [], [], [], [], [], [], [], []]
总数大,范围大:
方案二:
[[1, 11, 18, 30, 35, 58, 77, 100], [112, 128, 134, 139, 140, 141, 146, 159, 159, 184, 185, 193], [202, 216, 227, 236, 238, 241, 256, 257, 273, 300],
[302, 308, 309, 310, 342, 345, 355, 358, 363, 364, 391], [406, 410, 425, 431, 442, 471, 473, 482], [502, 538, 566, 573, 581, 588, 598, 599], [602, 60
9, 612, 646, 646, 660, 663, 663, 675, 684], [705, 708, 716, 719, 761, 761, 762, 773, 789, 793, 797], [801, 812, 823, 826, 845, 852, 853, 855, 874, 87
7, 880, 880, 882, 900], [904, 923, 932, 957, 969, 977, 985, 997]]
方案一:
[[1], [11, 18], [30], [35], [], [58], [], [77], [], [100], [], [112], [128], [134, 139, 140], [141, 146], [159, 159], [], [], [184, 185], [193], [202
], [216], [227], [236, 238], [241], [256, 257], [], [273], [], [300], [302, 308, 309, 310], [], [], [], [342, 345], [355, 358], [363, 364], [], [], [
391], [406], [410], [425], [431], [442], [], [], [471, 473], [482], [], [502], [], [], [538], [], [], [566], [573], [581, 588], [598, 599], [602, 609
], [612], [], [], [646, 646], [], [660, 663, 663], [675], [684], [], [705, 708], [716], [719], [], [], [], [761, 761, 762], [773], [], [789, 793, 797
], [801], [812], [823, 826], [], [845], [852, 853, 855], [], [874, 877], [880, 880, 882], [], [900, 904], [], [923], [932], [], [957], [], [969, 977]
, [985], [997]]

好吧,没有对比就没有伤害!通过这四种情况下的输出,我们可以得到直观的结论:
对小范围情况,第二种方案肯定都不是线性的。对于大范围情况,第二种方案的也基本不是线性的。
而对于第一种方案,基本都可以保持线性,但是在“总数大,范围小”的情况下的线性也不好。实际上这种情况比较适合计数排序

参考文献:
《算法导论》Thomas

没有更多推荐了,返回首页

私密
私密原因:
请选择设置私密原因
  • 广告
  • 抄袭
  • 版权
  • 政治
  • 色情
  • 无意义
  • 其他
其他原因:
120
出错啦
系统繁忙,请稍后再试