转载请注明出处:http://blog.csdn.net/fightlei/article/details/52586814
本篇汇总的算法将不再是基于比较的排序算法,因此会突破这类算法的时间复杂度下界O(nlog2n)。如果有朋友对前面的内容感兴趣,可以先去看看常见排序算法汇总与分析(中)(选择排序与归并排序)
我们先来总结基数排序算法,该算法在排序过程中不进行比较,而是通过“分配”和“收集”两个过程来实现的。
基数排序
【基本思想】
首先设立r个队列,对列编号分别为0~r-1,r为待排序列中元素的基数(例如10进制数,则r=10),然后按照下面的规则对元素进行分配收集1,先按最低有效位的值,把n个元素分配到上述的r个队列中,然后从小到大将个队列中的元素依次收集起来
2,再按次低有效位的值把刚收集起来的关键字分配到r个队列中,重复收集工作
3,重复地进行上述分配和收集,直到最高有效位。(也就是说,如果位数为d,则需要重复进行d次,d由所有元素中最长的一个元素的位数计量)
为什么这样就可以完成排序呢?
以从小到大排序为例
首先当按照最低有效位完成分配和收集后,此时得到的序列,是根据元素最低有效位的值从小到大排列的。
当按照次低有效位进行第二次分配和收集后,得到的序列,是先根据元素的次低有效位的值从小到大排列,然后再根据最低有效位的值从小到大排列。
以此类推,当按照最高有效位进行最后一次分配和收集后,得到的序列,是先根据元素的最高有效位的值从小到大排列,再根据次高有效位排列,。。。,再根据次低有效位,再根据最低有效位。自然就完成了每个元素的从小到大排列。
【空间复杂度】O(n+r)
【时间复杂度】
平均情况:O(d(n+r))
因为每一趟分配的时间开销是O(n),收集的开销是O(r),因此执行d趟的时间开销为O(d(n+r)),通常d,r为常数
最好情况:O(d(n+r))
最坏情况:O(d(n+r))
【稳定性】稳定
【优点】
稳定排序;时间复杂度可以突破基于关键字比较排序法的下界O(n)
【缺点】
需要额外的辅助空间
/** * 基数排序 * @param arr */ public static void radixSort(int[] arr) { //获得待排序列元素中的最大位数 int maxBit = getMaxBit(arr); //对每一位分别进行分配和收集 for (int bit = 1; bit <= maxBit; bit++) { //分配 List<List<Integer>> buf = distribute(arr, bit); //收集 collect(arr, buf); } } private static void collect(int[] arr, List<List<Integer>> buf) { int i = 0; //收集,依次将每个队列中的元素读出 for (List<Integer> temp : buf) { for (int ele : temp) { arr[i++] = ele; } } } private static int getMaxBit(int[] arr) { int maxBit = 0; int len = 0; for (int ele : arr) { //利用字符串的length()方法获得元素位数 len = (ele + "").length(); if (len > maxBit) { maxBit = len; } } return maxBit; } //分配 private static List<List<Integer>> distribute(int[] arr, int bit) { List<List<Integer>> buf = new ArrayList<>(); //构建r个队列 for (int i = 0; i < 10; i++) { buf.add(new ArrayList<>()); } for (int ele : arr) { int value = getValueByBit(ele, bit); //根据每个元素指定位数上的值,将元素存放到对应的队列上 buf.get(value).add(ele); } return buf; } //得到指定位数上的值 private static int getValueByBit(int ele, int bit) { //没有该位,则返回0 int value = 0; String temp = ele + ""; if (temp.length() >= bit) { value = (int) (temp.charAt(temp.length() - bit) - '0'); } return value; }
【本算法解读】
算法针对的是十进制数,所以r=10
算法首先获取到待排序列元素中的最大位数。然后按照基数排序的思想,对元素的每一位进行分配和收集。
分配distribute()方法:首先构建了r=10个队列,对应编号即是0~9。根据给定的位数,得到待排序列中每个元素在该位上的值,若元素没有该位,则返回0。根据每位上的值,将该元素放入相应的队列。完成一次分配。
收集collect()方法:依次从r个队列中读出其中的元素值,并将其存入原始序列中,即完成了一次收集。
【举个栗子】
对于待排序列413,10,8,28
首先获取到待排序列的最大位数,即为3。
然后根据每一位的值进行分配和收集,分配过程如下图:
由于是10进制数,所以构造10个队列,0~9。
首先从最低有效位开始,每个元素在该位上对应的值分别是3,0,8,8,根据该值,将对应元素放入对应的队列。分配结束后,依次从每个队列上读取元素值存入原始序列。可以看到一次分配以后,元素已经按照最低有效位从小到大排列。继续位数逐渐增大,直到最高有效位,重复上述操作即可完成排序。
计数排序
【基本思想】
计数排序是基于非比较排序,主要用于对于一定范围内的整数进行排序。采用的是空间换时间的方法。针对待排序列中的每一个元素x,得到序列中小于x的元素个数,有了这一信息可以直接把x放到最终的输出序列的正确位置上。 计数排序之所以能做到线性时间复杂度是因为使用了索引的思想。
对于下面我即将给出的算法,它的限制条件是待排序列中的元素是有限个正整数(包括0,由于将元素值作为数组下标),最大值用k=O(n)表示,元素个数用n表示。它利用元素的实际值来确定它们在输出序列中的位置
【空间复杂度】O(n+k)
【时间复杂度】
平均情况:O(n+k)
最好情况:O(n+k)
最坏情况:O(n+k)
【稳定性】稳定
【优点】
稳定,在k值较小时突破了基于比较的排序算法下界
【缺点】
存在前提条件,需要大量额外空间,k值较大时效率很低
/** * 计数排序 * @param arr */ public static void countSort(int[] arr) { int max = getMax(arr); int count[] = new int[max + 1]; //此时数组中元素值均被初始化为0 //在以元素值为 for (int ele : arr) { count[ele]++; } int k = 0; for (int i = 0; i <= max; i++) { for (int j = 0; j < count[i]; j++) { arr[k++] = i; } } } //获得待排元素中的最大值 private static int getMax(int[] arr) { int max = 0; for (int ele : arr) { if (ele > max) { max = ele; } } return max; }
【本算法解读】
可以看到算法首先获得待排序列元素中的最大值,然后构建最大值+1的长度的计数数组。遍历待排序列的每个元素,并在以元素值为下标的数组值中加1。然后遍历计数数组,若对应下标的位置上值大于0(等于几就表示有几个元素),则表示存在有元素且其值为下标的大小。将该元素添加到原始序列中。由于下标是从小到大的,所以对应得到的序列也是从小到大排列的。【举个栗子】
对于待排序列4,0,2,8,2
首先获取最大值即为0,然后构建8+1长度的计数数组。初始化时,计数数组中的值均为0.如下图所示:
最后过遍历计数数组,如,遍历到0下标位置,数组值为1,表示有一个元素,元素值是下标0,添加到原始序列中。遍历到2下标位置,数组值为2,表示有两个元素,元素值均为下标2,将两个元素依次添加到原始序列中。以此类推,最终得到有序序列0,2,2,4,8。
那么,到这里我们这个常用排序算法的汇总就顺利完成了。本汇总分析了常用排序算法中的,交换排序(冒泡排序,快速排序),插入排序(直接插入排序,希尔排序),选择排序(直接选择排序,堆排序),归并排序,基数排序,计数排序。应该说是这个总结是暂时告一段落了,后面遇到一些好的算法,还会继续与大家分享。
为了方便大家,我已将本系列中用到的所有算法源码(java实现)上传至CSDN,需要的朋友可以点此下载
如有纰漏,敬请海涵。
来源:https://www.cnblogs.com/iwiniwin/p/10793660.html