【排序】——计数排序
1、应用场景:
计数排序可以看作是一种特殊的桶排序。
当要排序的n个数据,所处范围并不大,我们就可以把数据划分成n个桶。每个桶内的数据值都是相同的,桶内不需要再排序。
比如高考的分数系统 满分900分,最低0分。因此数据范围只有0-900,但是考生却很多有几十万。此时我们要对这几十万个考生的分数进行排名,就可以使用到计数排序。
2、一个简化的例子说明:
1、假设只有8个考生,分数在0到5之间,我们把这8个考生的分数放到一个数组里面,记为分数数组arr:
int[] arr = {2,5,3,0,2,3,0,3};
2、考生的成绩从0到5分,用一个计数数组countArr[6]来表示桶,这个数组的下标对应分数0到5。数组的每个元素的值存的是对应下标表示的分数有多少个考生。此时数组的值应该是这样的。
int[] countArr = new int[6]; //对应下标为考试分数
countArr[0] = 2; //考试分数为0的考生有2个
countArr[1] = 0; //考试分数为0的考生有0个
countArr[2] = 2; //考试分数为2的考生有2个
countArr[3] = 3; //考试分数为3的考生有3个
countArr[4] = 0; //考试分数为4的考生有0个
countArr[5] = 1; //考试分数为5的考生有1个
3、计数数组有了,那么我们还需要一个数组去存排序后的分数,记这个临时数组为tempArr。那么此时,我们怎么计算出分数在这个临时数组的存储位置呢? 从计数数组中我们可以看到,分数为3的考生有3个,分数小于3的考生有4个,那么分数小于3的考生的存储位置就为:tempArr[0],tempArr[1],tempArr[2],tempArr[3]。而分数为3的考生存储的位置就为:tempArr[4],tempArr[5],tempArr[6]。
4、有了这个思路,我们可以把计数数组countArr的每个位置上的值累加,这样就会得到一个全新的计数数组:
这个全新的计数数组中每个元素的值就可以表示为分数(计数数组下标)在临时数组tempArr中的最大存储位置+1。
从分数数组arr最后的一个元素开始遍历(为了保证排序算法稳定性)。
第1趟,找到3,在countArr的下标为3的元素值为7,于是我们把3存储到tempArr中下标为6(7-1)的位置,
然后countArr下标为3的元素值减一。
第2趟,找到0,在countArr的下标为0的元素值为2,于是我们把0存储到tempArr中下标为1(2-1)的位置,
然后countArr下标为0的元素值减一。
第3趟,找到3,在countArr的下标为3的元素值为6,于是我们把3存储到tempArr中下标为5(6-1)的位置,
然后countArr下标为0的元素值减一。
以此类推…
3、实现代码
/**
* 思路:
* 1、一个计数数组countArr
* 2、一个临时数组存放排序后的数据tempArr
*
* @param arr
*/
private static void jishuSort(int[] arr) {
int arrlength = arr.length;
if (arrlength < 2) {
return;
}
/*计算数组的最大值*/
int max = arr[0];
for (int i = 1; i < arrlength; i++) {
if (max < arr[i]) {
max = arr[i];
}
}
/*一个计数数组countArr*/
int[] countArr = new int[max + 1];
/*计算每个元素的个数,放入计数数组中*/
for (int i = 0; i < arrlength; i++) {
countArr[arr[i]]++;
}
/*依次累加*/
for (int i = 1; i <= max; i++) {
countArr[i] = countArr[i - 1] + countArr[i];
}
/*一个临时数组存放排序后的数据tempArr*/
int[] tempArr = new int[arrlength];
/*把排序的数据存入临时数组*/
for (int i = arrlength - 1; i >= 0; i--) {
int tempIndex = countArr[arr[i]] - 1;
tempArr[tempIndex] = arr[i];
countArr[arr[i]]--;
}
/*将临时数组的结果返回给原数组*/
for (int i = 0; i < arrlength; i++) {
arr[i] = tempArr[i];
}
}
4、代码优化
思路:上面的代码计数数组中下标为0之类很小的下标有可能会浪费掉。
比如一组数据{671,673,672,667,667,674,676}
,其中最大值为676,按照上面的代码,我们需要创建一个长度为677的计数数组,但是我们可以发现,它前面的[667]
的空间完全浪费了,那怎样优化呢?
/**
* 计数排序优化 : 采用偏移量来处理,防止空间浪费
* @param arr
*/
private static void jishuSortPlus(int[] arr) {
int arrLength = arr.length;
if (arrLength < 2) {
return;
}
int min = arr[0];
int max = arr[0];
for (int i = 0; i < arrLength; i++) {
min = Math.min(min, arr[i]);
max = Math.max(max, arr[i]);
}
/*计数数组*/
int[] countArr = new int[max - min + 1];
/*计算每个元素的个数,放入计数数组中*/
for (int i = 0; i < arrLength; i++) {
countArr[arr[i] - min]++;
}
/*依次累加*/
for (int i = 1; i < max - min + 1; i++) {
countArr[i] = countArr[i - 1] + countArr[i];
}
/*一个临时数组存放排序后的数据tempArr*/
int[] tempArr = new int[arrLength];
for (int i = arrLength - 1; i >= 0; i--) {
int tempIndex = countArr[arr[i] - min] - 1;
tempArr[tempIndex] = arr[i];
countArr[arr[i] - min]--;
}
/*把临时数组的值返回给旧数组*/
for (int i = 0; i < arrLength; i++) {
arr[i] = tempArr[i];
}
}