桶排序与计数排序--非比较排序算法1

非比较排序算法

排序算法分两大类,基于比较的排序和非基于比较的排序。没啥好解释的,基于比较的,比如冒泡排序、插入排序、选择排序、希尔排序、堆排序、归并排序、快速排序,基于比较的排序复杂度下限为O(nlogn)。非基于比较的排序如,桶排序、计数排序、基数排序。非基于比较的排序不受O(nlogn)这个下限的约束,能够达到O(n)的复杂度。但是非基于比较的算法一般对数据有比较严格的要求,所以一般用来处理一些有特征性的数据的排序。


算法介绍:

BucketSort
这个算法,提出的时候虽然很简单,但是满有意思的。
非比较性的排序算法,也不知道大佬们怎么想到的,利用了数组下标的天然有序性,将元素作为新数组的下标放入新数组,然后循环输出新数组即可实现排序。

简单而特殊的桶排序--计数排序:

假设一组数{3, 8 ,5, 8 ,4 ,2 ,7},我们可以准备一个新数组,将这个数组的值,作为另一个数组的下标。循环原数组,将值一个个的丢到对应的下标中,丢一次,新数组中的值+1。然后直接循环新输出,判断如果新数组中的值大于0则输出,是几则输出几次。
这样一趟即可完成排序,复杂度可以达到O(n)。
上面的例子有几个特点:
1、元素范围确定
元素范围不确定的话,不知道要准备多少个桶。
2、元素值为正整数
假设上面的数组为{-1,3, 8 ,5, 8 ,4 ,2 ,7},不可能出现下标位小数的元素,所以这组数不适用桶排序算法
3、需要额外的空间O(n),如果n过大,可能要考虑其它原地排序的算法。
4、在桶中的数据完全堆叠的情况下,每个桶只是一个计数器,可以达到O(n)+O(n)的时空复杂度。
5、这种情况下好像没办法探讨稳定性。。。
因为数据堆叠在了一起,降低了维度,一组数,变成了下标与count,下面的写法将会解决这个问题、
6、截止上面的算法,我们可以用下面的sample1来描述,但是我在网上看,计数排序的写法比我们写的多走了一步。。。并不是直接按次数输出,虽然我觉得这样没毛病。
计数排序会将之前记的值再累加一次,累加的规则是自身+前一个元素的值,假设某个元素当前位置的和是n,那么这个元素应该放在n-1的位置。假设有很多个相同的数据,那么这个数据中最后一个出现的,应该在n-1的位置。我们从后面来遍历元素,假设某个值有多个重复的,遇到之后将和-1,继续循环下去即可,我们用sample2来实现。实现了稳定性,很可能是算法中采用下面这个写法的原因。

上面这组数的Java代码实现:

private static void sample1(){
    int[] arr = new int[]{3, 8 ,5, 8 ,4 ,2 ,7};

    // 观察范围,从2-8,那我们就准备0-8,一共9个桶,得从0开始,总不能绕过去吧,,
    int[] buckets = new int[9];
    // 使用arr的值,作为数组的下标,统计出现次数
    for(int i = 0; i < arr.length;i++){
        buckets[arr[i]]++;
    }
    // 循环输出
    for(int i = 0; i < buckets.length;i++){
        int cnt = buckets[i];
        if(cnt > 0){
            while(cnt - 1 >= 0){
                System.out.print(i);
                System.out.print(",");
                cnt--;
            }
        }
    }
}

标准的、保持稳定性的计数排序Java代码实现:

private static void sample2() {
        int[] arr = new int[]{10, 0, 3, 8, 5, 8, 4, 1, 7};

        // 观察范围,从1-10,那我们就准备0-10,一共11个桶,得从0开始,总不能绕过去吧,,
        int[] countings = new int[11];
        // 使用arr的值,作为数组的下标,统计出现次数
        for (int i = 0; i < arr.length; i++) {
            countings[arr[i]]++;
        }
        // 合起来
        for (int i = 1; i < countings.length; i++) {
            countings[i] += countings[i - 1];
        }
        // 新建数组,存放排序后的元素
        // 假设元素i,元素i,应该在result中的countings-1的位置,假设有重复
        // 为保证稳定性,要从后往前倒序
        int[] result = new int[arr.length];
        for(int i = arr.length - 1; i>=0 ;i--){
            // arr[i]的值
            int countingIndex= arr[i];
            // arr[i]值所对应的位置
            int countingValue = countings[countingIndex];
            // 找到arr[i]对应的countings中的值,这个值减1,就是数应该放的位置
            result[countingValue -1] = countingIndex;
            // 这个位置可能有多个,另外的元素要排在前面,所以counting要--
            countings[countingIndex]--;
        }
        // 新欢countings[i],
        System.out.println(Arrays.toString(result));
    }

正常的桶排序:

假设上面的数组有小数{1.1,3.28 ,5.3, 8 ,4 ,2 ,7},也不可能出现下标为小数的元素,但是我们可以这样,我们认为[1~2)之间的都放入1位置的桶里,[2~3)之间的数都放入2位置的桶里,这样仍然可以确定有0-8,9个桶,由于每个桶的数据不能堆叠,每个桶将会是一个数组,所以还需要结合排序算法来确定桶中数组的有序性。思考总结一下:
1、我们可以使用一定的方法,将多个元素放到一个桶中,假设这个方式是f(x),那么需要保证x和fx是相同的顺序,也就是如果x1 < x2,那么f(x1) < f(x2),x1 == x2,f(x1) == f(x2),x1 > x2那么f(x1) > f(x2)
这组数和之前有负数的不同,因为要利用数组下标的天然有序性,在有负数的情况下,为了保证方法和元素的顺序相同,再怎么折腾也没办法成为数组下标。
2、假设有n个元素,m个桶,桶中也是一个数组,假设我们使用数组这种数据结构的话,由于我们不可预知数据到底会如何分布,一个桶中的元素最多可能是n个,所以占用的空间特别多,达到O(m*n)
3、由于不知道桶中的数组会有多少个元素,需要一个另外的计数数组,来存放数组中有效元素的个数,计数数组和桶个数一一对应。
4、我们来推断这种一般情况下的时间复杂度,根据桶内排序算法复杂度不有关,下面的黑体字标出来的是两种复杂度
假设都不考虑空间复杂度
假设桶中的排序使用O(nlogn)的算法,不使用桶排序直接排序是O(nlogn);为了方便计算,我们假设平均分配,第一步拆分桶需要花费n,使用桶排序是m * (n/m * logn/m) = n * (logn - logm) + n = nlogn - nlogm + n,也就是nlogn VS nlogn - nlog(m/2);既然是拆分m一定大等于2,大等于2就意味着m/2大等于1,log(m/2)大于0,所以是快的,快了nlog(m/2)这么多的复杂度。
假设桶中的排序使用O(n^2)的算法,使用桶排序后变成m * ((n/m)^2),也就是O( (n ^2) / m),也就是m越大相对越快,往极限推,假设n == m,那么 (n ^2) / m就变成了n;也就是n个元素放到n个桶中,能达到O(n)。上面的例子,其实也是n个元素放到n个桶中,重复元素在这种情况下完全可以算作一个元素。
5、稳定性,根据桶内部实现排序的算法,可以实现为稳定排序
6、由于每一个桶是相互隔离的,可以通过并行计算来实现桶内元素的排序,然后再合并,这是其它排序没法做到的。
7、不得不说这玩意和HashMap有点辣么像

桶是个数组的Java代码实现,内部使用插入排序,随便使用什么都行:

private static void sample3(){
    int[] arr = new int[]{10, 132, 443, 22, 568, 233, 542, 579, 378, 246, 719, 12, 35,  386, 291, 129, 483, 555, 683};
    // 准备10个桶,每个里面放arr.length
    int len = arr.length;
    int[][] buckets = new int[len][10];
    int[] bucketLength = new int[arr.length];
    bucketSort(arr, buckets, bucketLength);
    for (int i = 0; i < buckets.length; i++) {
        int[] bucket = buckets[i];
        int blen = bucketLength[i];
        for (int j = 0; j < blen; j++) {
            System.out.print(bucket[j]);
            System.out.print(",");
        }
    }
}

private static int[][] bucketSort(int[] arr, int[][] buckets, int[] bucketLength) {
    // 初始化计数数组为0
    for (int i = 0; i < arr.length; i++) {
        // 确定桶的位置
        int bucketIndex = arr[i] / 100;
        int[] bucket = buckets[bucketIndex];
        // 如何知道桶到了多少位了,还需要另外一个辅助函数
        int index = bucketLength[bucketIndex];
        bucket[index] = arr[i];
        bucketLength[bucketIndex]++;
    }
    return buckets;
}

/**
 * 使用插入排序对数组内的元素进行插入排序
 *
 * @param arr
 */
private static void insertSort(int[] arr, int endIndex) {
    for (int i = 1; i <= endIndex; i++) {
        // 记录起始位置的value
        int value = arr[i];
        int j = i - 1;
        // 从前一个位置,递减到最后,找第一个小等于value的位置,如果没找到,需要知道没找到
        // 小等于是为了保证稳定性
        // 为了区分找到了0位置的元素还是没找到,把j--放到循环里面,放到break后面
        // 如果没找到小等于value的元素,说明都大于value,value应该放到0位置
        // 如果找到了小等于value的元素,value应该放到j+1的位置
        while (j >= 0) {
            if (arr[j] <= value) {
                break;
            }
            j--;
        }
        int idx = i;
        // 从i到j+2的位置的元素往后移动一位
        while (idx > j + 1) {
            arr[idx] = arr[idx - 1];
            idx--;
        }
        arr[j + 1] = value;
    }
}

猜你喜欢

转载自blog.csdn.net/u011531425/article/details/80642935