数据结构—计数排序学习笔记

数据结构—计数排序学习笔记

应用场景

数值范围小,且都是整型的数列进行排序,并且有严格的时间复杂度要求(小于O(n * logn)),常使用计数排序。

比如我有10亿个数,但这些数都是位于0到10的整型数据,要求时间复杂度严格小于O(n * logn)。

算法原理

首先我们查看适合计数排序的数据特点:
①数据量没有限制,但数值都集中在很小的范围内,比如0~100;
②数据类型必须要是整型的,结合特点①即数据取值只有很少的整形范围。

假定有20个待排序数列:
9,3,5,4,9,1,2,7,8,1,3,6,5,3,4,0,10,9,7,9

可以看到它们都是处于 [0,10] 内的整形数据

计数排序首先就是创建一个计数数组(视待排序数据量大小选择使用 int 、long 、short等数据类型进行数组初始化),计数数组的长度和待排序数据不同的数值个数有关。

比如上面20个待排序数组,取值范围是0到10,初始化时我们可以选择 byte 或者 short 作为数组数据类型来创建一个长度为 11 的计数数组。

如果待排序数组的取值不连续,比如 1,11,21这种,那么我们可以建立长度为3的数组,根据 num/10 的结果判断数据应该填入哪一个下标中。

然后遍历待排序数列,对每次访问的数据进行判断,对应计数数组下标的元素进行加一操作。

第一个数据是 9 ,则对计数数组下标为 9 的元素进行 +1 操作;
第二个数据是 3 ,则对计数数组下标为 3 的元素进行 +1 操作。

最终遍历完成后,计数数组会统计下标对应的整型数据共出现了多少次,我们根据数组下标和出现次数即可还原出有序的数列。

计数数组下标012345678910
计数数组元素12132212141

那么输出的有序序列就是:
0,1,1,2,3,3,3,4,4,5,5,6,7,7,8,9,9,9,9,10

复杂度

  • 时间复杂度:
    ①填充计数数组:遍历待排序数据需要 O(n) ,判断数据填入哪一个下标正常操作是 O(1) 的,因此填充阶段时间复杂度是 O(n);
    ②输出排序数组:遍历一遍需要 O(n);
    因此计数排序的时间复杂度是 O(n) 。

  • 空间复杂度:
    需要创建一个计数数组,其空间大小为待排序数据的取值范围,因此空间复杂度为 O(1) ~ O(n) 。

代码实现

朴素计数排序:根据最大值和最小值得出计数数组长度,然后建立数值和下标的偏移关系,维护计数数组来进行排序。

public void countSort(int[] nums){
    //找到最大值和最小值
    int max = nums[0],min = nums[0];
    for(int num:nums){
        if(num > max)
            max = num;
        if(num < min)
            min = num;
    }
    //计算 计数数组长度
    int len = max - min + 1;
    int[] countArray = new int[len];
    //遍历原序列
    for(int num:nums){
        //这里偏移地址计算时间复杂度是 O(1)
        countArray[(num-min)]++;
    }
    //输出排序后的序列
    int index = 0;
    for(int i=0;i<len;i++)
        for(int j=0;j<countArray[i];j++)
            //根据偏移恢复数据
            nums[index++] = (i+min);
}

当然计数排序还有一些短板,比如不稳定,计数数组某个元素>1 时,其对应的数据先后顺序就会乱掉。

为此可以将计数数组变形,计数数组从第二个元素开始,每一个元素都加上前面所有元素之和,得到一个统计排名的数组:

排名数组下标012345678910
排名数组元素13479111214151920

这个排名数组中存储的就是对应整数的最终排序位置。

获得排名数组后,我们从后往前遍历原始数列,每次取出整数 i ,并访问排名数组,找到整数 i 对应下标,取出排名数组元素 m ,将整数 i 插入最终序列中第 m 个位置,并把排名数组中的m - 1,代表下一次遇到整数 i 时,将这一个 i 插入到序列第 m - 1 的位置。

比如初始序列最后一个数据是 9 ,代表小黄得了 9 分,
它对应排名数组元素为 19 ,则把小黄的 9 插入到最终序列的第19位,
同时排名数组中的 19 - 1 = 18,
下一次再遇到小明的 9 时,插到第18位,也就是小黄的前面,保证了排序的稳定。

改进计数排序:在朴素计数排序的基础上,维护排名数组保持排序的稳定性。

public void countSort2(int[] nums){
    //找到最大值和最小值
    int max = nums[0],min = nums[0];
    for(int num:nums){
        if(num > max)
            max = num;
        if(num < min)
            min = num;
    }
    //计算 计数数组长度
    int len = max - min + 1;
    int[] countArray = new int[len];
    //遍历原序列
    for(int num:nums){
        //这里偏移地址计算时间复杂度是 O(1)
        countArray[(num-min)]++;
    }
    //维护排名数组
    int sum = 0;
    for(int i=0;i<len;i++){
        sum += countArray[i];
        countArray[i] = sum;
    }
    //输出最终排序,注意要从后往前遍历
    int[] cloneArray = nums.clone();
    for(int i=cloneArray.length-1;i>=0;i--){
        nums[--countArray[cloneArray[i]-min]] = cloneArray[i];
    }
}

改进的计数排序,由于多了一个维护排名数组的操作,因此时间复杂度由 O(3n) 变为 O(3n + m),简化为 O(n + m)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值