计数排序(Counting Sort)
本节内容,继续介绍排序算法,在本节内容之前,介绍过7种排序算法,那计数排序算法,对比前面的几种排序算法, 有没有不一样呢?请继续往下看。
- 前面介绍的冒泡,选择,插入,归并,快速,希尔,堆排序,都是基于比较的排序,这些基于比较的排序,有以下几个特点
- 平均时间复杂度最低的是O(nlogn)
- 而本节内容介绍的计数排序,不是基于比较的排序。其中不基于比较的排序还有桶排序,基数排序等
- 它们是典型的用空间换时间,在某些时候,平均时间复杂度可以比O(nlogn)更低,也就是说,在某些时候,这种利用空间换时间的排序算法,性能比前面基于比较的排序算法更快
计数排序是在1954年由Harold H.Seward提出,适合对一定范围内的整数进行排序。后面会讲为什么要这样规定
计数排序核心思想
统计每个整数在序列中出现的次数,进而推导出每个整数在有序序列中的索引
计数排序简单实现
例如将下图的这一堆整数,进行计数排序
然后利用一个数组,对应元素作为数组的索引,然后来记录每一个元素出现的次数,所以上图中的序列,对应到数组中出现的次数,可以用下图来进行表示
3出现1次,4出现1次,5出现2次,6出现1次,7出现2次,8出现1次。通过这样统计,每一个整数出现的次数都记录下来了。并且利用这种方式来存储每个元素出现的次数,则数组的大小取决于序列中最大的元素。这种设计虽然有很大的缺陷,但是却可以完成排序,并且在后面会对这种方式进行优化。
那看到这里,你可能会想,知道这些元素出现的次数,有什么用呢,就能完成排序了吗?是的,知道元素出现的次数后,就可以推断出元素在有序序列中对应的位置。接下来就是对索引数组进行扫描,从0位置开始扫描,依次将出现次数不为0的索引,按照出现次数进行依次排序,最终就可以将序列排好序。所以统计完成后的序列为
接下来,可以利用这种思路,尝试将算法进行实现。
protected void sort() {
//找出最大值
int max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
}
//开辟内存空间,存储每个整数出现的次数
int[] counts = new int[max + 1];
//统计每个整数出现的次数
for (int i = 0; i < array.length; i++) {
counts[array[i]]++;
}
//根据整数出现次数,对整数进行排序
int index = 0;
for (int i = 0; i < counts.length; i++) {
if (counts[i] > 0) {
while (counts[i]-- > 0) {
array[index++] = i;
}
}
}
}
写出了这个简单计数排序后,来观察以下现在这个算法存在哪些问题
- 无法对负整数进行排序
- 极其浪费内存空间
- 是一个不稳定排序
- ...
接下来,就可以对上面的算法进行改进了。那应该如何改进呢?首先,我觉得应该从内存空间开始
由于用来计数的数组,是根据序列中的最大值来申请的,所以当如果像上面例子中,最小值都比较大时,数组前面就会空出很大的空间,造成空间的浪费,所以可以利用最大值 - 最小值 + 1的值来创建用来计数的数组。
改进
下图表示要进行排序的序列
根据上面的内存优化思路,则可以通过下图的这种方式来存储数据
由于现在优化后,不是使用索引直接与元素值进行对应,所以巧妙的将不能对负整数进行排序的问题也解决了,因为现在是将索引统一减去了最小值的大小,所以,也可以存储负整数了。
那如何才能办到,变为一个稳定的排序呢?那可以进行另外一个优化,以前,在数组中存储的是每一个整数出现的次数,现在将存储的值改为每次出现的次数累加加上前面的所有次数,这样得到的就是元素在有序序列中的位置信息[下图]
次数解释:
- 例如现在的元素8,次数是8,这个值是累加前面的所有次数 + 自己出现的次数,即1 + 1 + 2 +1 + 2 +1 = 7 + 1 = 8,所以在8前面有7个元素,并且8出现了1次,索引为7
- 例如7现在的次数是7,这个字是通过前面的次数1 + 1 + 2 + 1 + 2 = 5 + 2 = 7,所以在7前面有5个元素,7出现了2次,索引为5/6
所以上面每个元素的索引是
元素 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|
索引次数 | 1 | 2 | 4 | 5 | 7 | 8 |
出现次数 | 1 | 1 | 2 | 1 | 2 | 1 |
真正索引 | 1 - 1 = 0 | 2 - 1 = 1 | 4 - 2 = 2/3 | 5 - 1 = 4 | 7 - 2 = 5/6 | 8 - 1 = 7 |
所以,根据最终的索引排序后的结果为
根据上面的的优化,可以得出下面的结论
- 假设数组中的最小值是min
- 数组中的元素k对应在counts数组中的索引为k - min
- 数组中的元素k在有序序列中的索引
- counts[k - min] - p(p表示倒数第几个k)
有这个结论后,可以根据结论计算
- 元素8在有序序列中的索引,counts[8 - 3] - 1 结果为7
- 第一个元素7在有序序列中的索引为:counts[7 - 3] - 1,结果为6
- 第二个元素7在有序序列中的索引为:counts[7 - 3] - 2,结果为5
然后,再对原来的数组进行遍历,并且创建一个容量相等了新数组,根据原来数组中的元素,直接去counts数组中去获取真正的索引。这样就能直接得到新数组对应的位置,并且可以保证是稳定排序算法。
根据上面的分析,最终得到的代码如下
protected void sort() {
// 找出最值
int max = array[0];
int min = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
if (array[i] < min) {
min = array[i];
}
}
// 开辟内存空间,存储次数
int[] counts = new int[max - min + 1];
// 统计每个整数出现的次数
for (int i = 0; i < array.length; i++) {
counts[array[i] - min]++;
}
// 累加次数
for (int i = 1; i < counts.length; i++) {
counts[i] += counts[i - 1];
}
// 从后往前遍历元素,将它放到有序数组中的合适位置
int[] newArray = new int[array.length];
for (int i = array.length - 1; i >= 0; i--) {
newArray[--counts[array[i] - min]] = array[i];
}
// 将有序数组赋值到array
for (int i = 0; i < newArray.length; i++) {
array[i] = newArray[i];
}
}
这样就将整个排序算法,进行了实现。现在与前面的几种算法进行比较,得到的结果如下
从结果来看,可以看到,计数排序的耗时表现非常优秀
复杂度分析
空间复杂度:O(n + k)
最好,最坏,平均时间复杂度:O(n + k)
k是整数的取值范围
属于稳定的排序算法
对自定义对象进行排序
如果自定义对象可以提供用以排序的整数类型,依然可以使用技术排序
完!
网友评论