计数排序感觉不难,但是细节比较复杂。
一、原理
计数排序实质上是桶排序的特殊情况,对于桶排序,当桶的数量等于元素数量时,可以达到最短的排序时长,这种情况不就是一个桶对应一个元素值吗?比如我们有取值范围为1-50的的元素数组,设置50个桶,那就是值相等的元素放到同一个桶里,然后按顺序取出每个桶里的元素,就可以得到一个有序数组了。
来看个例子,这有个无序数组 arr={8,4,5,7,1,4,3,6,7,9,7,2},我们要对它使用计数排序。
首先,做一下准备工作。计算出数组arr中元素的最大值max,创建一个计数数组countArr,下标范围为[0,max],长度为max+1。初始化countArr所有元素值为0。
遍历无序数组arr,将每个元素出现的次数记录在countArr中。如何记录呢?计数数组的下标范围为[0,max],因此数组中某个元素n出现的次数就记录在countArr的下标为n的位置上。
然后将这个数组上的元素逐个累加,令countArr[i+1] = countArr[i] + countArr[i+1],比如countArr[1] = countArr[0] + countArr[1]。
那么累加后的countArr有什么意义呢? 我们看它的下标为4的位置,元素值为5,它就代表了原数组arr中元素值<=4的元素有5个,就是这个意思。
以上过程如图:
接下来,我们开始对数组arr进行排序。首先,从后往前遍历数组arr的元素。然后,根据countArr找到元素应该排在哪一位。整个排序过程如下图所示。
第一个遍历的元素是1,先在countArr中找到下标为1的位置,这个位置的元素值为2,因此原数组arr中元素值<=1的元素有2个,我们当前遍历的1就是数组arr中最后一个1。既然如此,那么在最终排序结果中,这个1就是最终有序序列中的第2个元素,也就应该放在结果数组res下标为1的位置上。然后我们既然把1拿走了,那么countArr中元素1的数量就要减一。
第二个遍历的元素是7,先在countArr中找到下标为7的位置,这个位置的元素值为10,因此原数组arr中元素值<=7的元素有10个,我们当前遍历的7就是数组arr中最后一个7。既然如此,那么在最终排序结果中,这个7就是最终有序序列中的第10个元素,也就应该放在结果数组res下标为1的位置上。然后我们既然把7拿走了,那么countArr中元素7的数量就要减一。
重复以上过程,直到结果数组res被填满,也就是所有元素都完成了排序。
那么为啥要从后往前遍历呢?
因为计数排序是一个稳定排序算法,这也就是为啥从后开始遍历的原因。原数组arr中在后面的元素,在排序后的数组res中也放在后边。我们在计数数组countArr中获取元素8在有序数组res中的位置时,若8在原数组arr中出现了多次,我们是先把最后一个8放到有序数组res中的。
有没有发现countArr中下标为0的位置一直没有赋值?这是因为arr中根本没有元素0,因此countArr也不需要计算0出现的次数,这里放上0其实只是为了更简单地理解计数排序的思想。下次如果无序数组的取值范围在[50,100]之间呢,那是不是也得统计[0,49]里面的元素出现的次数?它们都是出现0次,统计这个没意义,而且浪费空间。
因此,countArr的下标n统计的元素出现的次数就不是n出现的次数了。我们还需要求出原始数组的最小值min,下标n位置上的值就是元素n+min 在原始数组中出现的次数。
二、代码实现示例
package Sort;
import java.util.Arrays;
public class CountSort {
public static void main(String[] args) {
int[] arr = new int[] {8,4,5,7,1,3,6,2};
int[] res = countingSort(arr, arr.length);
System.out.println(Arrays.toString(res));
}
/**
* 计数排序
* @param arr 无序数组,假设数组中存储的都是非负整数。
* @param len 无序数组的长度
* @return
*/
public static int[] countingSort(int[] arr, int len) {
if (len <= 1) return null;
// 查找数组中数据的范围[0,max]
int max = arr[0];
for (int i = 1; i < len; ++i) {
if (max < arr[i]) {
max = arr[i];
}
}
// 申请一个计数数组countArr,下标大小[0,max]。记录数组中每个值的数量
int[] countArr = new int[max + 1]; // 0~max,长度为max+1
for (int i = 0; i <= max; ++i) { // 初始化每个元素值为0,即次数为0
countArr[i] = 0;
}
// 计算每个元素的个数,放入ccountArr中
for (int i = 0; i < len; ++i) {
countArr[arr[i]]++; // arr[i]的值对应在countArr中的索引
}
// 依次累加,得到num中元素值<=countArr当前索引值的元素数量
for (int i = 1; i <= max; ++i) {
countArr[i] = countArr[i-1] + countArr[i];
}
// res存储排序之后的结果
int[] res = new int[len];
// 计算排序的关键步骤,有点难理解
// 从后往前扫描num数组,
for (int i = len - 1; i >= 0; --i) {
int index = countArr[arr[i]]-1; // num中第n个元素,其索引为n-1
res[index] = arr[i]; // 赋值到res数组中
countArr[arr[i]]--; // 此时countArr对应位置的数就少了一个,需要减一
}
// 将结果拷贝给num数组,也可以直接返回res
// for (int i = 0; i < len; ++i) {
// arr[i] = res[i];
// }
return res;
}
}
三、算法分析
计数排序是稳定排序算法。
计数排序只能用在数据范围不大的场景中。
计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。
时间复杂度:O(n+max-min)
空间复杂度:O(max-min)
max和min分别为原始数组的最大值和最小值