持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第12天,点击查看活动详情
前言
计数排序是非比较排序的一种,本文就来简单分享一波笔者的学习经验与心得。
笔者水平有限,难免存在纰漏,欢迎指正交流。
计数排序
引例
情景引入
某天考完试出成绩了,(十分制)小A和小E都考了5分,小B考了8分,小C考了3分,小D考了2分,现在他们要互相比成绩(经典桥段),如何排成升序呢?
看起来很简单是吧,毕竟只是个引例,重点看看思想。
思路分析
首先,我们申请一个数组arr[11],由于考试是十分制的,也就是数据大小范围在0~10,我们用数组下标对应分数,数组的值对应得到相应分数的人数,比如arr[1]等于0就说明得到1分的人数为0。
下面开始一个一个处理每个人的分数,其实就是一个一个输入程序嘛,那在输入的时候不就可以记录一下多少分的有多少人了吗,比如小A考了5分,输入程序,对应arr[5]++。
小C考了3分,输入程序,对应arr[3]++。
值得注意的是,这里考5分的有两个人,小E也考了5分,输入程序,对应arr[5]++,也就是说,数据输入有重复也没关系,毕竟数组的值存的就是数据出现次数。
处理后的最终结果如下
然后呢?还需要再排序数组里的元素吗?
完全不需要,实际上数组下标本身就是升序排列的,直接按顺序打印即可,即使要求降序打印也可以倒着打印。
代码实现
#include<stdio.h>
int main()
{
int n = 0; //要输入的元素个数
scanf("%d", &n);
int i =0 ;
int arr[11] = {0};//初始化数组元素为0
for(i = 0; i < n; i++)
{
int tmp = 0;//临时变量存下标
scanf("%d", &tmp);
arr[tmp]++;//记录对应数据出现次数
}
int j = 0;
for(i = 0; i < 11; i++)
{
for(j = 0; j < arr[i]; j++)
printf("%d ", i);
}
return 0;
}
看看这个:
知道为什么了吧……还不知道?
这种算法,就好比给桶编上编号,表示可能会出现的所有数值,每出现一个数,就在对应编号的桶中放上一个小旗子,只要数一数每个桶中有几个旗子就知道这个数出现了几次,而且顺序已经由数组下标升序排好了,总的来说也只是记录数值出现的次数罢了。
不足之处
换一下更大的数据范围看看
比如要排序n个数为升序,每个数范围在0~1000内。
#include<stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
int i = 0;
int j = 0;
int bucket[1001] = {0};
for(i = 0; i < n; i++)
{
int tmp = 0;
scanf("%d", &tmp);
bucket[tmp]++;
}
for(i = 0; i < 1001; i++)
{
for(j = 0; j < arr[i]; j++)
{
printf("%d ", i);
}
}
return 0;
}
该算法时间复杂度为O(M+N),M是桶的个数,N是待排序数的个数。速度还是很快的,只是空间复杂度也很大,非常浪费空间,比如要排序的数的范围是0~2100000000,那就要2100000000个桶,也就是要申请2100000001个变量,不管要排几个数,都需要这么多空间,确实浪费内存。
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
计数排序的思想
当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法,只不过使用场景受限。
由于用来计数的count数组的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。不过计数排序可以用在基数排序中的算法来排序数据范围很大的数组。
思想
计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用,特点是把待排序的数据和计数数组的的下标建立映射关系从而不需要比较就能实现排序。
绝对映射与相对映射
如果数据范围在100~120之间,需要开辟一个120个元素的数组吗?不需要,按照数据原大小与计数数组下标建立联系属于绝对映射,比如120有三个,那就让count[120]的值变为3。选用绝对映射容易造成空间浪费,就拿当前这个例子来说,计数数组下标0到99的位置是不是就闲置了呢?
所以一般都不会考虑使用绝对映射,而是采用相对映射。
拿负数举个例子。
如果要排序的数据中不只有正整数(其实浮点数不能用计数排序),还出现了负整数,而数组下标能为负数吗?那样就越界了,这可怎么办呢?
首先,我们得算一算最小的数(min)和最大的数(max)的绝对值哪个更大一些,取较大者设为size,那这样就可以建立映射关系:让每个数据都加上一个size来对应下标。
映射关系如图:
这样一来下标就不会出现负数了,原来的负数都对应上了某个正数。
操作步骤
- 找出待排序数组中最大和最小的元素(需要遍历原数组),创建计数数组count
- 统计相同元素出现次数并按照映射关系存入count数组中
- 根据统计的结果按count数组下标位置的顺序放入值覆盖待排序数组
这里建立相对映射关系,让数据都减去最小值,不过要注意把数据排回原数组时要根据映射关系变回去。
实现代码
void CountSort(int* arr, int sz)
{
assert(arr);
int max = arr[0];
int min = arr[0];
int size = 0;
for (int i = 1; i < sz; ++i)
{
if (arr[i] > max)
max = arr[i];
if (arr[i] < min)
min = arr[i];
}
int* count = (int*)calloc((max - min + 1), sizeof(int));
if (count == NULL)
{
perror("calloc fail");
return;
}
for (int j = 0; j < sz; ++j)
{
count[arr[j] - min]++;
}
for (int k = 0, m = 0; k < max - min + 1; ++k)
{
while(count[k] != 0)
{
arr[m++] = k + min;
count[k]--;
}
}
free(count);
}
计数排序的特性总结
- 计数排序在数据集中在一定范围内时,效率很高,如果数据小的很小、大的很大,数据范围比较分散时就拉胯了,适用范围及场景有限,只适用于整型,如果是浮点数或字符串排序,还得用比较排序。
- 时间复杂度:O(MAX(N,数据范围))
- 空间复杂度:O(数据范围)
- 稳定性:稳定
以上就是本文全部内容,感谢观看,你的支持就是对我最大的鼓励~