下面来看一下十大排序中的计数排序
目录
1.介绍
前面我们介绍的7中排序算法都是基于比较进行排序的,而计数排序不是基于比较进行排序的一种算法。
下面,我们来看一个例子。
首先,这里有一个无序数组[ 5,1,1,3,0 ],然后我们找到这个数组中的最大元素,即5,然后创建一个新数组,新数组的长度为5+1=6,即创建一个长度为6的数组。然后,我们用新数组的索引对应原数组的值,即原数组中 1 就对应新数组中索引1,数值 2 就对应新数组中索引2,这个很好理解,并且我们也能很清楚的明白为什么新数组长度为原数组中最大值+1了。之后,新数组中每个索引处存的值就是与新数组索引所对应的原数组的值的个数,比如新数组索引 0 ,对应原数组中数组0,而原数组中0有1个,所以新数组中索引0处就存1,如果没有对应的,就存0。之后,我们再遍历新数组,根据新数组的索引(即原始数组的元素)以及出现次数,生成排序后的内容。
计数排序的基本思想:
计数排序使用一个额外的数组C,其中第 i 个元素是待排序数组A中值等于 i 的元素的个数。
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),然后进行分配、收集处理:
① 分配。扫描一遍原始数组,以当前值-minValue作为下标,将该下标的计数器增1。
② 收集。扫描一遍计数器数组,按顺序把值收集起来。
实现逻辑
① 找出待排序的数组中最大和最小的元素
② 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
③ 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
④ 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
复杂度分析
平均时间复杂度:O(n + k)
最佳时间复杂度:O(n + k)
最差时间复杂度:O(n + k)
空间复杂度:O(n + k)
当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 O(n + k)。在实际工作中,当k=O(n)时,我们一般会采用计数排序,这时的运行时间为O(n)。
计数排序需要两个额外的数组用来对元素进行计数和保存排序的输出结果,所以空间复杂度为O(k+n)。
计数排序的一个重要性质是它是稳定的:具有相同值的元素在输出数组中的相对次序与它们在输入数组中的相对次序是相同的。也就是说,对两个相同的数来说,在输入数组中先出现的数,在输出数组中也位于前面。
计数排序的稳定性很重要的一个原因是:计数排序经常会被用于基数排序算法的一个子过程。我们将在后面文章中介绍,为了使基数排序能够正确运行,计数排序必须是稳定的。
2.代码实现
下面看一下计数排序的代码实现:
上面代码只能解决数组中非负数的元素,一旦数组中出现负数,就会报错,究其原因是因为数组没有负数索引,那怎么改进呢?
我们可以让数组中最小的负数对应count数组中索引0的位置,然后依次往后推,最大的元素对应count最大的索引的位置,这样形成一种映射关系。
下面,我们来看一下代码的实现:
代码如下:
package Sorts;
import java.util.Arrays;
//计数排序
public class CountSort {
public static void main(String[] args) {
int[] arr = {5,1,1,1,3,0};
System.out.println(Arrays.toString(arr));
countSort1(arr);
System.out.println(Arrays.toString(arr));
}
//数组中元素>=0
private static void countSort1(int[] arr) {
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max)
max = arr[i];
}
int[] count = new int[max+1];
for (int v : arr) { //原始数组的元素
count[v]++;
}
int k = 0;
for (int i = 0; i < count.length; i++) {
// i 代表原始数组中的元素,count[i]代表出现次数
while (count[i]>0){
arr[k++] = i;
count[i]--;
}
}
}
//不限制数组中元素的大小,可以有负数
private static void countSort2(int[] arr) {
//让数组中的最小值映射到数组的count[0]位置,最大值映射到count的最右侧
int min = arr[0];
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max)
max = arr[i];
if (arr[i] < min)
min = arr[i];
}
int[] count = new int[max - min + 1];
//原始数组元素 - 最小值 = count 索引
for (int v : arr) { //原始数组的元素
count[v - min]++;
}
int k = 0;
for (int i = 0; i < count.length; i++) {
// i+min 代表原始数组中的元素,count[i]代表出现次数
while (count[i]>0){
arr[k++] = i + min;
count[i]--;
}
}
}
}
3.总结与思考
初看这个算法是不是眼前一亮?还能这样排?但是认真的看完流程,发现其实一点都不难,甚至比之前的一些排序算法还要简单,那为什么我们之前就没想过这种方法?这是一个问题。
我一直都有这样的一种想法,我们学习学的不应该仅仅只是那些死知识,像什么数组啊、链表啊、栈啊、树啊再到后面的图啊等等,我们学的不应该只是这些死的东西,我们学的更多的应该是一种思维方式,是一种思维的拓展!比如你以前只会往前走,那么你学习之后,你学会了往左拐,然后你利用你学习的那种思维方式,你创造了右拐,甚至创造了转身,这才应该是我们学的。并且我认为知识是有限,我们现在学的东西是前人几百年智慧的浓缩,是前人智慧的精华。前人去芜留菁,把繁琐的实验验证、诞生过程全部给你省略,只留下几行极具逻辑与智慧的证明,然后我们学习那些前人总结的结论,然后学习那些逻辑的证明与推理,从中体会并吸收前人的思维方式,然后站在前人为我们留下的智慧与结论上,再开拓创新,这才是我们应该做的。学习,即要学那些知识,因为这些知识是你开拓创新的基础,还要学这些知识背后的逻辑推理过程,因为这是你进行创新的那一瞥灵光!
扯远了,回到计数排序。原版的计数排序没啥好说的,改进版的就是一种数学逻辑,只要好好的凑一凑,把那些数字捋一捋,都知道该怎么写。好了,就这么多。