基于比较的排序:选择排序、冒泡排序、插入排序、归并排序、快速排序、堆排序。
非基于比较的排序:计数排序、基数排序。
非基于比较的排序,指不直接对比两条记录的排序算法,一般只适用于数值型数组,适用范围有限,下面我们主要谈论基于比较的排序。
排序总结图:
一、稳定性
稳定性,指相同关键字的记录排序后,仍保持相对次序。
下面我们分析不同排序算法的稳定性:
1、选择排序:选择排序是在一个数组中选定一个位置,比如从0位开始,排好当前位置顺序后,再往后继续排序。看下图java实现代码:
// 选择排序
public static void sort(Integer[] arr) {
for(int i = 0; i < arr.length - 1; i++) {
for(int j = i+1; j < arr.length; j++) {
if(arr[i] > arr[j]) {
DataStore.swap(arr, i, j);
}
}
}
}
可以看到,选择排序对比的是arr[i] 和 arr[j]的大小,不是相邻数之间的对比,势必会打破稳定性。
2、冒泡排序:冒泡排序是将符合条件的值不断冒出的过程,实现过程是对比相邻数,如果碰到相等的值,不交换位置,所以能保持稳定性。java实现代码:
// 冒泡排序
public static void sort(Integer[] arr) {
for(int i=0; i<arr.length-1; i++) {
for(int j=0; j<arr.length-1-i; j++) {
if(arr[j] > arr[j+1]) {
DataStore.swap(arr, j, j+1);
}
}
}
}
3、插入排序:插入排序是往有序数组中插入值的过程。实现过程也是相邻数之间的对比,所以也能保持稳定性。java实现代码:
// 插入排序
public static void sort(Integer[] arr) {
for(int i = 1; i < arr.length; i++) {
for(int j = i-1; j >= 0; j--) {
if(arr[j] > arr[j+1]) {
DataStore.swap(arr, j, j+1);
}
}
}
}
4、归并排序:归并排序是将数组不断二分,然后再重新有序合并的过程。在归并方法中,只要做到,碰到相等值时,先插入左区间的值,则能保持稳定性。
5、快速排序:快速排序通过对数组分割出基准值小于区、相等区、大于区的方法,找出基准值在数组中索引,从而排序的过程。在分三区的过程中,不是相邻数之间的对比,数组被大幅度改变,不能保证稳定性。
6、堆排序:堆排序是将数组转化成堆,从而利用堆结构排序的过程。在数组转化成堆的过程,已经破坏了稳定性。
二、优先选择
排序算法这么多种,那我们如何选择呢?
第一个要看的指标,肯定是时间复杂度。基于比较的排序中,最小的时间复杂度是O(N*logN)。
一般的,我们会优先选择快速排序,因为在实验结果下,快速排序常数项时间最低,速度最快。
如果有空间限制的时候,选择堆排序,它的空间复杂度只有O(1)。
如果要求稳定性的时候,选择归并排序。
三、推论
1、基于比较的排序,没有排序算法的时间复杂度能低于O(N*logN)。
时间复杂度低于O(N*logN)的排序算法,一定是非基于比较的排序算法,比如:
计数排序,Ο(n+k)(其中k是整数的范围);
基数排序,O(m * (n + k))(m为执行的计数排序次数,k为位数上的取值范围)。
2、基于比较的排序,没有排序算法的时间复杂度低于O(N^2)并且空间复杂度低于O(N)的情况下能保持稳定性。
从排序总结表中可以看出,时间复杂度低于O(N^2)的排序算法是归并排序、快速排序、堆排序,其中能保持稳定性的是归并排序,空间复杂度是O(N)。
四、工程上对排序的改进
1、充分利用O(NlogN)和O(N^2)排序各自的优势。
比如对快速排序做优化,当样本量小于60时,直接使用插入排序返回。这是因为当样本量小时,插入排序更具优势。
在c、c++、java等语言中的排序实现,都是利用不同排序的优势,形成综合排序。
2、稳定性的考虑
系统array.sort方法中,当判断到是基础类型排序时,使用快速排序。因为快速排序有更快的速度,而且,基础类型不需要保持稳定性。当判断到是非基础类型排序时,使用归并排序,保持稳定性。