计数排序是一种线性时间的排序算法,适用于整数或有限范围内的非负整数排序。它的核心思想是通过计数每个元素的出现次数来进行排序。计数排序不是比较排序,速度快于任何比较排序算法。但对于数据范围很大的数组,需要大量时间和内存。
并且由于目前我们的机器的内存比较充足,为了提高机器的运行效率,就利用了空间换时间的思想。下面,我们详细说说计数排序的内容。
计数排序
算法概念
计数排序区别于冒泡排序和选择排序这两种比较排序,它是非比较排序算法,该算法于1954年由 Harold H. Seward提出,通过计数将时间复杂度降到了O(n)。
算法步骤--基础版
第一步:找出待排序数组arr的最大值max;
第二步:定义一个新的数组cntArr,新数组的长度为max+1,所有元素默认值为0;
第三步:循环遍历待排序数组arr的元素,元素值作为新数组cntArr的下标,新数组的元素就是统计原数组每个元素的个数。
第四步:由于新数组的下标本身就是有序的,循环遍历新数组,如果新数组的某索引值大于0(即代表原数组的有等于该索引值的元素存在),每次循环都需要将cntArr[]的值-1(代表已经排序了,待排序数据少了一个),并将索引值赋值给新开辟的结果数组resArr。依次处理每一个元素。
第五步:返回排序后的数组resArr。
代码实现--基础版
int* cntSort(int* arr, int n) {
int max = arr[0];
for (int i = 1; i < n; i++) {
if (max < arr[i])max = arr[i];
}
int* cntArr = new int[max + 1] {0};
for (int i = 0; i < n; i++) {
cntArr[arr[i]]++;
}
int* resArr = new int[n];
int j = 0;
for (int i = 0; i <= max; i++) {
while (cntArr[i] > 0) {
resArr[j++] = i;
cntArr[i]--;
}
}delete[] cntArr;
return resArr;
}
算法分析
假如有一个数组{101,103,105,109,110};明显最大值为110,我们需要开辟大小为111的数组来计数,这明显太过奢侈,前面的[0,100]的空间完全浪费掉了。
按照上述实现的方法有一个这样明显的缺点,就是浪费空间,而且不是一般的浪费。避免是不可能的,算法本身就是空间换时间,但我们怎么能减小这个缺点呢?下面来看一下进阶版的计数排序:
算法步骤--进阶版
1.遍历原数组,找出原数组的最大值max,最小值min;
2.定义计数数组cntArr,大小为max-min+1,所有元素默认为0;
3.遍历原数组,将原数组的元素减去min值作为计数数组的下标,新数组存储该索引值在原数组出现的次数。
4.定义结果数组resArr,大小为原数组大小。
5.遍历统计数组,将数组索引位的值不为零的索引值加上min值存到resArr中。
6.返回resArr
代码实现--进阶版
//计数排序:时间复杂度 O(n),空间复杂度为 O(n),不稳定
int* CountSort(int* arr, int size)
{
//求最值
int max = arr[0];
int min = arr[0];
for (int i = 1; i < size; i++)
{
if (max < arr[i])max = arr[i];
if (min > arr[i])min = arr[i];
}
//开统计数组
int len = max - min + 1;
int* cntArr = new int[len]{0};
//统计
for (int i = 0; i < size; i++)
cntArr[arr[i] - min]++;
//结果数组
int* resArr=new int[size];
int index = 0;
for (int i = 0; i < len; i++)
{
while (cntArr[i] > 0)
{
resArr[index++] = i + min;
cntArr[i]--;
}
}delete[] cntArr;
return resArr;
}
算法分析
聪明的小伙伴看到我的第一行注释了,目前来说该算法不够稳定,两个相等的值不能保证他们按照原来的顺序排。当然,这个问题也有解决的办法:
进阶优化
我们想要原始数组中元素值相同的元素在排序后依旧保持相同元素的前后相对位置,该怎么办呢?我们试想一下,不稳定的原因是什么,还不是因为在统计时先统计前位的值,后统计后位的值,在输出存储的时候,先把后位的放出去了,有点意思,像不像栈呢?
那我们想让结果稳定,一个优化方法就是倒序存储:请看VCR(开个玩笑,请各位看代码)。
int* CountSort(int* arr, int size)
{
//求最值
int max = arr[0];
int min = arr[0];
for (int i = 1; i < size; i++)
{
if (max < arr[i])max = arr[i];
if (min > arr[i])min = arr[i];
}
//开统计数组
int len = max - min + 1;
int* cntArr = new int[len]{0};
//统计
for (int i = 0; i < size; i++)
cntArr[arr[i] - min]++;
//结果数组
int* resArr=new int[size];
int index = size-1;
for (int i = len-1; i >=0 ; i--)
{
while (cntArr[i] > 0)
{
resArr[index--] = i + min;
cntArr[i]--;
}
}delete[] cntArr;
return resArr;
}
进阶延伸
除了改变存储顺序外,还有办法解决上述不稳定的问题吗?
Of Casue!当然有了。
第一步:找出数组最大值max,最小值min;
第二步:创建计数数组cntArr,长度为max-min+1+1;
第三步:遍历数组元素,以原数组的值作为cntArr数组的索引,以原数组元素的个数作为cntArr的元素值
第四步:对cntArr数组变形,新元素的值是前面元素累加之和,即cntArr[i+1]=cntArr[i+1]+cntArr[i];
第五步:创建结果数组resArr,长度为原数组长度;
第六步:从前向后遍历原始数组的元素,当前元素arr[i]减去最小值min,作为索引,在技术数组中找到对应的元素值cntArr[arr[i]-min]就是arr[i]在结果数组resArr中的位置,做完上述操作,cntArr[arr[i]-min]++;
int* CountSort(int* arr, int size)
{
//求最值
int max = arr[0];
int min = arr[0];
for (int i = 1; i < size; i++)
{
if (max < arr[i])max = arr[i];
if (min > arr[i])min = arr[i];
}
//开统计数组
int len = max - min + 1;
int* cntArr = new int[len+1]{0};
//统计
for (int i = 0; i < size; i++)
cntArr[arr[i] - min]++;
//Add:计数数组变形
for(int i=1;i<size;i++)
cntArr[i]+=cntArr[i-1];
//结果数组
int* resArr=new int[size];
for (int i = 0; i < size; i++)
{
// 如果后面遇到相同的元素,在前面元素的基础上往后排
// 如此就保证了原始数组中相同元素的原始排序
int val=arr[i]-min;//值
int pos=cntArr[val];//位置
resArr[pos]=arr[i];//赋值
cntArr[val]++;//自增
}delete[] cntArr;
return resArr;
}
最后这块儿挺难理解的,值(arr[i]-min)作为变形计数数组的索引,从该索引处取值,当作arr[i]在结果数组的位置。
感谢大家!