一种O(n)的排序——计数排序引发的围观风波
前言
计算机课上,老师给一串数字6 1 6 9 9 1 4 2 1 5 8 8,问道:这一串数字,你们写个程序给我看,要求效率较高。学不出来的别下课了。
顿时场下一片哗然,但有很多小朋友硬着头皮啪啪啪的开始敲了。
老师走到pigpian身边,pigpian很难得皱了皱眉头
很难很难得写下了下面代码:
int a[]= {6,1,6,9,9,1,4,2,1,5,8,8};
for(int i=a.length-1;i>=0;i--)
{
for(int j=0;j<i;j++)
{
if(a[j]>a[j+1])
{
int team=a[j];
a[j]=a[j+1];
a[j+1]=team;
}
}
}
System.out.println(Arrays.toString(a));
老师:"gun吧,都2020年还用O(n2)的算法,快,快回去吃饭吧,快gun吧"。pigpian一脸无奈得走出教室,接着老师问道有没有其他人写出来,慢慢得挪到doudou得旁边。
doudou着急解释道:老师,你看我的O(nlogn)的快排算法:
int a[]= {6,1,6,9,9,1,4,2,1,5,8,8};
Arrays.sort(a);
System.out.println(Arrays.toString(a));
老师轻蔑的嘲讽道:"gun 吧,就知道投机取巧,我看你海!回去吃饭吧" 紧接着老师走到bigmao 的旁边,bigmao 给老师看了他的代码:
private static void quicksort(int [] a,int left,int right)
{
int low=left;
int high=right;
//下面两句的顺序一定不能混,否则会产生数组越界!!!very important!!!
if(low>high)
return;
int k=a[low];//取最左侧的一个作为衡量,最后要求左侧都比它小,右侧都比它大。
while(low<high)
{
while(low<high&&a[high]>=k)
{
high--;
}
//这样就找到第一个比它小的了
a[low]=a[high];
while(low<high&&a[low]<=k)
{
low++;
}
a[high]=a[low];
}
a[low]=k;
quicksort(a, left, low-1);
quicksort(a, low+1, right);
}
老师脸角泛起微光:"不错不错,手写快排还是挺棒的,回去吃饭吧!"。
此时bigsai举起他的小手手:"老师快来,我写的这个贼快"。bigsai亮起他的代码:
int a[]= {6,1,6,9,9,1,4,2,1,5,8,8};
int count[]=new int[10];
for(int i=0;i<a.length;i++)
{
count[a[i]]++;
}
int index=0;
for(int i=0;i<count.length;i++)
{
while (count[i]-->0) {
a[index++]=i;
}
}
System.out.println(Arrays.toString(a));
"不错不错,这个方法效率确实很高,你回去把这种排序的方法和大家分享一下吧!"老师惊艳道!
待bigsai出门后,站在门外的pigpian和doudou拦住问道:"sai哥这是啥东东啊"。
"计数排序。流程看图,听我下面慢慢讲:"
计数排序介绍
或许上面的代码你看起来还有点懵逼,但是不要紧,我们在这里给你讲明白什么是计数排序。对于计数排序,百度百科是这么说的:
计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为
Ο(n+k)
(其中k是整数的范围),快于任何比较排序算法。当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(n*log(n))
的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(n*log(n))
, 如归并排序,堆排序)
对于额外数组该如何理解呢? 我们慢慢来,在以前介绍桶排序的时候,我们知道每个桶里面是可以给一个范围的数字放进去。从每个桶的实质来看可以是List集合。
但如果每个桶中只有一种元素,那么这个桶就可以不需要使用集合去储存标记,而是用一个数字即可进行标记认为它出现了多少次。
所以这种每个桶只能放一种元素的,我们不需要每个桶再用List集合去装,而用数组的值储存对应编号出现的词数即可,例如上述的a[1]=2
表示其中的1号桶出现两次,而a[3]=0
表示元素3没有出现过。
而这样的数值如何计算呢?
- 很简单,对待排序目标序列遍历一次,每次遍历的值让这个值的编号加上1,说明对应元素词数加一。例如上述第一个1就a[1]++,第二个5就a[5]++……
- 然后取值时候遍历这个数字,顺序将目标编号的数字取出来即可。(每取一个对应位置数值减1直到为0为止)。例如上述遍历这个数组,就获得
1 1 2 4 4 5
这个序列。你看看,这个时间复杂度是不是O(n)的?
上面算法设计就很好了嘛?当然不是,如果是1,2 ,3之类数据肯定没啥问题,但是如果1000001,1000002,1000003之类的序列你这么开数组不是太多空间了?并且前面也要遍历很多无用的次数。
所以我们在设计具体算法的时候,先找到最小值min,再找最大值max。然后创建这个区间大小的数组,从min的位置开始计数,这样就可以最大程度的压缩空间,提高空间的使用效率。
代码实现
通过上述分析,计数排序的实现代码为:
import java.util.Arrays;
public class test {
public static void jishusort(int a[])
{
int min=Integer.MAX_VALUE;int max=Integer.MIN_VALUE;
for(int i=0;i<a.length;i++)//找到max和min
{
if(a[i]<min)
min=a[i];
if(a[i]>max)
max=a[i];
}
int count[]=new int[max-min+1];//对元素进行计数
for(int i=0;i<a.length;i++)
{
count[a[i]-min]++;
}
//排序取值
int index=0;
for(int i=0;i<count.length;i++)
{
while (count[i]-->0) {
a[index++]=i+min;//有min才是真正值
}
}
}
public static void main(String[] args) {
int a[]= {6,1,6,9,9,1,4,2,1,5,8,8};
jishusort(a);
System.out.println(Arrays.toString(a));
}
}
打印结果为:
[1, 1, 1, 2, 4, 5, 6, 6, 8, 8, 9, 9]
结语
通过上面我想计数排序你已经搞得很清楚了,计数排序的时间复杂度为O(n+k)其中k为正数范围;线性时间大部分都比其他排序快一点,但是也不一定,例如你遇到1 2 4 2 100001
这样一个序列,其中k的范围为10000,虽然他是O(n+k)=O(k)
k远大于n情况,但是此时O(k)>O(nlogn)
因为n太小,而K太大,需要遍历的词数太多了。
所以即使计数排序它是线性但是并非所有情况都是最好的方法,并且也占用了太多内存。当数据范围波动不是很大,数据相对比较集中,这时候用计数排序肯定是最好的啦,这点和桶排序的要求很像哦,没错,它其实就是一种特殊的桶排序,他的桶大小为1,用数值计数词数而以,其他都是一样的操作。
此时bigsai沾沾自喜终于讲完了,在旁边的pigpian和doudou直呼:讲的真的太好了,我不光要把它收藏下来,我还要给你点赞!
- Microsoft ASP.NET SignalR
- 飞机大战
- 译《ES6的6个小特性》
- 微信小程序从使用到分析快速解析
- andriod游戏音效
- JavaScript模块探索
- 数据层扩展包EFCachingProvider 总结
- Mac配置Maven
- FileSystemWatcher 导致Mono ASP.NET应用程序CPU使用率比较高
- 网卡收包流程
- Android 异步加载图片,使用LruCache和SD卡或手机缓存,效果非常的流畅
- Terminal &zsh &oh-my-zsh配置
- 【Python量化投资】基于网格优化、遗传算法对CTA策略进行参数优化
- 将我的 Windows Phone 应用程序更新到 Windows Phone 8
- 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 数组属性和方法
- ElasticSearch索引 VS MySQL索引
- Python随机打乱列表中的元素
- Python面试题汇总
- lldb 入坑指北(3) - 打印 c++ 实例的虚函数表
- 一文让你彻底搞懂`__str__`和`__repr__`?
- lldb 入坑指北(1) - 给Xcode批量添加启用&禁用断点功能
- Xcode 中的 Workspace、Project、Target 和 Scheme
- 学习Python一年,这次终于弄懂了浅拷贝和深拷贝
- 为速度而生的构建系统 - Ninja
- Python面试题:字符串连接
- Python面试突击
- 我半夜爬了严选的女性文胸数据,发现了惊天秘密
- 二分查找(Python实现)
- 图解JavaScript——代码实现【2】(重点是Promise、Async、发布/订阅原理实现)
- 编译器 bug 系列(1)