经典排序算法之计数排序

上一篇: 桶排序
下一篇: 基数排序

计数排序(counting sort)就是一种牺牲内存空间来换取低时间复杂度的排序算法,是桶排序的一种扩展

核心思想:
对于一个输入数组中的任意一个元素x, 知道了这个数组中比x小的元素的个数,那么我们就可以直接把x放到(x+1)的位置上。这就是计数排序的基本思想。
比如说有5个元素小于x,那就把x放到第六个位置上。当有元素相等时,需要略作修改,因为不能把他们都放在同一个位置上。

基于这个思想,计数排序的一个主要问题就是如何统计数组中元素的个数。再加上输入数组中的元素都是0-k区间的一个整数这个条件,那么就可以通过另外一个数组的地址表示输入元素的值,数组的值表示元素个数的方法来进行统计。
在这里插入图片描述

流程:

1:首先找到此组数据的最大值与最小值–max min, 初始化桶int []buckets=new int[max-min+1] --因为我们需要这些数据的值作为数组buckets的索引,所以定义max-min+1个桶
2:对数据进行对号入桶:我们遍历所有数据,将每个值为 arr[index]-min 的数据放入到第 arr[index]-min个桶中,比方说第一个的值为 5,则放入5-min(2)=buckets[3]的桶中
并记录该桶放入数据的个数buckets[index-min]++;

3:遍历所有的桶,依次从桶中取出数据将其赋值与arr数组,:每取出一次计数器减减;直到计数器为0为止 ;到此排序完成

//将对应的元素值作为数组的索引放入数组中并计数,然后按根据计数器的个数从数组中一次取出,即为有序排列,缺点不稳定,不能保证相同元素在排序后前后位置依然相同

    public static int[] countingSort(int[] arr){
        if (arr == null || arr.length == 0) {
            return null;
        }

        int max= Arrays.stream(arr).max().getAsInt();
        int min= Arrays.stream(arr).min().getAsInt();

        int[] buckets = new int[max-min+1];
        //找出每个数字出现的次数
        for (int j : arr) {
            buckets[j-min]++;
        }

        int arrIndex = 0;
        for(int i = 0; i < buckets.length; i++){
            while(buckets[i]> 0){
                arr[arrIndex++] = i+min;
                buckets[i]--;
            }
        }

        return arr;
    }

以上无法保证排序的稳定性,不能保证相同元素在排序后前后位置依然相同。

改进

放入桶中的方式依然相同,只是后续对计数桶数据进行求和处理,即从从index=1位置开始,当前位置的元素=当前位置元素+其前一位置的元素;目的是记录每个桶上的元素最后次出现的位置 求和后的数组countBucketArr 中的每一个元素的值value-1对应着arr数组中的元素最后次出现的位置,即需要赋值给零时数组sortedArr的索引—代码里有详细解释

 public  static  int[] countingSort1(int[] arr){
        boolean isLegal=arr==null|| arr.length ==0;

        if (isLegal) {
            return arr;
        }
        int max= Arrays.stream(arr).max().getAsInt();
        int min= Arrays.stream(arr).min().getAsInt();
        int countBucket=max-min+1;//计数

        int[] countBucketArr=new int[countBucket];//计数桶
        int[] sortedArr=new int[arr.length];

        for (int j : arr) {//记录元素出现次数
            //countBucketArr[max-j]++;//大到小
             countBucketArr[j-min]++;

        }

        /*  arr[6,1,2,7,9,6]--> buckets[j-min(1)]++ -->       countBucketArr[1,1,0,0,0,2,1,0,1]①
         *  --> countBucketArr[i]+=countBucketArr[i-1]   ②--->countBucketArr[1,2,2,2,2,4,5,5,6]③---->countBucketArr[arr[j]-min]-1]=arr[j]--> [1, 2, 6, 6, 7, 9]
         * 
         * 解析:
         *        ③中的4元素对应着①中的2元素。而①中的2则映射着arr值=6的元素(如果倒叙遍历则先取最后位置的6) 所以我们给临时数组赋值的索引即为4-1=3的位置
         * 推导:        

         *   比如arr中最后的6元素 在①中位于索引为5的位置且出现2次,经过②操作后在③中表示①中2的元素(即arr中的元素6)最终会出现在元素location=4减一的位置即新数组索引3的位置
         *        而location=arr[5]-min=6-1=5-->countBucketArr[5]=4-->综合下得到countBucketArr[arr[j]-min]-1]=arr[j]=6
         *
             试想如果在最终赋值的时候正序遍历,则最开始获取的就是arr中第一个元素6放在新数组sortedArr的索引3的位置,然后计数器从4减一为3,则
             第arr中最后一个元素6则被放在sortedArr的索引2的位置,则新数组中第一6元素在第二个6元素后。就失去了算法的稳定性,所以必须倒序遍历才能保证算法的稳定性

         *  注:减一因为数组索引为0开始索引赋值的时候需要减一

         * */
       //计算数组中小于等于每个元素的个数,因为小于等于0的数的个数就是等于0的数的个数, 即从countBucketArr中的第一个元素开始,每一项和前一项相加
        for (int i = 1; i < countBucketArr.length; ++i) {
            countBucketArr[i]+=countBucketArr[i-1];
        }

        //填充数组
        //保证最后一个等于数组countBucketArr下标j的元素排在最后,然后countBucketArr[i]–,倒数第二个等于下标i的元素排在这个元素之前
        //记录每个桶位上的最后个元素出现的位置

        for(int j=arr.length-1;j>=0;j--){
            //因为数组的下标从0开始所以小于等于arr[j]的数的个数为x就应该将该数放在数组sortedArr的第x-1个位置

            sortedArr[countBucketArr[arr[j]-min]-1] = arr[j] ;

            countBucketArr[arr[j]-min]--;

//            sortedArr[countBucketArr[max-arr[j]]-1] = arr[j];//大到小
//            countBucketArr[max-arr[j]]--;//大到小
        }


        arr=sortedArr.clone();
        return arr;

    }

扩展:对字符串如何使用计数排序

//计数排序字符串
    public static String[] countingSortWithStr(String[] arr){
        if (arr==null||arr.length==0){
            return null ;
        }

        //元素最长位数
        int maxLength  =Arrays.stream(arr).max(Comparator.comparing(String::length)).get().length();


        //排序结果数组
        String[] sortedArr = new String[arr.length];
        int[] buckets = new int[128];//最大ASCII 127 最小0 极值128
         //从个位开始比较,一直比较到最高位
        for(int k = maxLength;k >0;k--) {

            for (String s : arr) {
                int index = 0;
                //w位数不足的位置补0
                if (s.length() >= k ) {
                    index = s.charAt(k-1);
                }

                buckets[index]++;
            }
            //将各个桶中的数字个数,转化成各个桶中最后一个数字的下标索引

            for (int i = 1; i < buckets.length; i++) {

                buckets[i]+=buckets[i-1];
            }
            //遍历原始数列,进行排序
            for (String s : arr) {
                int index = 0;

                if (s.length() >= k) {
                    index = s.charAt(k - 1);
                }
                sortedArr[buckets[index] - 1] = s;
                buckets[index]--;
            }

            Arrays.fill(buckets, 0);
        }
        return sortedArr;
    }

同样8000个数,计数排序非常快

在这里插入图片描述

计数排序是一个稳定的排序算法.

  • 当输入的元素是n个0到k之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法.
  • 当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法.
  • 计数排序的缺点是当最大值最小值差距过大时,不适用计数排序,当元素不是整数值,不适用计数排序.
  • 可以看到辅助数组的长度和桶的数量由最大值和最小值决定,假如两者之差很大,而待排序数组又很小,那么就会导致辅助数组或桶大量浪费。

稳定性:计数排序很重要的性质是它是稳定的。即使是相同的数,在输入数组中先出现的数,在输出数组中也位于前面。通常这种稳定性只有当排序数据还附带卫星数据是才很重要。其次,它也是基数排算法的一个子过程,因为基数排序必须要求子程序是稳定的。

一般使用在量非常大,数组元素之间范围小的场景 比如:2 万名员工的年龄(量大,范围小(0-100岁));50万人查询高考成绩(0-750分)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值