1 计数排序
计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(n*log(n)),如归并排序,堆排序)。
以上来自于百度百科。
2 原理
简单来说计数排序就是不基于元素比较而是利用数组下标来确定元素的正确位置的算法,前提是需要进行排序的数组内的元素都是 int 型的。具体做法是创建一个长度为原始数组最大值 +1 的计数数组,然后遍历原始数组中所有元素,每一个元素出现时,计数数组中与该元素相等的下标的值就 +1,最后遍历计数数组,输出数组元素的下标值,元素的值是几,就输出几次,输出结果就是原始数组排序后的结果。
假设有一个数组 { 2, 3, 5, 6, 0, 1, 5, 6, 6, 8 },最大值为 8,所以我们创建一个长度为 9 的计数数组,计数数组中所有元素初始值都为 0,然后遍历原始数组,第 1 个值为 2,所以计数数组下标为 2 的元素值 +1,变成 1,第 2 个值为 3,所以计数数组下标为 3 的元素值 +1,变成 1,遍历完后计数数组就变成 { 1, 1, 1, 1, 0, 2, 3, 0, 1 },如下图:
最后我们将计数数组的下标顺序输出,下标对应元素的值是几就输出几次,输出结果为 { 0, 1, 2, 3, 5, 5, 6, 6, 6, 8 },原始数组就排序完成了。
3 代码实现
public static int[] countSort1(int[] origin) {
if (origin == null || origin.length == 0) {
return new int[]{};
}
System.out.println("origin--->" + Arrays.toString(origin));
// 获取数组中最大值
int max = origin[0];
for (int i = 0; i < origin.length; i++) {
if (origin[i] > max) {
max = origin[i];
}
}
System.out.println("max--->" + max);
// 根据最大值创建一个统计数组容器,数组每一个下标位置的值,代表了数列中对应整数出现的次数。
int[] countArray = new int[max + 1];
// 遍历数组,统计数组下标对应的整数每出现一次,统计次数 +1,即该下标的对应值 +1。
for (int i = 0; i < origin.length; i++) {
countArray[origin[i]]++;
}
System.out.println("countArray--->" + Arrays.toString(countArray));
// 遍历统计数组,根据统计数组排序
int[] sortedArray = new int[origin.length];
int index = 0;
for (int i = 0; i < countArray.length; i++) {
for (int j = 0; j < countArray[i]; j++) {
sortedArray[index] = i;
index++;
}
}
System.out.println("sortedArray--->" + Arrays.toString(sortedArray));
return sortedArray;
}
用数组 { 2, 3, 5, 6, 0, 1, 5, 6, 6, 8 } 运行一次结果如下:
4 优化
上面的初版实现是根据原始数组中最大值来创建计数数组的,假设最大值为 8 就会创建长度为 9 的计数数组,那么假设原始数组是 { 92, 93, 95, 96, 90, 91, 95, 96, 96, 98 },最大值是 98,那么就会创建长度为 99 的计数数组,但是实际上我们用到的下标还是 90-98 区间的元素,前面 0-89 下标的元素永远不会 +1,所以就造成了空间浪费。如何解决很简单,就是我们不根据原始数组的最大值创建计数数组,而根据最大最小值的差来创建计数数组,然后最小值作为一个偏移量,在遍历原始数组找到计数数组中对应下标时减去偏移量,最后输出结果时再加回来就可以了:
public static int[] countSort2(int[] origin) {
if (origin == null || origin.length == 0) {
return new int[]{};
}
System.out.println("origin--->" + Arrays.toString(origin));
// 获取数组中最大值、最小值
int max = origin[0];
int min = origin[0];
for (int i = 0; i < origin.length; i++) {
if (origin[i] > max) {
max = origin[i];
}
if (origin[i] < min) {
min = origin[i];
}
}
System.out.println("max--->" + max);
System.out.println("min--->" + min);
// 根据最大值与最小值的差创建一个统计数组容器,数组每一个下标位置的值,代表了数列中对应整数出现的次数。
int[] countArray = new int[max - min + 1];
// 遍历数组,统计数组下标对应的整数每出现一次,统计次数 +1,即该下标的对应值 +1。
// (因为统计数组的长度是根据最大最小值的差创建的,所以对应值要减去最小值,最小值相当于一个偏移量)
for (int i = 0; i < origin.length; i++) {
countArray[origin[i] - min]++;
}
System.out.println("countArray--->" + Arrays.toString(countArray));
// 遍历统计数组,根据统计数组排序
int[] sortedArray = new int[origin.length];
int index = 0;
for (int i = 0; i < countArray.length; i++) {
for (int j = 0; j < countArray[i]; j++) {
// 因为统计数组中的对应值减去最小值,所以这里要加回来,最小值相当于一个偏移量
sortedArray[index] = i + min;
index++;
}
}
System.out.println("sortedArray--->" + Arrays.toString(sortedArray));
return sortedArray;
}
5 终级优化
假如有一个学生数组:
我们将其年龄排序后是 { 18, 18, 19, 19, 19, 20, 22 },但是问题来了,在输出的时候,如果是年龄是唯一的,还能在原始数组中根据年龄取到对应的名字,但是如果是相同的年龄,就不知道对应的应该输出谁了,也就是这一段代码。
Student[] sortedArray = new Student[origin.length];
int index = 0;
for (int i = 0; i < countArray.length; i++) {
for (int j = 0; j < countArray[i]; j++) {
// 因为统计数组中的对应值减去最小值,所以这里要加回来,最小值相当于一个偏移量
Student student=new Student();
student.setAge(i + min);
// setName() 是不知道该取原始数组中哪个元素的 name
sortedArray[index] = student;
index++;
}
}
要解决这个问题也很简单,我们需要将计数数组做一下变形,让计数数组的每一个元素都等于前面所有元素的和,这样计数数组存储的元素值就是对应整数的最终排序位置了,比如下标为 4 的元素值为 7,就代表原始数组中的整数 4 的最终排序为 5。上面的学生数组原本的计数数组为 [2, 3, 1, 0, 1],那么经过变形后计数数组 [2, 5, 6, 6, 7],在输出结果时我们从后往前遍历,首先遍历最后一个学生小紫的年龄为 22,减去最小值(偏移量) 18 后为 4,所以找到计数数组中下标为 4 的元素,值为 7,代表小紫的最终排序为第 7 位,同时给计数数组中下标为 4 的元素的值减 1 变成 6,代表下次遇到年龄为 22 的学生时(实际上遇不到了)最终排序是 6。然后遍历倒数第二行的学生小蓝年龄为 18,减去最小值 18 后为 0,所以找到计数数组中下标为 0 的元素,值为 2,代表小蓝的最终排序为第 2 位,同时给计数数组中下标为 0 的元素的值减 1 变成 1,代表下次遇到年龄为 18 的学生时(小红)最终排序是 1,以此类推。
这样年龄相同的学生我们也可以排除顺序了,说起来复杂,用代码来实现的时候其实只需要修改两个地方:
public static Student[] countSort3(Student[] origin) {
if (origin == null || origin.length == 0) {
return new Student[]{};
}
System.out.println("origin--->" + Arrays.toString(origin));
// 获取数组中最大值、最小值
int max = origin[0].getAge();
int min = origin[0].getAge();
for (int i = 0; i < origin.length; i++) {
if (origin[i].getAge() > max) {
max = origin[i].getAge();
}
if (origin[i].getAge() < min) {
min = origin[i].getAge();
}
}
System.out.println("max--->" + max);
System.out.println("min--->" + min);
// 根据最大值与最小值的差创建一个统计数组容器,数组每一个下标位置的值,代表了数列中对应整数出现的次数。
int[] countArray = new int[max - min + 1];
// 遍历数组,统计数组下标对应的整数每出现一次,统计次数 +1,即该下标的对应值 +1。
// (因为统计数组的长度是根据最大最小值的差创建的,所以对应值要减去最小值,最小值相当于一个偏移量)
for (int i = 0; i < origin.length; i++) {
countArray[origin[i].getAge() - min]++;
}
System.out.println("countArray--->" + Arrays.toString(countArray));
// 统计数组变形,每一个元素都加上前面所有元素之和
// 这样做的目的是为了让统计数组存储的元素值等于相应整数的最终排序位置
// 如下标 4 的元素值为 5,代表原始数列的整数 4(加上偏移量) 的最终排序在第 5 位
int sum = 0;
for (int i = 0; i < countArray.length; i++) {
sum += countArray[i];
countArray[i] = sum;
}
System.out.println("countArray--->" + Arrays.toString(countArray));
// 遍历统计数组,根据统计数组排序
Student[] sortedArray = new Student[origin.length];
int index = 0;
for (int i = origin.length - 1; i >= 0; i--) {
// 从后往前遍历,最后一个如果是 19,减去偏移量为 9,
// 找到 countArray 中下标为 9 的元素,值是 10,代表这个元素最终位置应该是在第 10 位(比如是第 10 位,在数组中的下标应该 -1,是
// 9)。
// 同时给 countArray 中下标为 9 的元素值 -1,变成 9,代表下次再遇到 19 时应该排在第 9 位。
sortedArray[countArray[origin[i].getAge() - min] - 1] = origin[i];
countArray[origin[i].getAge() - min]--;
}
System.out.println("sortedArray--->" + Arrays.toString(sortedArray));
return sortedArray;
}
与优化版的技术排序相比,主要是得到计数数组后再对计数数组做一下变形,然后在输出结果时有些不同
学生类:
private class Student {
private String name;
private int age;
private Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age='" + age + '\'' +
'}';
}
}
运行结果如下:
上面的排序是为了测试学生,所以把数组都改成 Student 数组,其实应该是 int 型的数组:
// 统计排序最终版,根据数组的最大最小值的差值创建统计数组,不会造成空间浪费。
// 排序后的数组中如果有相同的元素,会根据出现的顺序排序
// [14, 16, 16, 15, 10, 10, 10, 12, 17, 19]
public static int[] countSort(int[] origin) {
if (origin == null || origin.length == 0) {
return new int[]{};
}
System.out.println("origin--->" + Arrays.toString(origin));
// 获取数组中最大值、最小值
int max = origin[0];
int min = origin[0];
for (int i = 0; i < origin.length; i++) {
if (origin[i] > max) {
max = origin[i];
}
if (origin[i] < min) {
min = origin[i];
}
}
System.out.println("max--->" + max);
System.out.println("min--->" + min);
// 根据最大值与最小值的差创建一个统计数组容器,数组每一个下标位置的值,代表了数列中对应整数出现的次数。
int[] countArray = new int[max - min + 1];
// 遍历数组,统计数组下标对应的整数每出现一次,统计次数 +1,即该下标的对应值 +1。
// (因为统计数组的长度是根据最大最小值的差创建的,所以对应值要减去最小值,最小值相当于一个偏移量)
for (int i = 0; i < origin.length; i++) {
countArray[origin[i] - min]++;
}
System.out.println("countArray--->" + Arrays.toString(countArray));
// 统计数组变形,每一个元素都加上前面所有元素之和
// 这样做的目的是为了让统计数组存储的元素值等于相应整数的最终排序位置
// 如下标 4 的元素值为 5,代表原始数列的整数 4(加上偏移量) 的最终排序在第 5 位
int sum = 0;
for (int i = 0; i < countArray.length; i++) {
sum += countArray[i];
countArray[i] = sum;
}
System.out.println("countArray--->" + Arrays.toString(countArray));
// 遍历统计数组,根据统计数组排序
int[] sortedArray = new int[origin.length];
int index = 0;
for (int i = origin.length - 1; i >= 0; i--) {
// 从后往前遍历,最后一个如果是 19,减去偏移量为 9,
// 找到 countArray 中下标为 9 的元素,值是 10,代表这个元素最终位置应该是在第 10 位(比如是第 10 位,在数组中的下标应该 -1,是
// 9)。
// 同时给 countArray 中下标为 9 的元素值 -1,变成 9,代表下次再遇到 19 时应该排在第 9 位。
sortedArray[countArray[origin[i] - min] - 1] = origin[i];
countArray[origin[i] - min]--;
}
System.out.println("sortedArray--->" + Arrays.toString(sortedArray));
return sortedArray;
}
6 总结
时间复杂度
假设原始数组长度为 n,最大值最小值差值为 m,那么时间复杂度为 O(n+m)。
空间复杂度
假设原始数组长度为 n,最大值最小值差值为 m,那么空间复杂度为 O(m)。
优点
1.不基于比较,时间线型变化。
2.最终优化后的计数排序元素顺序可确定,是稳定排序。
缺点
1.当原始数组最大值最小值差值过大时不适用,如给定 20 个随机整数,范围从 0 到 100000000 之间,会创建长度为 100000000 的数组,造成严重的空间浪费,时间复杂度也随之提升。
2.当原始数组的元素不是整数时无法使用,因为原始数组中的元素对应计数数组的下标,但是下标只能是小数,所以如果原始数组中的元素为小数时无法使用计数排序。