计数排序(counting sort)就是一种牺牲内存空间来换取低时间复杂度的排序算法,是桶排序的一种扩展
核心思想:
对于一个输入数组中的任意一个元素x, 知道了这个数组中比x小的元素的个数,那么我们就可以直接把x放到(x+1)的位置上。这就是计数排序的基本思想。
比如说有5个元素小于x,那就把x放到第六个位置上。当有元素相等时,需要略作修改,因为不能把他们都放在同一个位置上。
基于这个思想,计数排序的一个主要问题就是如何统计数组中元素的个数。再加上输入数组中的元素都是0-k区间的一个整数这个条件,那么就可以通过另外一个数组的地址表示输入元素的值,数组的值表示元素个数的方法来进行统计。
流程:
1:首先找到此组数据的最大值与最小值–max min, 初始化桶int []buckets=new int[max-min+1] --因为我们需要这些数据的值作为数组buckets的索引,所以定义max-min+1个桶
2:对数据进行对号入桶:我们遍历所有数据,将每个值为 arr[index]-min 的数据放入到第 arr[index]-min个桶中,比方说第一个的值为 5,则放入5-min(2)=buckets[3]的桶中
并记录该桶放入数据的个数buckets[index-min]++;
3:遍历所有的桶,依次从桶中取出数据将其赋值与arr数组,:每取出一次计数器减减;直到计数器为0为止 ;到此排序完成
//将对应的元素值作为数组的索引放入数组中并计数,然后按根据计数器的个数从数组中一次取出,即为有序排列,缺点不稳定,不能保证相同元素在排序后前后位置依然相同
public static int[] countingSort(int[] arr){
if (arr == null || arr.length == 0) {
return null;
}
int max= Arrays.stream(arr).max().getAsInt();
int min= Arrays.stream(arr).min().getAsInt();
int[] buckets = new int[max-min+1];
//找出每个数字出现的次数
for (int j : arr) {
buckets[j-min]++;
}
int arrIndex = 0;
for(int i = 0; i < buckets.length; i++){
while(buckets[i]> 0){
arr[arrIndex++] = i+min;
buckets[i]--;
}
}
return arr;
}
以上无法保证排序的稳定性,不能保证相同元素在排序后前后位置依然相同。
改进
放入桶中的方式依然相同,只是后续对计数桶数据进行求和处理,即从从index=1位置开始,当前位置的元素=当前位置元素+其前一位置的元素;目的是记录每个桶上的元素最后次出现的位置 求和后的数组countBucketArr 中的每一个元素的值value-1对应着arr数组中的元素最后次出现的位置,即需要赋值给零时数组sortedArr的索引—代码里有详细解释
public static int[] countingSort1(int[] arr){
boolean isLegal=arr==null|| arr.length ==0;
if (isLegal) {
return arr;
}
int max= Arrays.stream(arr).max().getAsInt();
int min= Arrays.stream(arr).min().getAsInt();
int countBucket=max-min+1;//计数
int[] countBucketArr=new int[countBucket];//计数桶
int[] sortedArr=new int[arr.length];
for (int j : arr) {//记录元素出现次数
//countBucketArr[max-j]++;//大到小
countBucketArr[j-min]++;
}
/* arr[6,1,2,7,9,6]--> buckets[j-min(1)]++ --> countBucketArr[1,1,0,0,0,2,1,0,1]①
* --> countBucketArr[i]+=countBucketArr[i-1] ②--->countBucketArr[1,2,2,2,2,4,5,5,6]③---->countBucketArr[arr[j]-min]-1]=arr[j]--> [1, 2, 6, 6, 7, 9]
*
* 解析:
* ③中的4元素对应着①中的2元素。而①中的2则映射着arr值=6的元素(如果倒叙遍历则先取最后位置的6) 所以我们给临时数组赋值的索引即为4-1=3的位置
* 推导:
* 比如arr中最后的6元素 在①中位于索引为5的位置且出现2次,经过②操作后在③中表示①中2的元素(即arr中的元素6)最终会出现在元素location=4减一的位置即新数组索引3的位置
* 而location=arr[5]-min=6-1=5-->countBucketArr[5]=4-->综合下得到countBucketArr[arr[j]-min]-1]=arr[j]=6
*
试想如果在最终赋值的时候正序遍历,则最开始获取的就是arr中第一个元素6放在新数组sortedArr的索引3的位置,然后计数器从4减一为3,则
第arr中最后一个元素6则被放在sortedArr的索引2的位置,则新数组中第一6元素在第二个6元素后。就失去了算法的稳定性,所以必须倒序遍历才能保证算法的稳定性
* 注:减一因为数组索引为0开始索引赋值的时候需要减一
* */
//计算数组中小于等于每个元素的个数,因为小于等于0的数的个数就是等于0的数的个数, 即从countBucketArr中的第一个元素开始,每一项和前一项相加
for (int i = 1; i < countBucketArr.length; ++i) {
countBucketArr[i]+=countBucketArr[i-1];
}
//填充数组
//保证最后一个等于数组countBucketArr下标j的元素排在最后,然后countBucketArr[i]–,倒数第二个等于下标i的元素排在这个元素之前
//记录每个桶位上的最后个元素出现的位置
for(int j=arr.length-1;j>=0;j--){
//因为数组的下标从0开始所以小于等于arr[j]的数的个数为x就应该将该数放在数组sortedArr的第x-1个位置
sortedArr[countBucketArr[arr[j]-min]-1] = arr[j] ;
countBucketArr[arr[j]-min]--;
// sortedArr[countBucketArr[max-arr[j]]-1] = arr[j];//大到小
// countBucketArr[max-arr[j]]--;//大到小
}
arr=sortedArr.clone();
return arr;
}
扩展:对字符串如何使用计数排序
//计数排序字符串
public static String[] countingSortWithStr(String[] arr){
if (arr==null||arr.length==0){
return null ;
}
//元素最长位数
int maxLength =Arrays.stream(arr).max(Comparator.comparing(String::length)).get().length();
//排序结果数组
String[] sortedArr = new String[arr.length];
int[] buckets = new int[128];//最大ASCII 127 最小0 极值128
//从个位开始比较,一直比较到最高位
for(int k = maxLength;k >0;k--) {
for (String s : arr) {
int index = 0;
//w位数不足的位置补0
if (s.length() >= k ) {
index = s.charAt(k-1);
}
buckets[index]++;
}
//将各个桶中的数字个数,转化成各个桶中最后一个数字的下标索引
for (int i = 1; i < buckets.length; i++) {
buckets[i]+=buckets[i-1];
}
//遍历原始数列,进行排序
for (String s : arr) {
int index = 0;
if (s.length() >= k) {
index = s.charAt(k - 1);
}
sortedArr[buckets[index] - 1] = s;
buckets[index]--;
}
Arrays.fill(buckets, 0);
}
return sortedArr;
}
同样8000个数,计数排序非常快
计数排序是一个稳定的排序算法.
- 当输入的元素是n个0到k之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法.
- 当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法.
- 计数排序的缺点是当最大值最小值差距过大时,不适用计数排序,当元素不是整数值,不适用计数排序.
- 可以看到辅助数组的长度和桶的数量由最大值和最小值决定,假如两者之差很大,而待排序数组又很小,那么就会导致辅助数组或桶大量浪费。
稳定性:计数排序很重要的性质是它是稳定的。即使是相同的数,在输入数组中先出现的数,在输出数组中也位于前面。通常这种稳定性只有当排序数据还附带卫星数据是才很重要。其次,它也是基数排算法的一个子过程,因为基数排序必须要求子程序是稳定的。
一般使用在量非常大,数组元素之间范围小的场景 比如:2 万名员工的年龄(量大,范围小(0-100岁));50万人查询高考成绩(0-750分)