一、什么是计数排序
计数排序的核心思想是——对于数组 array 中的某一个元素 X,倘若我们能统计出数组中小于 X 的元素个数 N。利用这一信息,可以直接把元素 X 放在数组中第 N+1个位置处。
举个例子,假如有一数组:
int array[] = {2,5,3,4,1};
倘若 X = 3,我们可以通过一趟遍历统计出小于 3 的元素一共有 2 个(2和1),立即得数 3 应该放在整个数组的第三位(array[2] = 3),但是这样一来会覆盖原有位置的元素。为了简单起见,初始化一个和 array 同维度的全 0 数组 outputarray (你也可以试着实现原址操作)。通过统计数组中元素出现的频率不难得到:
outputarray[] = { 1,2,3,4,5};
计数排序原理简单易懂,且能够在线性时间内完成排序,但是有一个小问题:如果数组中出现重复元素怎么办?
int array[] = {2,5,3,0,2,3,0,3};
这个问题也不难解决, 只要将计数条件从*“大于 X”* 改为*“大于或等于 X”* 就可以了,每次在outarray中填入一个重复元素 i,就让temp[i] = temp[i] - 1 ,以便下次继续填充。
二、计数排序代码实现
为了方便记数组中各个元素出现的次数,不妨维护一个数组 temp[M] ,temp[i] 表示原数组中小于等于数值 i 的元素出现的个数,其实也就是数值 i 的正确位置。显然,对于非负数组来说, temp 的维度 M 应满足:
M = max(array) + 1; // 因为数组中可能会出现0
具体的数值你可以通过一趟遍历得出。
通过一趟遍历统计出原数组所有元素出现过的次数,就可以根据 temp 数组将原数组中的元素按照顺序赋值到 outarray 中,代码如下:
/* parameters:
input array:待处理数组
output array:输出数组
length:输入数组的维度
k:数组中所有数值的上限
*/
void Counting_Sort(int* inputarray, int* outputarreay,int length, int k)
{
int *temp = new int[k]; //临时数组,统计出现次数
for (int i = 0; i < k; i++) //初始化数组
temp[i] = 0; //初始化
for (int j = 0; j < length; j++) //统计元素出现的个数
temp[inputarray[j]] += 1;
for (int l = 1; l < k; l++)//统计小于等于当前值的个数
temp[l] = temp[l] + temp[l - 1];
for (int k = length-1; k>-1; k--)//赋值,个数-1,自后向前保持稳定
{
outputarreay[temp[inputarray[k]]-1] = inputarray[k];
temp[inputarray[k]] -= 1;
}
delete[]temp;
}
值得注意的一点是,在最后一趟遍历赋值是自后向前的,这样可以让数组中重复元素的相对位置不变,因此快速排序是稳定的。
在主函数中调用跑一下看看结果:
int main()
{
int array[] = { 2,5,3,0,2,3,0,3,1,2,3 };
int output[] = { 0,0,0,0,0,0,0,0,0,0,0 };
int k = 0;
for (int i : array)
k = (i > k ? i : k);
Counting_Sort(array, output, size(array), k + 1);
for (int i : output)
cout << i << " ";
cout << endl;
return 0;
}
最后,程序还有可以改进之处——倘若数据中出现负数,如何定义temp?如何得出 outarray?我只想到根据数组元素的下界设置偏移量,达到统计负数出现次数的目的。
思考:倘若数组中的元素相当分散,使用计数排序是否会浪费大量额外的空间?例如
int array[] = {1,5,4,10,100};
此时如何改进呢?欢迎讨论。