漫画:什么是计数排序?
计数排序
计数排序(Counting Sort)是一种针对于特定范围之间的整数进行排序的算法。它通过统计给定数组中不同元素的数量(类似于哈希映射),然后对映射后的数组进行排序输出即可。
我们以数组 [1,4,1,2,5,2,4,1,8]
为例进行说明。
第一步:建立一个初始化为 0 ,长度为 9 (原始数组中的最大值 8 加 1) 的数组 count[]
:
第二步:遍历数组 [1,4,1,2,5,2,4,1,8]
,访问第一个元素 1 ,然后将数组 count[]
中下标为 1 的元素加 1,表示当前 1 出现了一次,即 count[1] = 1
;
第三步:访问数组 [1,4,1,2,5,2,4,1,8]
的第二个元素 4 ,然后将数组 count[]
中下标为 4 的元素加 1 ,表示当前访问的元素 4 当前出现了 1 次,即 count[4] = 1
;
第四步:访问数组 [1,4,1,2,5,2,4,1,8]
的第三个元素 1 ,然后将数组 count[]
中下标为 1 的元素加 1,即 count[1] = 2
,表示当前 1 出现了 2 次:
第五步:访问数组 [1,4,1,2,5,2,4,1,8]
的第四个元素 2 ,然后将数组 count[]
中下标为 2 的元素加 1,即 count[2] = 1
,表示当前 2 出现了 1 次:
第六步:访问数组 [1,4,1,2,5,2,4,1,8]
的第五个元素 5 ,然后将数组 count[]
中下标为 5 的元素加 1,即 count[5] = 1
,表示当前 5 出现了 1 次:
第七步:访问数组 [1,4,1,2,5,2,4,1,8]
的第六个元素 2 ,然后将数组 count[]
中下标为 2 的元素加 1,即 count[2] = 2
,表示当前 2 出现了 2 次:
第八步:访问数组 [1,4,1,2,5,2,4,1,8]
的第七个元素 4 ,然后将数组 count[]
中下标为 4 的元素加 1,即 count[4] = 2
,表示当前 4 出现了 2 次:
第九步:访问数组 [1,4,1,2,5,2,4,1,8]
的第八个元素 1 ,然后将数组 count[]
中下标为 1 的元素加 1,即 count[1] = 3
,表示当前 1 出现了 3 次:
第十步:访问数组 [1,4,1,2,5,2,4,1,8]
的第九个元素 8 ,然后将数组 count[]
中下标为 8 的元素加 1,即 count[8] = 1
,表示当前 8 出现了 1 次:
此时遍历数组 [1,4,1,2,5,2,4,1,8]
结束,我们得到了一个 count[]
数组,而只要得到了这个count[]
数组,我们的排序算法就相当于结束了,接下来的就只是输出了。
如果不考虑计数排序的稳定性,我们按照数组 count[]
中对应下标的出现次数直接输出即可:
for(int i = 0; i < count.length; i++){
if (count[i] != 0){
for(int j = 0; j < count[i]; j++){
System.out.print(i + " ");
}
}
}
为了保证计数排序的稳定性,我们又该如何做呢?
先不考虑这么复杂,但是从宏观的角度来看,我们的目的就是找到待排序数组中每一个元素在排序后数组当中的正确位置。
首先看一下 count[]
数组本身, 数组中的 0 对于我们的输出没有任何影响,所以我们可以考虑将其直接去掉:
那么此时的我们就可根据去掉之后的数组得到排序后数组的一个轮廓图:
但是这样我们并不知道相同的数字在对应原始数组 arr[]
中的哪一个元素,就相当于直接输出,而没有考虑元素的相对顺序;但是对这个过程的理解有助于我们接下来理解稳定性的处理过程。
我们看到,数组 count[]
中的每一个值表示它所对应的下标在排序后数组的出现次数,那么我们遍历数组(下标从 1 开始),并对数组 count[]
中的每一个元素执行 count[i] = count[i] + count[i-1] 会得到什么呢?
此时得到新的 count[]
将表示他们的位置信息,比如 3 表示它的下标 1 一定出现在前 3 的位置;而紧接其后 5 则表示下标 2 出现在第 4 和第 5 个位置;下标为 3 的 count[3] = 5
,其与前面的 count[2]
值相同,两者之差也就表示其出现次数,0 次,所以不占位置;下标为 4 的位置值为 7 ,则表示下标 4 出现在第 6 和 7 的位置,依次类推,你也可以对新的 count[]
数组中的每一个元素做出解释。
但我们怎么可能停留在这里呢?
有了这个新的 count[]
数组,我们如何得到元素数组 arr[]
在排序后的输出数组 output[]
中的正确位置呢?
回答了这个问题,稳定的计数排序也就彻底理解了~~
第一步:从后向前遍历,具体为什么是从后向前,看完了你就会明白了!首先是 i = n-1 = 8
,然后计算出 arr[i] = arr[8] = 8
在排序后数组的正确位置 count[arr[i]] - 1 = count[8] - 1 = 8
,即排序后 arr[8]
的正确位置为 8 ,然后将 arr[8]
赋值给 output[8] = 8
,但是 count[arr[8]] = count[8]
减 1 :
第二步:i = n - 2 = 7
,然后计算 arr[7] = 1
在排序后数组的正确位置 count[arr[7]] - 1 = count[1] - 1 = 2
,即最后一个 1 在排序后的正确位置下标为 2 ,然后将 count[arr[7]]
的值减 1 。这里为什么要减 1 ,因为我们已经找到了最后一个 1 的正确位置,目前就剩余两个 1 没有找到正确位置。
以此类推,就可以得到原数组 arr[] 中每一个元素在排序后的正确位置
这就是稳定的计数排序,那我们再来回答一下为什么从后向前遍历新的 count[]
数组?
因为只有这样才能保证计数排序的稳定性!比如原始数组 arr[]
中 3 个 1 的在排序后的相对位置就没有发生变化,依旧保持:
实现代码
public class CountingSort {
public void countingSort(int arr[]) {
int n = arr.length;
int output[] = new int[n];
int count[] = new int[256];
for(int i = 0; i < 256; i++) {
count[i] = 0;
}
for(int i = 0; i < n; i++) {
++count[arr[i]];
}
for(int i = 1; i <= 255; i++) {
count[i] += count[i-1];
}
for(int i = n-1; i >= 0; i--) {
output[count[arr[i]] - 1] = arr[i];
--count[arr[i]];
}
for(int i = 0; i < n; i++) {
arr[i] = output[i];
}
}
public static void main(String args[]) {
CountingSort os = new CountingSort();
int arr[] = {1,4,1,2,5,2,4,1,8};
os.countingSort(arr);
for(int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + ",");
}
}
}
可是问题又来了,如果我们的数组变成了 arr[] = {-1,4,1,-2,5,-2,4,-1,8} ,上面介绍的计数排序的实现方式不就出现了问题吗?因为数组下标也没有负数的情况呀!
我们只需要找到数组 arr[] = {-1,4,1,-2,5,-2,4,-1,8} 中的最小值 min = -2 ,以及最大值 max = 8 ,然后开辟一个大小为 max - min + 1
的 count[] 数组,统计出数组当中每一个元素出现的次数即可,就像下面这样:
其中数组 arr[] 的最小值 min = -2 ,-2
被映射到了 count[] 数组下标为 0 的位置,原数组中包含 2 个 -2
,所以 count[0] = 2 ;原数组 arr[] 当中有 3 个 -1 ,其中 -1 - (-2) = 1
,也就说 -1
映射到了 count[] 数组下表为 1 的位置,所以 count[1] = 3 .
得到了 count[] 数组,之后的操作还不简单吗?记得自己调试一下奥!!!
改进的计数排序实现:
import java.util.*;
class CountingSort
{
static void countSort(int[] arr)
{
int max = Arrays.stream(arr).max().getAsInt();
int min = Arrays.stream(arr).min().getAsInt();
int range = max - min + 1;
int count[] = new int[range];
int output[] = new int[arr.length];
for (int i = 0; i < arr.length; i++)
{
count[arr[i] - min]++;
}
for (int i = 1; i < count.length; i++)
{
count[i] += count[i - 1];
}
for (int i = arr.length - 1; i >= 0; i--)
{
output[count[arr[i] - min] - 1] = arr[i];
count[arr[i] - min]--;
}
for (int i = 0; i < arr.length; i++)
{
arr[i] = output[i];
}
}
static void printArray(int[] arr)
{
for (int i = 0; i < arr.length; i++)
{
System.out.print(arr[i] + " ");
}
System.out.println("");
}
public static void main(String[] args)
{
int[] arr = {-1,4,1,-2,5,-2,4,-1,8};
countSort(arr);
printArray(arr);
}
}
复杂度分析
时间复杂度
在整个代码实现过程中,我们仅仅出现了一层的 for 循环,没有出现任何 for 循环的嵌套,所以计数排序的时间复杂度为
量级。
空间复杂度
由于计数排序过程中,我们使用到了一个 max - min + 1
大小的 count[] 数组,所以计数排序的空间复杂度为
量级。
优缺点分析
- 如果输入数据的范围
range = max - min + 1
不明显大于要待排序数组的长度n = arr.length
,则计数排序是相当高效的,比时间复杂度为
的快速和归并排序都优秀。
- 计数排序不是基于比较的排序算法,时间复杂度为
,空间复杂度与数据范围成正比。
- 计数排序通常用作另一个排序算法(例如基数排序)的子过程。
- 计数排序可以使用部分哈希(partial Hashing)在
的时间内统计数据的频率。
- 计数排序适用于负输入。
- 计数排序不适用于小数的情况。
最后再强烈推荐一下之前推荐 过的一个网站:https://visualgo.net/en/sorting?slide=1 !一定对你学习排序算法有帮助~~
来个直击灵魂的三连吧!
- PHP数据结构(二十五) ——并归排序
- PHP数据结构(二十六) ——基数排序实现36进制数排序
- Apache配置
- jquery事件
- 设计模式专题(二)——策略模式
- ASP.NET AJAX(10)__Authentication ServiceAuthentication ServiceAuthentication Service属性Authentication
- 高效开发 MVVM 和 databinding 你需要使用的工具
- ASP.NET AJAX(9)__Profile Service什么是ASP.NET Profile如何使用ASP.NET ProfileProfile ServiceProfile Service预
- 设计模式专题(三)——装饰模式
- ASP.NET AJAX(8)__Microsoft AJAX Library中异步通信层的使用什么是异步通信层Micorsoft AJAX Library异步通信层的组成WebRequestExec
- ASP.NET AJAX(7)_Microsoft AJAX Library扩展客户端组件继承时需要注意的问题扩展类型如何修改已有类型
- ASP.NET AJAX(6)__Microsoft AJAX Library中的面向对象类型系统命名空间类类——构造函数类——定义方法类——定义属性类——注册类类——抽象类类——继承类——调用父类方
- 设计模式专题(四)——代理模式
- Array数组函数(一)
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- ConcurrentDictionary线程不安全么,你难道没疑惑,你难道弄懂了么?
- 【一起学系列】之迭代器&组合:虽然有点用不上啦
- 移动端touch事件影响click事件以及在touchmove添加preventDefault导致页面无法滚动的解决方法
- 使用ActionFilterAttribute 记录 WebApi Action 请求和返回结果记录
- scipy.stats连续分布的基本操作
- InvocationHandler中invoke方法中的第一个参数proxy的用途
- height、offsetheight、clientheight、scrollheight、innerheight、outerheight
- mysql sql-mode 解析和设置
- JAVABEAN EJB POJO区别
- @Component和@Bean以及@Autowired、@Resource
- mybatis generator and 和or条件
- 『.Net反射』ILGenerator.Emit 动态MSIL 编程
- Spring通过XML配置文件以及通过注解形式来AOP 来实现前置,后置,环绕,异常通知
- 切面编程(环绕通知与前后置通知区别)
- Spring在代码中获取bean的几种方式