计数排序 | 详细思路、Java实现与注释


计数排序感觉不难,但是细节比较复杂。

一、原理

计数排序实质上是桶排序的特殊情况,对于桶排序,当桶的数量等于元素数量时,可以达到最短的排序时长,这种情况不就是一个桶对应一个元素值吗?比如我们有取值范围为1-50的的元素数组,设置50个桶,那就是值相等的元素放到同一个桶里,然后按顺序取出每个桶里的元素,就可以得到一个有序数组了。

来看个例子,这有个无序数组 arr={8,4,5,7,1,4,3,6,7,9,7,2},我们要对它使用计数排序。

首先,做一下准备工作。计算出数组arr中元素的最大值max,创建一个计数数组countArr,下标范围为[0,max],长度为max+1。初始化countArr所有元素值为0。

遍历无序数组arr,将每个元素出现的次数记录在countArr中。如何记录呢?计数数组的下标范围为[0,max],因此数组中某个元素n出现的次数就记录在countArr的下标为n的位置上。

然后将这个数组上的元素逐个累加,令countArr[i+1] = countArr[i] + countArr[i+1],比如countArr[1] = countArr[0] + countArr[1]。

那么累加后的countArr有什么意义呢? 我们看它的下标为4的位置,元素值为5,它就代表了原数组arr中元素值<=4的元素有5个,就是这个意思。

以上过程如图:

在这里插入图片描述


接下来,我们开始对数组arr进行排序。首先,从后往前遍历数组arr的元素。然后,根据countArr找到元素应该排在哪一位。整个排序过程如下图所示。

第一个遍历的元素是1,先在countArr中找到下标为1的位置,这个位置的元素值为2,因此原数组arr中元素值<=1的元素有2个,我们当前遍历的1就是数组arr中最后一个1。既然如此,那么在最终排序结果中,这个1就是最终有序序列中的第2个元素,也就应该放在结果数组res下标为1的位置上。然后我们既然把1拿走了,那么countArr中元素1的数量就要减一。

第二个遍历的元素是7,先在countArr中找到下标为7的位置,这个位置的元素值为10,因此原数组arr中元素值<=7的元素有10个,我们当前遍历的7就是数组arr中最后一个7。既然如此,那么在最终排序结果中,这个7就是最终有序序列中的第10个元素,也就应该放在结果数组res下标为1的位置上。然后我们既然把7拿走了,那么countArr中元素7的数量就要减一。

重复以上过程,直到结果数组res被填满,也就是所有元素都完成了排序。

那么为啥要从后往前遍历呢?

因为计数排序是一个稳定排序算法,这也就是为啥从后开始遍历的原因。原数组arr中在后面的元素,在排序后的数组res中也放在后边。我们在计数数组countArr中获取元素8在有序数组res中的位置时,若8在原数组arr中出现了多次,我们是先把最后一个8放到有序数组res中的。

在这里插入图片描述


有没有发现countArr中下标为0的位置一直没有赋值?这是因为arr中根本没有元素0,因此countArr也不需要计算0出现的次数,这里放上0其实只是为了更简单地理解计数排序的思想。下次如果无序数组的取值范围在[50,100]之间呢,那是不是也得统计[0,49]里面的元素出现的次数?它们都是出现0次,统计这个没意义,而且浪费空间。

因此,countArr的下标n统计的元素出现的次数就不是n出现的次数了。我们还需要求出原始数组的最小值min,下标n位置上的值就是元素n+min 在原始数组中出现的次数。

二、代码实现示例

package Sort;

import java.util.Arrays;

public class CountSort {
    public static void main(String[] args) {
        int[] arr = new int[] {8,4,5,7,1,3,6,2};
        int[] res = countingSort(arr, arr.length);
        System.out.println(Arrays.toString(res));
    }

    /**
     * 计数排序
     * @param arr 无序数组,假设数组中存储的都是非负整数。
     * @param len 无序数组的长度
     * @return
     */
    public static int[] countingSort(int[] arr, int len) {
        if (len <= 1) return null;
        // 查找数组中数据的范围[0,max]
        int max = arr[0];
        for (int i = 1; i < len; ++i) {
            if (max < arr[i]) {
                max = arr[i];
            }
        }
        // 申请一个计数数组countArr,下标大小[0,max]。记录数组中每个值的数量
        int[] countArr = new int[max + 1];        // 0~max,长度为max+1
        for (int i = 0; i <= max; ++i) {          // 初始化每个元素值为0,即次数为0
            countArr[i] = 0;
        }
        // 计算每个元素的个数,放入ccountArr中
        for (int i = 0; i < len; ++i) {
            countArr[arr[i]]++;                   // arr[i]的值对应在countArr中的索引
        }
        // 依次累加,得到num中元素值<=countArr当前索引值的元素数量
        for (int i = 1; i <= max; ++i) {
            countArr[i] = countArr[i-1] + countArr[i];
        }
        // res存储排序之后的结果
        int[] res = new int[len];
        // 计算排序的关键步骤,有点难理解
        // 从后往前扫描num数组,
        for (int i = len - 1; i >= 0; --i) {
            int index = countArr[arr[i]]-1;      // num中第n个元素,其索引为n-1
            res[index] = arr[i];                 // 赋值到res数组中
            countArr[arr[i]]--;                  // 此时countArr对应位置的数就少了一个,需要减一
        }
        // 将结果拷贝给num数组,也可以直接返回res
//        for (int i = 0; i < len; ++i) {
//            arr[i] = res[i];
//        }
        return res;
    }
}

三、算法分析

计数排序是稳定排序算法。

计数排序只能用在数据范围不大的场景中。

计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。

时间复杂度:O(n+max-min)

空间复杂度:O(max-min)
max和min分别为原始数组的最大值和最小值

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值