计数排序
前面所讲的排序都是基于比较的排序,都可称为”比较排序“,其下界为O(nlogn)。那么有没有时间复杂度为O(n)的线性时间排序算法呢?计数排序便是很基础的一种线性时间排序,它是基数排序的基础。基本思想是:对每一个元素x,确定小于x的元素个数,就可以把x直接放到它在有序序列中的位置上。具体说来即:假设待排序序列a中值的范围[0,k],其中k表示待排序序列中的最大值。首先用一个辅助数组count记录各个值在a中出现的次数,比如count[i]表示数值i在a中的个数。然后依次改变count中元素值,使count[i]表示a中不大于i的元素个数。然后从后往前扫描a数组,a中的元素根据count中的信息直接复制给a。
/*
* 计数排序后的顺序为从小到大
* arr[0,...,len-1]为待排数组,每个元素均是0-k中的一个值
* crr[0,...,k]保存0,...,k中每个值在数组arr中出现的次数
*/
public static int[] countSort(int[] arr, int k) {
if (arr == null || k < 1)
return null;
// 创建一个容量为k的数组crr,并且将crr中的所有数据都初始化为0
int[] crr = new int[k];
for (int i = 0; i < k; i++)
crr[i] = 0;
// 1. 计数
for (int i = 0; i < arr.length; i++)
crr[arr[i]]++;
// 2. 排序
for (int i = 0, j = 0; i < k; i++) {
while ((crr[i]--) > 0) {
arr[j++] = i;
}
}
return arr;
}
最后我们稍微总结下计数排序的特点:
1、不是基于比较的排序,因此可以达到线性排序时间;
2、采取空间换时间的思想,需要brr和crr等辅助空间,但是时间复杂度仅为O(n+k);
3、稳定性好,这也是计数排序最重要的一个特性。
在实际工作中,当k=O(n)时,我们一般才会采取计数排序,如果k很大,则不宜采取该算法,尤其在如下情形下:待排序元素为:1、3、8、5、10000000,这样会造成很大的资源浪费。
基数排序
在计数排序中,当k很大时,时间和空间的开销都会增大(可以想一下对序列{8888,1234,9999}用计数排序,此时不但浪费很多空间,而且时间方面还不如比较排序)。基数排序的基本思想就是把待排序记录分解成个位(第一位)、十位(第二位)....然后分别以第一位、第二位...对整个序列进行计数排序。这样的话分解出来的每一位不超过9,即用计数排序序列中最大值是9.
实现过程如下图所示:
每一步都需要对各个位上的数进行排序,为了保证基数排序的稳定性,我们对每个位上的数进行排序时可以选用计数排序。
实现代码:
public class RadixSort {
/*
* 在第一种计数排序的实现形式上做了些修改 计数排序后的顺序为从小到大 arr[0,...,len-1]为待排数组,我们这里采用三位数
* brr[0,...,len-1]为排序后的有序数组 w[0,...,len-1]用来保存取出的每一位上的数,其每个元素均是0-k中的一个值
* crr[0,...,k]保存0,...,k中每个值出现的次数
*/
public static void countSort(int[] arr, int[] brr, int[] w, int[] crr,int len, int k) {
int i;
// 数组crr各元素置0
for (i = 0; i <= k; i++)
crr[i] = 0;
// 统计数组w中每个元素重复出现的个数
for (i = 0; i < len; i++)
crr[w[i]]++;
// 求数组w中小于等于i的元素个数
for (i = 1; i <= k; i++)
crr[i] += crr[i - 1];
// 把arr中的元素放在brr中对应的位置上
for (i = len - 1; i >= 0; i--) {
brr[crr[w[i]] - 1] = arr[i];
// 如果有相同的元素,则放在下一个位置上
crr[w[i]]--;
}
// 再将brr中的元素复制给arr,这样arr就有序了
for (i = 0; i < len; i++) {
arr[i] = brr[i];
}
}
/*
* 基数排序后的顺序为从小到大 其中参数d为元素的位数
*/
public static void radixSort(int[] arr, int[] brr, int[] w, int[] crr,
int len, int k, int d) {
int i, j, val = 1;
// 从低位到高位依次进行计数排序
for (i = 1; i <= d; i++) { // w中保存的是arr中每个元素对应位上的数
// 范围在0-k之间
for (j = 0; j < len; j++)
w[j] = (arr[j] / val) % 10;
// 对当前位进行计数排序
countSort(arr, brr, w, crr, len, k);
val *= 10;
}
}
public static void main(String args[]) {
int i;
// 待排序数组,每个元素的每一位均在0-7之间
int arr[] = { 217, 125, 362, 136, 733, 522 };
int brr[] = new int[6]; // 用来保存每次计数排序后的结果
int w[] = new int[6]; // 每次循环时,保存该位上的数
int crr[] = new int[8]; // 每次循环时,保存该位上的数出现的次数
radixSort(arr, brr, w, crr, 6, 7, 3);
for (i = 0; i < 6; i++)
System.out.print(arr[i] +" ");
}
}
最后我们同样对基数排序稍微做下总结:
1、同样不是基于比较的排序,因此可以达到线性排序时间;
2、同样采取空间换时间的思想,需要额外的辅助空间,但是时间复杂度仅为O(d(n+k));
3、基数排序的稳定性同样也很好。
时间复杂度:O(d(n+k)),对n个记录(假设记录含有d个关键字,每个关键字有k个取值)基数排序。每一趟分配的时间复杂度是O(n),每一趟收集的时间复杂度是O(k),总共需要d趟分配和收集。
空间复杂度:O(k * n)(链式基数排序中空间复杂度不同)
另附:计数排序、基数排序、桶排序的另一篇博文,来自博主:兰亭风雨
http://blog.csdn.net/ns_code/article/details/20478753