上一篇排序算法<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. 单点登录之 CAS SSO 从入门到精通(第一天)

    转自:http://blog.csdn.net/lifetragedy/article/details/43817903 啊......it's quite a long time. 好久没更新博客了 ...

  2. DOM学习之图片库切换效果

    addloadevent(prepareplaceholder()) addloadevent(prepareGallery()) //页面加载完时执行函数 function addloadevent ...

  3. URAL - 1114-Boxes (分步乘法原理)

    题意; 给你n个盘子,A个红球,B个黑球,放的时候没有限制,可以不放,可以放一个红球,可以放一个黑球,也可以两个同时放,可以有剩余的球. 求一共有多少放法. 思路: 可以利用分步乘法原理,红球和黑球是 ...

  4. 递归、作用域、生命周期(day09)

    数组可以作为形式参数使用 数组做形式参数的时候真正的形式参数并 不是数组而是一个可以作为数组使用的 变量 数组形式参数里包含的存储区都不是被调用 函数提供的 声明数组形式参数的时候可以省略中括号 里的 ...

  5. Python语言数据结构和语言结构(2)

    目录 1. Python预备基础 2. Python数据类型 3. Python条件语句 4. while循环和for循环 1. Python预备基础 1.1 变量的命名   变量命名规则主要有以下几 ...

  6. lua返回页面时中文乱码

    1.在nginx.conf文件中的server标签里添加charset utf-8; 2.查看lua文件编码是否为utf-8

  7. FreeMarker基本使用

    FreeMarker是一个模板引擎,一个基于模板生成文本输出的通用工具,使用纯Java编写 l         FreeMarker被设计用来生成HTML Web页面,特别是基于MVC模式的应用程序 ...

  8. POJ 3528

    三维凸包 /* 增量法求凸包.选取一个四面体,同时把它各面的方向向量向外,增加一个点时,若该点与凸包上的某些面的方 向向量在同一侧,则去掉那些面,并使某些边与新增点一起连成新的凸包上的面. */ #i ...

  9. Android 最新面试题

    1. Intent的几种有关Activity启动的方式有哪些,你了解每一个含义吗? Intent的一些标记有FLAG_ACTIVITY_BROUGHT_TO_FRONT .FLAG_ACTIVITY_ ...

  10. JAVA经常使用数据结构及原理分析

    前不久面试官让我说一下怎么理解java数据结构框架,之前也看过部分源代码,balabala讲了一堆,如今总结一下. java.util包中三个重要的接口及特点:List(列表).Set(保证集合中元素 ...