这篇文章中我们来探讨一下常用的非比较排序算法:
- 计数排序
- 基数排序
- 桶排序
在一定的条件下,它们的时间复杂度可以达到O(n).
1、计数排序(Counting Sort)
基数排序用到一个额外的计数数组C,根据数组C来将原数组A中的元素排到正确的位置。
通俗的来讲,例如有10个年龄不同的人,假如统计出有8个人的年龄不比小明大(即小于等于小明的年龄,这里也包括小明),那么小明的年龄就排在第8位,通过这种思想可以确定每个人特殊处理(保证稳定性);通过反向填充目标数组,填充完毕后将对应的数字统计递减,可以确保计数排序的稳定性。
计数排序的步骤如下:
1、统计数组A中每个值A[i]出现的次数,存入C[A[i]];
2、从前向后,使数组C中的每个值等于其与前一项相加,这样数据C[A[i]]就变成了代表数组A中小于等于A[i]的元素个数;
3、反向填充目标数组B:将数组元素A[i]放在数组B的第C[A[i]]个位置(下标为C[A[i]]-1),每放一个元素就将C[A[i]]递减。
代码实现如下:
public static void sortCounting(int[] A){
//C数组应该以A中最大的值为数组大小。这个值就是基数假设为100
int[] C = new int[100];
for (int i : C) {
C[i]= 0;
}
//为每个元素计数
for (int i = 0;i<A.length;i++){
C[A[i]]++;
}
//把C中每个位置都是前面的计数之和。
for (int i = 1;i<C.length;i++) {
C[i] = C[i] + C[i - 1];
}
//分配临时空间b ,做暂存数据
int[] B = new int[A.length];
//把A的每个元素放到按照位置学定理放到B上
for (int i = A.length - 1; i >= 0; i--) {
B[--C[A[i]]] = A[i];
}
for (int i = 0;i<A.length;i++) {
A[i] = B[i];
}
}
计数排序的时间复杂度和空间复杂度与数组A的数据范围(A的最大值与最小值的差加上1)有关,因此对于数据范围很大的数组,计数排序需要大量的时间和内存。
2、基数排序(Radix Sort)
基数排序的发明可以追溯带1887年赫尔曼何乐礼在打孔卡片制表机上的贡献。
它是这样实现的:
将所有待比较正整数统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始进行基数为10的计数排序,一直到最高位计数排序完后,数列就变成一个有序序列(利用了计数排序的稳定性)。
Java代码如下:
/**
* 基数排序
*
* @param A 待排序数组
* @param dn 待排序数组中的最大位数
*/
public static void sortRadix(int[] A, int dn) {
//基数为10
int k = 10;
//保留每个序列的值,个位、十位。。。
int[] A1 = new int[A.length];
int[] C = new int[k];
for (int j = 1; j <= dn; j++) {
//每次归零
for (int i = 0; i < k; i++) {
C[i] = 0;
}
for (int i = 0; i < A.length; i++) {
int radix[] = {1, 1, 10, 100, 1000, 10000, 100000};
A1[i] = A[i] / radix[j] % 10;
}
//不能单纯的调用,因为这里要保留A[i]中原来的数据
//sortByCount(C);
//为每个元素计数
for (int i = 0; i < A.length; i++) {
C[A1[i]]++;
}
//把C中每个位置都是前面的计数之和。
for (int i = 1; i < C.length; i++) {
C[i] = C[i] + C[i - 1];
}
//分配临时空间b ,做暂存数据
int[] B = new int[A.length];
//把A的每个元素放到
for (int i = A.length - 1; i >= 0; i--) {
B[--C[A1[i]]] = A[i];
}
for (int i = 0; i < A.length; i++) {
A[i] = B[i];
}
}
}
基数排序的时间复杂度是O(n*dn),其中n是待排序元素个数,dn是数字位数。这个时间复杂度不一定优于O(n log n),dn的大小取决于数字位的选择(比如比特位数),和待排序数据所属数据类型的全集的大小,dn决定了进行多少轮处理,而n是每轮处理的操作数目。
如果考虑和比较排序进行对照,基数排序的形式复杂度虽然不一定更小,但由于不进行比较,因此其基本操作的代价较小,而且如果适当的选择基数,dn一般不大于log n,所以基数排序一般要快过基于比较的排序,比如快速排序。由于证书也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序并不是只能用于整数排序。
3、桶排序(Bucket Sort)
桶排序也叫箱排序。工作的原理是将数组元素映射到有限数量个桶里,利用计数排序可以定位桶的边界,每个桶再各自进行桶内排序(使用其它排序算法或递归方式继续使用桶排序)
桶排序的实现代码如下:
/**
* 桶排序
*/
public static void sortBucket(int[] A) {
//设置桶的个数,根据要排序的数组中的值合理设置这个值,这决定了map桶的映射;
//这里假设A为[0,39]之间,则以10分割,分割为四个桶。
int bn = 4;
//桶里存放的信息
int C[] = new int[bn];
for (int i = 0; i < A.length; i++) {
C[A[i] / 10]++;
}
for (int i = 1; i < bn; i++) {
C[i] = C[i] + C[i - 1];
}
int[] B = new int[A.length];
for (int i = A.length - 1; i >= 0; i--) {
B[--C[A[i] / 10]] = A[i];
}
for (int i = 0; i < A.length; i++) {
A[i] = B[i];
}
//对每个桶中的数进行插入排序
for (int i = 0; i < bn; i++) {
int left = C[i];
int right = (i == bn - 1 ? A.length - 1 : C[i + 1] - 1);
if (left < right) {
insertSort(A, left, right);
}
}
}
/**
* 插入排序
*
* @param A
* @param left
* @param right
*/
private static void insertSort(int[] A, int left, int right) {
for (int i = left + 1; i <= right; i++) {
int get = A[i];
int j = i - 1;
while (j >= left && A[j] > get) {
A[j + 1] = A[j];
j--;
}
A[j + 1] = get;
}
}
桶排序不是比较排序,不受到O(n log n)下限的影响,它是鸽巢排序的一种归纳结果,当所要排序的数组分散均匀的时候,桶排序拥有线性的时间复杂度。