计数排序
前面已经介绍过的所有排序,包括插入排序、选择排序、冒泡排序、归并排序、鸡尾酒排序、希尔排序、快速排序和堆排序都是比较排序,因为在他们的排序过程中,都需要经过比较,比较元素的大小然后调整顺序。可以证明,在最坏情况下,任何比较排序都需要经过 Ω ( n log n ) \Omega(n \log n) Ω(nlogn) 次比较,所以比较排序的时间复杂度最好也就能到 O ( n log n ) O(n \log n) O(nlogn) 了。
那如果我们想进一步降低时间复杂度怎么办呢?自然我们就不能用比较排序了。那么有不比较就能排序的方法吗?答案是有,还不止一种。这一篇我们就先讲一种:计数排序。
原理
计数排序的思想是对每一个元素,确定不大于它的元素个数,然后把它放到对应的位置就行了。如同一个班的学生从低到高排序,对一个学生来说,如果知道班上有10个人不比他高,那么他就应该排到第11位。
如果有相同的元素,这里还需要一点小技巧。比如有3个相同的元素,则要把这3个挨着放,最好能保持其稳定,即虽然都是一样大小,但之前在前面的排序完还是在前面,3个元素的相对位置不变。
具体步骤如下:
- 用一个数组记录每个元素分别有几个。
- 计算出对每个元素分别有多少个元素不大于它。
- 把每个元素按位置放好,若有相同值的元素,注意要调整其位置以避免相同元素放到相同位置。
实现
按照以上原理我们来用代码实现。
下面就是用C语言实现的代码。
- 要排序的数组a有n个元素。注意a中所有元素值都在[0,k]范围内。
- 数组c先记录每个元素分别有几个,后记录对每个元素分别有多少个元素不大于它。
- 数组b用于保存排序结果。
- 最终将排序结果复制到a中。
/* a中所有元素值都在[0,k]范围内 */
void count_sort(int a[], int n, int k)
{
int *b, *c;
int i;
b = (int*)malloc(sizeof(int)*n);
if (b==NULL) return;
c = (int*)malloc(sizeof(int)*(k+1));
if (c==NULL) {
free(b);
return;
}
memset(c, 0, sizeof(int)*(k+1));
for (i=0; i<n; i++) {
c[a[i]]++;
} //此时c[i]中存的是a中有几个i
for (i=1; i<=k; i++) {
c[i] += c[i-1];
} //此时c[i]中存的是有几个元素<=i
for (i=n-1; i>=0; i--) {
b[c[a[i]]-1] = a[i]; //将a[i]放入b中正确的位置
c[a[i]]--; //调整b的位置指针,避免相同元素放到相同位置
}
memcpy(a, b, sizeof(int)*n); //将排序结果复制到a中
free(c);
free(b);
}
为了验证此函数的效果,加上了如下辅助代码,对3个数组进行排序,运行结果在最后,可见排序成功。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SIZE_ARRAY_1 5
#define SIZE_ARRAY_2 6
#define SIZE_ARRAY_3 20
void show_array(int a[], int n);
void count_sort(int a[], int n, int k);
void main()
{
int array1[SIZE_ARRAY_1]={1,4,2,9,0};
int array2[SIZE_ARRAY_2]={10,5,2,1,9,2};
int array3[SIZE_ARRAY_3];
for(int i=0; i<SIZE_ARRAY_3; i++) {
array3[i] = (int)((10.0*rand())/(RAND_MAX+1.0));
}
printf("Before sort, ");
show_array(array1, SIZE_ARRAY_1);
count_sort(array1, SIZE_ARRAY_1, 9);
printf("After sort, ");
show_array(array1, SIZE_ARRAY_1);
printf("Before sort, ");
show_array(array2, SIZE_ARRAY_2);
count_sort(array2, SIZE_ARRAY_2, 10);
printf("After sort, ");
show_array(array2, SIZE_ARRAY_2);
printf("Before sort, ");
show_array(array3, SIZE_ARRAY_3);
count_sort(array3, SIZE_ARRAY_3, 10);
printf("After sort, ");
show_array(array3, SIZE_ARRAY_3);
}
void show_array(int a[], int n)
{
if(n>0)
printf("This array has %d items: ", n);
else
printf("Error: array size should bigger than zero.\n");
for(int i=0; i<n; i++) {
printf("%d ", a[i]);
}
printf("\n");
}
运行结果:
Before sort, This array has 5 items: 1 4 2 9 0
After sort, This array has 5 items: 0 1 2 4 9
Before sort, This array has 6 items: 10 5 2 1 9 2
After sort, This array has 6 items: 1 2 2 5 9 10
Before sort, This array has 20 items: 8 3 7 7 9 1 3 7 2 5 4 6 3 5 9 9 6 7 1 6
After sort, This array has 20 items: 1 1 2 3 3 3 4 5 5 6 6 6 7 7 7 7 8 9 9 9
分析
时间复杂度
从代码可见,count_sort中只有一层循环,有三次,量级分别是 n n n、 k k k、 n n n,所以其时间复杂度为 O ( n + k ) O(n+k) O(n+k),若 k = O ( n ) k = O(n) k=O(n),则计数排序的时间复杂度为 O ( n ) O(n) O(n)。
空间复杂度
因为计数排序需要两个辅助数组b和c来存储中间结果,所以其空间复杂度为 O ( n + k ) O(n+k) O(n+k),若 k = O ( n ) k = O(n) k=O(n),则计数排序的空间复杂度为 O ( n ) O(n) O(n)。
要注意的是计数排序要求所有数据都是属于某个小区间的整数。如果范围太大,即 k k k 太大,则时间复杂度、空间复杂度都会变成增加,会影响排序效率。
稳定性
稳定。