上一篇排序算法<1>中,排序算法的时间复杂度从N2到NlgN变化,但他们都有一个共同的特点,基于比较和交换数组中的元素来实现排序,我们称这些排序算法为比较排序算法。对于比较排序算法,所有的算法都可以表达成一个决策树的模型(参看MIT算法课),数的叶子节点表示比较排序的一种可能结果,树的深度为得到排序结果经过的决策次数。可以证明:N个元素最少要经过NlgN次决策才能得到排序结果。所以基于比较的排序算法时间复杂度最优情况下为NlgN,由此可知:快排(平均情况下),归并,堆排序三者都是渐进最优的。工程上较多使用快排的原因时它的优点:1、虽然它时间复杂度是从NlgN到N2变化的,但平均状况下它是NlgN的;2、相比于归并,空间复杂度更低;3、相比于堆排序,更高效的利用了计算机缓存。另外:实际应用中的排序算法,多数是综合了的排序算法,比如在N较小时使用插入排序,N较大时使用快排。因为N较小时,快排递归调用占用了排序时间的很大成本。

  如果突破比较排序模型的限制,有没有更快的算法呢,比如时间复杂度为N的算法?有的,下面要介绍三种时间复杂度为N的算法:计数排序、基数排序、桶排序。

1、计数排序/count sort

  计数排序的模型是这样的,假设数组中所有元素都是[0,k]的整数,我们可以建立一个常为k+1的计数数组,统计原数组中各元素出现的次数,将信息整合到计数数组中,计数数组索引为原数组的元素,计数数组的元素为原数组对应元素的出现次数。如int[] a=[5,4,3,6,2,1,7,0,6,3],计数数组为int[] count=[1,1,1,2,1,1,2,1],计数数组已经完全可以包含元素的信息。1个0,1个1,2个3等等,从前往后遍历计数数组,即可得到原数组的排序。代码如下:

 import java.util.Arrays;
public class Test{
public static int[] countSort(int[] a){
//找出数组中最大元素k
int k=a[0];
for(int i=0;i<a.length;i++)
if(a[i]>k) k=a[i];
//辅助数组
int[] b=countSort(a,k);
return b;
} private static int[] countSort(int[] a,int k){
int[] count=new int[k+1];//计数数组 //更新计数数组,排序数组信息存储在计数数组中
for(int i=0;i<a.length;i++)
count[a[i]]++; //更新计数数组中信息表达方式
for(int i=1;i<=k;i++)
count[i]+=count[i-1]; //根据计数数组和原数组更新排序后数组
int[] b=new int[a.length];
for(int i=a.length-1;i>=0;i--){
b[count[a[i]]-1]=a[i];
count[a[i]]--;
}
return b;
} public static void main(String[] args){
int[] a={5,4,3,6,2,1,7,0,6,3};
a=countSort(a);
System.out.println(Arrays.toString(a));
}
}

  这段代码有几个要注意的点:1,前面我们的排序函数都是没有返回值的,这里排序函数返回了一个数组,这是由于这里排序后的数组是新创建的一个数组,基于java引用数据参数传递的机制,所以返回了一个数组。2,在更新完计数数组后,计数数组中已经包含了原数组所有的信息,所以可以直接根据计数数组更改原数组,这样空间利用率更高。3,好像计数数组并不需要一定是k+1长的,保证原数组所有元素可以在计数数组中表示即可,比如原数组中数为9900-10000的元素。计数数组成为101即可。下面给出更新后的代码:

 import java.util.Arrays;
public class Test{
public static void countSort(int[] a){
//找出a中元素最大值和最小值
int hi=a[0],lo=a[0];
for(int i=0;i<a.length;i++){
if(a[i]>hi) hi=a[i];
else if(a[i]<lo) lo=a[i];
} //构建计数数组,更新计数数组信息
int[] count=new int[hi-lo+1];
for(int i=0;i<a.length;i++)
count[a[i]-lo]++; //根据计数数组信息,更新原数组,原数组即为排序后数组
int k=0; //更新原数组的索引
for(int i=0;i<=hi-lo;i++)
while(count[i]>0){
a[k++]=lo+i;
count[i]--;
}
} public static void main(String[] args){
int[] a={5,4,3,6,2,1,7,0,6,3};
countSort(a);
System.out.println(Arrays.toString(a));
}
}

  这段代码是非常好的计数排序的实现。下面来分析下计数排序的局限性及性能:

性能:计数排序的性能是与数组元素的范围k相关的,N+k,可以看到当k很小时,计数排序的时间复杂度是线性的,~cN,当k远大于N时,时间复杂度可以变得很高。

局限:计数排序尽管当k值不大时,能达到线性的时间复杂度,但当数组中元素波动很大,导致k值很大,时间复杂度很高,更阔怕的是空间复杂度会变得很高。另外,由于计数排序是使用计数数组的索引表示元素值,用值表示原数组元素出现次数,所以就要求排序对象是[0,k]的整数(负数做下转换也是可以的)。这大大降低了这种算法的通用性。尽管如此,计数排序算法在很多地方还是很有用的,比如统计排序全国高考生的数学成绩,比如统计排序全国人民的年龄等。

2.基数排序

  想象我们有这样一个数组[329,457,657,839,435,720,355],我们使用另外一种排序方法进行排序,先使用稳定排序算法将数组中元素按照个位排序,得到[720,435,355,457,657,329,839],再将这里得到的数组用稳定排序算法将元素按十位排序,得到[720,329,435,839,355,457,657],然后按照百位排序,得到[329,355,435,457,657,720,839],MD的,数组变有序了。怎么做到的呢?可以证明一下:

  对于数组中的元素,如果位数t下的t-1位有序,那么在稳定排序算法下,t位排序分两种情况,1,元素t位相等,因为他们t-1是有序的,所以想等位也有序。2,元素t位不等,那么将它们排序。二者结合,显然t位也是有序的(注意这里一定使用的是稳定排序算法)。

  思想已经知道了,就是将元素从低位到高位依次排序,那么选择什么稳定的排序算法呢?上面讲到的计数排序是一个好选择,因为一定条件下,计数排序的时间复杂度是线性的。我们使用计数排序部分第一块代码,稍作修改,得到我们的基数排序:

 import java.util.Arrays;
public class Test{
public static int[] radixSort(int[] a,int b){
for(int i=1;i<=b;i++){
a=countSort(a,i);
}
return a;
} private static int[] countSort(int[] a,int b){
//如果我们将数字限制在十进制,那么只需要构造10个桶
final int radix=10;
int[] count=new int[radix]; //按照数组a更新桶信息
for(int i=0;i<a.length;i++)
count[value(a[i],b)]++; //更新计数数组中信息的表达方式
for(int i=1;i<radix;i++)
count[i]+=count[i-1]; //根据计数数组获得排序后的数组
int[] aux=new int[a.length];
for(int i=a.length-1;i>=0;i--){
int k=value(a[i],b);
aux[count[k]-1]=a[i];
count[k]--;
}
return aux;
} private static int value(int a,int b){
int[] A={1,1,10,100,1000,10000};
return a/A[b]%10;
} public static void main(String[] args){
int[] a={329,457,657,839,436,720,355};
a=radixSort(a,3);
System.out.println(Arrays.toString(a));
}
}

这段代码跟计数部分代码1有很多共同点,其实就是重复利用代码1,稍作修改。之所以不用计数代码2,是因为这里排序必须要构造辅助数组,因为计数数组无法保存原数组的所有信息。下面分析下基数排序的性能和局限。

性能:通过观察代码可以发现,基数排序多次利用了计数排序,所以它的性能和计数排序有很大关系。关键时这里计数排序k值得选择。以及基数排序元素的位数。上面代码中的情况是元素为10进制,事实上计数排序count的大小可以任意选择(计算机保持合适性能),二进制也可以。那么count选多大合适呢?只要确保桶k<lgN,(MIT算法课程上有性能的分析和证明)。

局限:基数排序可以看做一个增强了的计数排序,增强的部分在于大大扩展了k的范围,而时间复杂度保持不变。但计数排序的一些局限依然保留在基数排序中,另外一点是,很多情况下基数排序比快排等要慢,因为它无法高效利用计算机的缓存。它看起来是非常优美好用的算法,只是在实践上有些遗憾。

更抽象的层面上,基数排序可以类比于桶排序。构造一个个的桶,将数据依次放入不同的桶里,然后从桶里将数据依次倒出,然后拼接。这是桶排序的思想。计数排序也近似于一种桶排序。上面代码中桶是由计数数组和辅助数组来实现。也可以以队列这种数据结构来实现。

想象一下,如果一个人对上面一堆数进行排序,他一定会从高位往低位排,先根据高位排序,再到低位。这种也是可以实现的。我们根据高位将数据丢入一个个桶中,递归的将每个桶中的元素按中位丢入另外一批桶中,最后拼接。这种实现比较符合人类的思维习惯,叫MSD,而上面的低位基数排序算法叫LSD,显然,低位基数排序算法远优于高位排序算法。

3.桶排序

  我认为,准确来说,桶排序是一种思想,即将一组数据根据关键字key分桶/分堆,然后对每个堆单独处理。有点像多分的分治思想。上面提到的计数排序就可以看做一种近似的桶排序,计数排序的桶是计数数组的索引,桶内数据是计数数组的元素。可以把计数算法看做一种用最简单的数组实现的桶排序。

  更一般性的桶排序:首先是将一堆数据分桶,分桶的依据是一个映射函数i=f(key),key为数据的键,f为映射函数,得到的i即为桶的编号。分桶应该尽量均分分,并尽可能多分桶。这样能够保证更高效的性能。桶的实现可以基于数组,也可以基于链表。相联系的数据结构为栈或队列。然后将桶内数据分别处理,比如排序时,可以在分桶后对各个桶内数据快排,计数排序等等。需要保证的是一定要确保桶i内的元素全部小于桶i+1的元素。及保证桶的单项性。 最后,将各个桶内的元素拼接起来,得到目标结果。

  性能分析:桶排序性能分为两个部分,1是分桶,2是桶内处理,分桶需要遍历数据,所以是线性的。桶内处理时间是i个桶时间复杂度之和,如果桶内采用快排等,则时间复杂度为O(N)+i*O(NlgN),其中i为分得的桶的个数。如果桶分成足够多个,确保桶内处理在线性时间完成,那么最后的时间复杂度为O(N),如果为一个桶,那么就是O(NlgN)。 桶排序的空间复杂度在于各个桶的空间占用之和。如果分很多桶,并且存在大量空桶和低效桶(分桶不均),那么显然空间复杂度是很高的。

搞不懂的算法-排序篇<2>的更多相关文章

  1. 搞不懂的算法-排序篇<1>

    最近在学习算法,跟着<Algorithms>这本书,可能是自己水平不够吧,看完排序算法后各种,希尔,归并,快排,堆的实现在脑子里乱成一锅粥,所以就打算大概总结一下,不求精确,全面,只想用平 ...

  2. C#算法设计排序篇之04-选择排序(附带动画演示程序)

    选择排序(Selection Sort) 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/681 访问. 选择排序是一种简 ...

  3. C#算法设计排序篇之07-希尔排序(附带动画演示程序)

    希尔排序(Shell's Sort) 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/687 访问. 希尔排序是插入排序的 ...

  4. 4. GC 算法(实现篇) - GC参考手册

    您应该已经阅读了前面的章节: 垃圾收集简介 - GC参考手册 Java中的垃圾收集 - GC参考手册 GC 算法(基础篇) - GC参考手册 学习了GC算法的相关概念之后, 我们将介绍在JVM中这些算 ...

  5. (转载)微软数据挖掘算法:Microsoft 神经网络分析算法原理篇(9)

    前言 本篇文章继续我们的微软挖掘系列算法总结,前几篇文章已经将相关的主要算法做了详细的介绍,我为了展示方便,特地的整理了一个目录提纲篇:大数据时代:深入浅出微软数据挖掘算法总结连载,有兴趣的童鞋可以点 ...

  6. JS的十大经典算法排序

    引子 有句话怎么说来着: 雷锋推倒雷峰塔,Java implements JavaScript. 当年,想凭借抱Java大腿火一把而不惜把自己名字给改了的JavaScript(原名LiveScript ...

  7. js 数组排序和算法排序

    1.算法排序 a.插入排序 var arr = [23,34,3,4,23,44,333,444]; var arrShow = (function insertionSort(array){ if( ...

  8. 【编程练习】收集的一些c++代码片,算法排序,读文件,写日志,快速求积分等等

    写日志: class LogFile { public: static LogFile &instance(); operator FILE *() const { return m_file ...

  9. 微信技术分享:微信的海量IM聊天消息序列号生成实践(算法原理篇)

    1.点评 对于IM系统来说,如何做到IM聊天消息离线差异拉取(差异拉取是为了节省流量).消息多端同步.消息顺序保证等,是典型的IM技术难点. 就像即时通讯网整理的以下IM开发干货系列一样: <I ...

随机推荐

  1. 22.external version

    主要知识点 基于external version进行乐观锁并发控制 es提供了一个feature,就是说,你可以不用它提供的内部_version版本号来进行并发控制,可以基于你自己维护的一个版本号来进 ...

  2. js中window.location的用法

    用window.location处理解析当前页面URL window.location 对象所包含的属性 属性 描述 hash 从井号(#)开始的URL(锚点) host 主机名和当前URL的端口号 ...

  3. 用户输入input函数和代码注释

    一.读取用户输入 py3中input()读取用户输入,输出全部是默认str字符串数据类型,一般将其赋值变量,用户输入才继续往下走程序.(py2的不同已单独列出随笔) 二.注释 注释的作用:代码量大的时 ...

  4. AtCoder ARC 076E - Connected?

    传送门:http://arc076.contest.atcoder.jp/tasks/arc076_c 平面上有一个R×C的网格,格点上可能写有数字1~N,每个数字出现两次.现在用一条曲线将一对相同的 ...

  5. 2.Git可视化操作

    1.在本地新建版本库 首先,我们打开Git GUI是这样的一个界面,选择第一项,新建版本库. 然后选择你需要进行版本管理的项目路径,我选择了一个LoginDemo的项目. 当你创建了版本库的时候,你可 ...

  6. jQuery动态效果

    1.一号店首页 2.淘宝网购物车

  7. 【习题 4-9 UVA - 815】Flooded!

    [链接] 我是链接,点我呀:) [题意] 在这里输入题意 [题解] 题目很迷啊. 不会出现盆地? 可以理解为一条线. 从左往右高度上升的一座座山. 然后V升的水从最左边的山倒进去. 然后问你最后海拔多 ...

  8. 【例题 4-5 uva 512】Spreadsheet Tracking

    [链接] 我是链接,点我呀:) [题意] 在这里输入题意 [题解] 每个操作对与一个点来说变化是固定的. 因此可以不用对整个数组进行操作. 对于每个询问,遍历所有的操作.对输入的(x,y)进行相应的变 ...

  9. (10)Spring Boot修改端口号【从零开始学Spring Boot】

    Spring boot 默认端口是8080,如果想要进行更改的话,只需要修改applicatoin.properties文件,在配置文件中加入: server.port=9090 常用配置: #### ...

  10. elasticsearch 权威指南聚合阅读笔记(七)

    count(1) select clssId,count(1) from student group by  classId { "size":0, "aggs" ...