偷得浮生半日闲。想了之前面试的一些经历,虽然没有涉及到算法,但是想起自己掌握一个东西,还真得反复斟酌和深入思考,一时的理解总敌不过岁月的擦抹。总结和理解下当前的一些主要排序算法。
算法与复杂度
稳定性及复杂度概述
排序算法 | 是否稳定 | 最坏时间复杂度 | 平均时间复杂度 | 最好时间复杂度 | 空间复杂度 |
---|---|---|---|---|---|
直接插入 | 稳定 | O(n^2) | O(n^2) | O(n) | O(1) |
二分插入 | 稳定 | O(n^2) | O(n^2) | O(nlog2(n)) | O(1) |
希尔排序 | 不稳定 | O(n^2) | O(n^1.3) | O(n)) | O(1) |
冒泡排序 | 稳定 | O(n^2) | O(n^2) | O(n) | O(1) |
选择排序 | 不稳定 | O(n^2) | O(n^2) | O(n^2) | O(1) |
堆排序 | 不稳定 | O(nlog2(n)) | O(nlog2(n)) | O(nlog2(n)) | O(1) |
快速排序 | 不稳定 | O(n^2) | O(nlog2(n)) | O(nlog2(n)) | O(1) |
归并排序 | 稳定 | O(nlog2(n)) | O(nlog2(n)) | O(nlog2(n)) | O(n) |
计数排序 | 稳定 | O(n) | O(n) | O(n) | O(n) |
桶排序 | 稳定 | O(n) | O(N+C) | O(n) | O(N+M) |
算法的优化点与解析
- 直接插入法处理的待排数组是有序数组时:
- 当处理顺序与有序数组的顺序表现一致时,时间复杂度为O(n);
- 当处理顺序与有序数组的顺序表现相反时,为最坏场景。
- 二分插入在直接插入法取得0(n)时间复杂度时,其最好表现也是O(nlog2(n))。但是当n很大时,其搜索插入点时比较的次数会比直接插入法少得多,但是由于找到插入点,两个算法都需要对后续部分元素进行挪动,故时间复杂度一样为O(n^2)。=。=二分插入大喊我不服!
- 希尔排序对比与直接插入法,当待排数组元素个数n值很大时,增量m(数据项的距离)也很大,数据项每一趟排序需要的个数很少;当n值减小时每一趟需要和动的数据增多,但已经接近于排序后的最终位置。这使得希尔排序效率比插入排序高很多。但是对于n很大时,我们会选择更好的算法,如快排等!需要注意的是,希尔的最坏时间复杂度会收增量算法的影响。
- 当在某一趟冒泡排序中没有出现过元素交换,则认为剩余的元素都是有序状态,可以直接停止!
- 选择排序实际上我是不推荐使用,在任何场景下都会默认走一遍比较,就是那么辣鸡!
- 快排优化方案
- 三平均分区法,选择待排数组的第一个数作为中轴,而是选用待排数组最左边、最右边和最中间的三个元素的中间值作为中轴。
- 当快排处理大数据过程中,当数据集被递归到较小时,改用堆排序(Instrosirt)来克服在处理小规模数据中复杂的中轴选择;或者在规模达到一定小的情况下,改用插入排序,这时几乎是线性处理效果。
- 当块排分区的所有元素都一样时会出现选轴糟糕状态,可以把分区分成三部分,小于等于大于中轴值;或当发现最左和最右两个值相等时采用其他排序算法。
- 快速排序过程中,每次划分分区为1和n-1为最坏情况,此时为时间复杂度为O(n^2);而最好的情况出现在每次划分的分区都为n/2。
- 上述表格中,快速排序的空间复杂度O(1)为辅助变量空间;但是对于递归空间来说,当出现最坏情况,其会被退化为冒泡排序,且空间复杂度为O(n);当在最有情况下,其空间复杂度为O(log2(n))。
- 归并排序中,所使用的空间为临时空间n加上递归使用的空间log2(n),故空间复杂度为O(n)。
- 对于桶排序,如N个待排数据,M个桶,平均每个桶[N/M]个数据的桶排序平均时间复杂度为:
O(N)+O(M(N/M)log(N/M))=O(N+N(logN-logM))=O(N+NlogN-NlogM)
当N=M时,即极限情况下每个桶只有一个数据时。桶排序的最好效率能够达到O(N)。
总结:桶排序的平均时间复杂度为线性的O(N+C),其中C=N(logN-logM),如果相对于同样的N,桶数量M越大,其效率越高,最好的时间复杂度达到O(N)。
算法实现
考虑到java编程中,除了基础类型的比较之外经常涉及到对象之间的比较。意味着,特定类实现 Comparable 接口即可在逻辑上给定对象之间的比较逻辑。下面算法的实现大部分都是基于 Comparable 实例展开。
直接插入
1 | /** |
二分插入
1 | /** |
希尔排序
1 | /** |
冒泡排序
1 | /** |
选择排序
1 | /** |
堆排序
1 | /** |
快速排序
1 | /** |
归并排序
1 | /** |
计数排序
1 | /** |
桶排序
1 | /** |
10W随机数测试
直接上测试代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 public static void main(String[] args){
int num = 100000;
Integer[] types = new Integer[num];
Random random = new Random();
for(int i = 0; i <num; i++){
types[i] = random.nextInt(num);
}
long time1 = System.currentTimeMillis();
// Sort.insertionSort(types);
// Sort.bInsertSort(types);
// Sort.shellSort(types);
// Sort.bubbleSort(types);
// Sort.selectSort(types);
// Sort.heapSort(types);
// Sort.quickSort(types,0,num-1);
// Sort.mergeSort(types,0,num-1);
// Sort.countSort(types);
// Sort.bucketSort(types);
long time2 = System.currentTimeMillis();
System.out.print(time2-time1 + "ms");
}
由于每次测试花费时间都是重新产生数组,可能会导致排序的难度场景不太相同,但是对于整体验证复杂度还是可以比较直观体验。1
2
3
4
5
6
7
8
9
10
11
12
13
14测试机器:
cpu: 4核 Intel(R) Core(TM) i5-6300HQ CPU @ 2.30GHz
内存:8G
测试结果:
直接插入:11782ms
二分插入:8464ms
希尔排序:65ms
冒泡排序:56564ms
选择排序:14697ms
堆排序:72ms
快速排序:61ms
归并排序:115ms
计数排序:11ms
桶排序:62ms
忽略测试数据的随机性,大致参考总结为:
- 计数和桶排序都非常快,但是消耗了更多的额外空间,具体场景具体取舍
- 堆排序/快排序/归并排序表现差不多,这三者大部分场景下都优于希尔排序
- 数据量大适合快速排序和归并排序,数据量小可考虑堆排序,且堆排序稳定性最高
作者水平有限,本文仅供参考学习,如有错误遗漏,望慷慨指出!