时间复杂度为O(n)的排序

以下排序算法都是针对特定场景才有优势的排序算法

桶排序

特定场景描述

  1. 为20XX年全国X0万考生的高考分数排序
  2. 为截止20XX年全国1X亿公民的身高排序
  3. 为截止20XX年全国1X亿公民的体重排序
  4. 为20XX年天猫双十一所有订单的金额排序
  5. 为20XX年滴滴打车所有订单的下单时间排序
  6. 为20XX年微信所有后台日志的时间排序

以上场景一般具有以下特点:

  • 要排序的数据有明显的范围。比如高考分数一般介于0~900,公民身高一般介于0~3000,公民体重一般介于0~150,订单金额一般介于0~max(当年),而某一年的时间,如果把时间以时间戳形式计算,一定是介于某个固定区间的

  • 既然有了明显的范围区间,就很方便划分区间段(类似统计中的柱状图),而且区间段的数据满足单调性,比如区间段f(x)和区间段f(x+1)一定满足如下关系:max(f(x)[i]) < min(f(x+1)[i]),即上一个区间段内的最大值一定比下一个区间段的最小值要小(这里说的是升序排列)。比如高考分数可划分的区间段是0~100101~200201~300301~400401~500501~600601~700701~800801~900,在区间段201~300中的最大值很明显会小于区间段301~400中的最小值。

  • 划分好了区间段,还要满足各区间段内的数据量相对均匀。还拿高考分数为例,意思是区间段201~300中的考生数和区间段301~400中的考生数和…几乎相当。这个其实挺难的。因为很多类似的分布都会符合正态分布的,两头少,中间多,当然也有特定的情况,这完全是根据数据的业务属性而来的。但各区间段内的数据量相对均匀是桶排序很关键的一点。

为了满足各区间段内的数据量相对均匀,就需要根据实际业务场景,重新划分区间段(桶)。
比如考生的分数,公民的身高和体重,一般来说符合正态分布,仍以分数为例,这需要参考以往经验了,假设得到的经验是,一般会在500~800之间扎堆儿,该区间考生数占据了所有的大约80%,那么根据这条经验,可以做出新的桶划分。比如:0~500501~550551~600600~650651~700701~750751~800801~900,或许500~800还可以分的更细致些的。
比如微信的后台日志时间戳,以天为单位的话,假设得到的经验是,春节前后使用量很大,五一、十一也会很大。相应的调整新的桶划分。
以上的目的就是尽量满足各桶中的数据量均匀。

思路

以空间换时间
尽可能减少数据间的比较
设置好若干个桶之后,首先遍历待排序数据,放入特定的桶中,时间复杂度是O(n),假设严丝合缝,也要开辟空间复杂度为O(n)的多个桶做临时存储
接着将各桶中的数据分别排序,如果需要稳定性(好像没在排序里说明稳定性的意思,后边单说吧),使用归并排序,如果不需要稳定性,使用快速排序,总之单个桶的时间复杂度是O(klogk)。假设设置的桶足够多,可以让一个桶里的元素值相等,那就不需要桶内排序了。但这需要开辟的桶空间可就更大了(这也算是把“以空间换时间”发挥到极致了吧)。
最后按序遍历各桶以及各桶中元素,依次放入原数组,完成排序。

实现代码

比较纠结这个排序算法要怎么写合适,网上也没找到一个很通用的,毕竟设定几个桶,桶的深度都要结合实际情况来看的。这里只能放一个简单的例子,以及代码实现。
全班10人,身高分别是[150,163,158,166,170,169,158,175,181,175],按身高升序排序
在执行排序前,得知身高最大值max=181,最小值min=150,设置m=3个桶,桶区间依次是150~160161~170171~181,每个桶的深度为5
下边是伪代码

public void bucketSort(int[] array, int length) {int bucketCount = 3;int[] bucket1 = new int[5];int bucket1Min = 150;int bucket1Max = 160;int[] bucket2 = new int[5];int bucket2Min = 161;int bucket2Max = 170;int[] bucket3 = new int[5];int bucket3Min = 171;int bucket3Max = 181;int bucket1Index = 0;int bucket2Index = 0;int bucket3Index = 0;for(int i = 0; i < array.length; i++) {int item = array[i];if(item < bucket2Min) {bucket1[bucket1Index++] = item;} else if (item < bucket3Min) {bucket2[bucket2Index++] = item;} else {bucket3[bucket3Index++] = item;}}quickSort(bucket1, bucket1.length);quickSort(bucket2, bucket2.length);quickSort(bucket3, bucket3.length);for(int i = 0; i < bucket1Index; i++) {array[i] = bucket1[i];}for(int i = 0; i < bucket2Index; i++) {array[bucket1Index + i] = bucket2[i];}for(int i = 0; i < bucket3Index; i++) {array[bucket2Index + i] = bucket3[i];}
}

计数排序

特定场景描述

场景和通排序的场景类似,只不过有了更苛刻的要求

  • 要排序的数据不仅有明显的范围,而且数值范围不大。比如桶排序列出的诸多场景中,分数、身高、体重都是很好的;金额可能有点邪乎,主要是如果土豪太多,双十一一单花了好几个亿那种,区间就太大了,不过据说单个订单有金额上限,如果这样的话就很好了;按时间排序,这可能就有点头大了,但如果不是按纳秒,而是粗略一些忽略到毫秒(也难),忽略到秒(一年31,536,000秒,也难),忽略到分钟(一年525,600分钟,挺好),忽略到小时(一年8,760小时,挺好),再忽略估计这个排序的意义可能就丧失了。

  • 要将被排序的所有元素按照某个一对一映射的函数,先计算成非负整数。比如身高和体重,身高169.55可以换算为16955,换算公式是f(x) = 100 * x;而x = f(x) / 100;两个函数的计算结果都是唯一的。再比如时间排序,换算成时间戳,再减去一个待排序最小时间的时间戳,在允许的情况下忽略纳秒、毫秒、甚至秒(这个逆向的映射有些麻烦,但既然忽略了一些精度,就相当于按更粗略的单位来排序了)。

思路

计数排序是特殊的桶排序,实现思路如下:
因为对数据值的范围做了限定:一方面不大,另一方面都可转为非负整数,那么就可以进行如下操作了。
假设数据值的最大值是max,接着设定max+1个桶,而这里的桶不用来存储元素的集合,而用来存储元素的个数,所以每个桶的长度为1
所以可创建一个长度为max+1的数组充当这max+1个桶

int[] temp = new int[max+1];

接着遍历原数组array,统计每个值的个数,并将结果保存到桶中

for(int i = 0; i < array.length; i++) {int item = array[i];temp[item] = temp[item] + 1;
}

举个例子,数组array = {3,5,7,2,4,2,7,3,1}
其中最大元素是7,那么桶就是temp = new int[8]
遍历array

index = 0; item = 3; temp[3] = temp[3] + 1; temp = {0,0,0,1,0,0,0,0};
index = 1; item = 5; temp[5] = temp[5] + 1; temp = {0,0,0,1,0,1,0,0};
index = 2; item = 7; temp[7] = temp[7] + 1; temp = {0,0,0,1,0,1,0,1};
index = 3; item = 2; temp[2] = temp[2] + 1; temp = {0,0,1,1,0,1,0,1};
index = 4; item = 4; temp[4] = temp[4] + 1; temp = {0,0,1,1,1,1,0,1};
index = 5; item = 2; temp[2] = temp[2] + 1; temp = {0,0,2,1,1,1,0,1};
index = 6; item = 7; temp[7] = temp[7] + 1; temp = {0,0,2,1,1,1,0,2};
index = 7; item = 3; temp[3] = temp[3] + 1; temp = {0,0,2,2,1,1,0,2};
index = 8; item = 1; temp[1] = temp[1] + 1; temp = {0,1,2,2,1,1,0,2};

最终得到temp = {0,1,2,2,1,1,0,2}
从数组temp可以看出,数组array中元素i的个数是temp[i]
而如果希望数组中元素值所表达如下含义呢?
从数组temp可以看出,数组array<= i的元素的个数是temp[i]
数组temp要做如下的元素叠加处理

for(int i = 1; i < temp.length; i++) {temp[i] = temp[0] + temp[i];
}

最终得到temp = {0,1,3,5,6,7,7,9}
从数组temp可以看出,数组array<= i的元素个数是temp[i]
array = {3,5,7,2,4,2,7,3,1},设置一个等长的临时数组result = new int[array.length]
第一个元素3,得到temp[3] = 5,说明<= 3的元素个数是5,那么经过排序后的arrayindex = 4(第5个元素)的位置上放置的一定就是3,所以result[4] = 3。既然3的位置已经确定,<= 3的元素个数就是4了,需要执行temp[3] = temp[3] - 1
此时result = {0,0,0,0,3,0,0,0,0}; temp = {0,1,3,4,6,7,7,9};
第二个元素5,得到temp[5] = 7,说明<= 5的元素个数是7,那么经过排序后的arrayindex = 6(第7个元素)的位置上放置的一定就是5,所以result[6] = 5。同样,temp[5] = temp[5] - 1
此时result = {0,0,0,0,3,0,5,0,0}; temp = {0,1,3,4,6,6,7,9};
第三个元素7
此时result = {0,0,0,0,3,0,5,0,7}; temp = {0,1,3,4,6,6,7,8};
第四个元素2
此时result = {0,0,2,0,3,0,5,0,7}; temp = {0,1,2,4,6,6,7,8};
第五个元素4
此时result = {0,0,2,0,3,4,5,0,7}; temp = {0,1,2,4,5,6,7,8};
第六个元素2
此时result = {0,2,2,0,3,4,5,0,7}; temp = {0,1,1,4,5,6,7,8};
第七个元素7
此时result = {0,2,2,0,3,4,5,7,7}; temp = {0,1,1,4,5,6,7,7};
第八个元素3
此时result = {0,2,2,3,3,4,5,7,7}; temp = {0,1,1,3,5,6,7,7};
第九个元素1
此时result = {1,2,2,3,3,4,5,7,7}; temp = {0,0,1,3,5,6,7,7};
最终得到数组result = {1,2,2,3,3,4,5,7,7}就是排序结果
再将其逐个复制到原数组array中即可

最终代码

public void countingSort(int[] array, int length) {// 找最大值int max = array[0];for (int i = 1; i < length; i++) {if (max < array[i]) {max = array[i];}}// 创建统计元素个数的数组int[] temp = new int[max + 1];for (int i : array) {temp[i] = temp[i] + 1;}// 叠加元素个数for (int i = 1; i < temp.length; i++) {temp[i] = temp[i - 1] + temp[i];}// 创建存储结果的临时数组int[] result = new int[length];for (int i : array) {result[temp[i] - 1] = i;temp[i] = temp[i] - 1;}// 将result拷贝到array中去for (int i = 0; i < array.length; i++) {array[i] = result[i];}
}@Test
public void countingSort() {int[] array = {2,6,7,3,1,5,3,2,3};System.out.println(Arrays.toString(array));countingSort(array,array.length);System.out.println(Arrays.toString(array));
}

采坑啦!
其实是在写基数排序时,使用了上述计数排序,就踩到坑了
坑的位置在

// 创建存储结果的临时数组
int[] result = new int[length];
for (int i : array) {result[temp[i] - 1] = i;temp[i] = temp[i] - 1;
}

针对重复元素,因为temp[i]要减一,所以后插入的元素是在先插入的前边
而这段代码中是按array从前向后遍历的,这和上述很巧妙的方式正好反向,导致这里的排序不是稳定的排序
修改方案就是改为逆向遍历

// 创建存储结果的临时数组
int[] result = new int[length];
for (int i = length - 1; i >= 0; i--) {int item = array[i];result[temp[item] - 1] = item;temp[item] = temp[item] - 1;
}

完整代码如下:

public void countingSort(int[] array, int length) {// 找最大值int max = array[0];for (int i = 1; i < length; i++) {if (max < array[i]) {max = array[i];}}// 创建统计元素个数的数组int[] temp = new int[max + 1];for (int i : array) {temp[i] = temp[i] + 1;}// 叠加元素个数for (int i = 1; i < temp.length; i++) {temp[i] = temp[i - 1] + temp[i];}// 创建存储结果的临时数组int[] result = new int[length];for (int i = length - 1; i >= 0; i--) {int item = array[i];result[temp[item] - 1] = item;temp[item] = temp[item] - 1;}// 将result拷贝到array中去for (int i = 0; i < array.length; i++) {array[i] = result[i];}
}@Test
public void countingSort() {int[] array = {2, 6, 7, 3, 1, 5, 3, 2, 3};System.out.println(Arrays.toString(array));countingSort(array, array.length);System.out.println(Arrays.toString(array));
}

基数排序

特定场景

  1. 将北京市所有常驻人口按11位手机号排序
  2. 将全国人民按身份证号排序
  3. 将牛津词典中所有单词排序

这里涉及到的都是字符串排序,而且满足如下条件:
第一个字符小的排在前
第一个字符相同时,第二个字符小的排在前
第一第二个字符相同时,第三个字符小的排在前

思路

思路其实是没有的,只是觉得很巧吧
主要是执行固定次数的时间复杂度为O(n)的稳定排序
比如如下5个字符串
["113","217","121","212","221"]
首先按第三位排序得
["121","221","212","113","217"]注意这里是稳定排序(当然,在这里还用不到稳定排序的优势),121221顺序不变
接着按第二位排序得
["212","113","217","121","221"]再次注意这里的稳定排序
最后按首位排序得
["113","121","212","217","221"]

最终代码

private void countingSort(String[] array, int length, int index) {char max = array[0].charAt(index);for (int i = 1; i < length; i++) {char c = array[i].charAt(index);if (max < c) {max = c;}}int maxValue = charToInt(max);int[] temp = new int[maxValue + 1];for (int i = 0; i < length; i++) {int item = charToInt(array[i].charAt(index));temp[item] = temp[item] + 1;}for (int i = 1; i < temp.length; i++) {temp[i] = temp[i - 1] + temp[i];}String[] result = new String[length];for (int i = length - 1; i >= 0; i--) {int item = charToInt(array[i].charAt(index));result[temp[item] - 1] = array[i];temp[item] = temp[item] - 1;}for (int i = 0; i < array.length; i++) {array[i] = result[i];}}private int charToInt(char c) {return c - '0';
}public void radixSort(String[] array, int length) {int stringLength = array[0].length();for (int i = stringLength - 1; i >= 0; i--) {countingSort(array, length, i);}
}@Test
public void radixSort() {String[] array = {"12345678901", "16789012342", "17890123453", "13456789014", "11234567895","15678901236", "13456789017", "12345678908", "13456789019"};System.out.println(Arrays.toString(array));radixSort(array, array.length);System.out.println(Arrays.toString(array));
}

这里附上一段经某位同学点拨后写出的代码,适用于亿级别int类型整数的高速排序

public void radixSort(int[] array, int length) {for (int i = 0; i < 32; i++) {radixSort(array, length, i);}
}private void radixSort(int[] array, int length, int index) {int[] tempArray = new int[2];for (int i = 0; i < array.length; i++) {int item = (array[i] >> index) & 1;tempArray[item] = tempArray[item] + 1;}for (int i = 1; i < tempArray.length; i++) {tempArray[i] = tempArray[i] + tempArray[i - 1];}int[] result = new int[length];for (int i = length - 1; i >= 0; i--) {int item = (array[i] >> index) & 1;result[tempArray[item] - 1] = array[i];tempArray[item] = tempArray[item] - 1;}for (int i = 0; i < length; i++) {array[i] = result[i];}}

说明一下:
这相当于把一个int类型值先转为二进制,再按二进制排序。int最多32
上万级别的数据,某位上的最大值为0的概率极低,所以最大值为1,存储01的个数数组就是int[2]了。
其余操作不变,但对于取某位上的数值,可使用如下公式:
取二进制数的倒数第i位的值 x >> (i - 1) & 1

时间复杂度为O(n)的排序(JAVA)相关推荐

  1. 八大排序:Java实现八大排序及算法复杂度分析

    目录 QUESTION:八大排序:Java实现八大排序及算法复杂度分析 ANSWER: 一:冒泡排序 1.算法分析 2.时间复杂度分析 3.代码 二:选择排序 1.算法分析 2.时间复杂度分析 3.代 ...

  2. 二分排序java实现

    1.什么是二分排序: 二分排序是指利用二分法的思想对插入排序进行改进的一种插入排序算法,不同于二叉排序,可以利用数组的特点快速定位指定索引的元素: 算法思想:二分法插入排序是在插入第i个元素时,对前面 ...

  3. 桶排序JAVA软件测试_111-堆排序的速度测试和小结

    2.网上数据结构和算法的课程不少,但存在两个问题: 1)授课方式单一,大多是照着代码念一遍,数据结构和算法本身就比较难理解,对基础好的学员来说,还好一点,对基础不好的学生来说,基本上就是听天书了 2) ...

  4. 算法练习5---快速排序Java版

    基本思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成 ...

  5. 计数排序和桶排序 java代码实现

    文章目录 计数排序 java代码实现 单元测试 桶排序 java代码实现 单元测试 计数排序 java代码实现 package csdn.dreamzuora.sort;import java.uti ...

  6. 希尔排序java写法_java高级排序之希尔排序

    希尔排序对于多达几千个数据项的,中等大小规模的数组排序表现良好,希尔排序不像快速排序和其它时间复杂度为o(n*logn)的排序算法那么快,因此,对非常大的文件排序,它不是最优选择,但是希尔排序比选择排 ...

  7. 希尔排序java代码_希尔排序及希尔排序java代码

    由上图可看到希尔排序先约定一个间隔(图中是4),然后对0.4.8这个三个位置的数据进行插入排序,然后向右移一位对位置1.5.9进行插入排序按照此规律直到全部参与了排序.然后将间隔约定为4-1=3,然后 ...

  8. Java List排序 java ListMap 排序 Java listmap 模拟 oracle 排序 Java listmap 模拟 mysql 排序

    Java List排序 java ListMap 排序 Java listmap 模拟 oracle 排序 Java listmap 模拟 mysql 排序 一.概述 近期的开发工作中,遇到一个需求: ...

  9. 姓名,地址按照A,B,C......来排序java

    姓名,地址按照A,B,C-来排序 java代码: 导入依赖: 查询首字母的读音 <dependency><groupId>com.belerweb</groupId> ...

  10. Map排序(Java)

    Java中的Map排序问题 在Java编写程序的过程中,常常会碰到使用map(key,value)来记录数据的情况,有些时候我们需要根据实际需要来对map中的数据进行排序.以下就是个人总结的map排序 ...

最新文章

  1. python 日志 logging 的用法
  2. 用户运营的三种思维层级,你在哪一层?
  3. 数据结构与算法 | 计数排序
  4. 有限元ansys/lsdyna学习笔记-组件component与组元part_02
  5. mysql 变量 数据类型_浅谈mysql(二)数据类型
  6. 细说安防宽动态:背光补偿与二次曝光
  7. HDFS文件系统的JAVA-API操作(一)
  8. python 发包爬取中国移动充值页面---可判断手机号是否异常
  9. android 自定义 对号,Android自定义View实现打钩动画功能
  10. TP5项目lnmp环境500错误
  11. 图解:知识竞赛现场管理系统-PPT双屏版,展示题目时,试题内容提前预审修改及监控图片的产生使用等
  12. 让Cygwin支持中文
  13. Keras中文文档总结
  14. 从零开始搭建Vue开发环境(windows)
  15. 左耳朵耗子给出的学习指南
  16. Linux下oracle数据库备份方案
  17. 安卓投屏软件_免费领取15天懒人听书会员+安卓美食菜谱整合app+安卓乐播投屏+办公软件幕布405天免费领取...
  18. C# base64获取图片后缀
  19. 冒泡排序与快速排序比较
  20. 网络工程师枕边书:精选30本图书抢先读

热门文章

  1. EDIUS 8中的快捷键该如何自定义
  2. 微信隐藏功能系列2:微信语音怎么转发?
  3. 爬虫python漏洞群_python3-爬取cnnvd漏洞信息
  4. 计算机二级c选择题题库,全国计算机二级C选择题题库套
  5. 网易云音乐APP分析
  6. 《keras中文文档》资料分享
  7. Java自学路线图之Java进阶自学
  8. 非线性系统 知识梳理
  9. less最后一页 linux_linux中less命令使用
  10. 解决Unity中文显示乱码问题