在介绍归并排序之前,先简单的说一下O(NlogN)和O(N2)之间的比较,通过下面的图片可以明显的看出来,前者的优势是很明显的,并且随着N的增大,优势会越来越明显,优化之后的代码可能意味着笨的算法一辈子都算不出来结果,而优化之后的算法,一瞬间就算出来了(细思极恐,这不就是现实生活吗...)
归并排序:归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并
为什么说归并排序就是一个O(NlogN)的算法呢?请看下图
一个有N个元素的数组,假设N=8,那么采用分治法,通过上图可以看出来,进行3次二分法就能将数组拆分为单个元素,然后逐层进行归并操作,3就是通过log28=3得出来的 通过这种拆分方法,把问题降低到通过O(N)的时间复杂度就能得到一个排好序的子序列,因此归并排序是一个O(NlogN)的算法,其在性能上要比O(N2)要好,并且是一个很稳定的算法.
当然任何事情都是有两面性,归并算法的缺陷是需要借助于比较大的内存,但通过空间换取时间是比较值得的
可以使用递归思想或者循环来实现归并排序,本次代码中使用递归思想来实现归并排序
// [l,r]
public static void sort(int[] arr, int l, int r) {
if (l >= r)
return;
int mid = (r + l) / 2;
sort(arr, l, mid);
sort(arr, mid + 1, r);
merge(arr, l, r, mid);
}
// [l,mid] [mid+1,r]是已经排好序的 直接进行merge操作
private static void merge(int[] arr, int l, int r, int mid) {
int[] aux = new int[r - l + 1];// 大小从l 到r
for (int i = l; i <= r; i++) {// 将arr中l到r的元素复制到aux中
aux[i - l] = arr[i];
}
int i = l;// 在arr中的 左边有序的下标
int j = mid + 1;//在arr中的 右边有序的下标
// 进行归并操作
for (int k = l; k <= r;) {
if (i > mid) {// 说明左边已经比完了 直接将右边j下标对应的数字给arr
arr[k++] = aux[j++ - l];
} else if (j > r) {// 说明右边已经比完了,直接将左边i下标对应的数字给arr
arr[k++] = aux[i++ - l];
} else {// 否则比较值 直接用三目运算
arr[k++] = aux[i - l] < aux[j - l] ? aux[i++ - l] : aux[j++ - l];
}
}
}
然而,上面的代码在某些情况下,其性能是不如插入排序的,因此归并排序是有一些优化的点的.来分析代码
public static void sort(int[] arr, int l, int r) {
if (l >= r)
return;
int mid = (r + l) / 2;
sort(arr, l, mid);
sort(arr, mid + 1, r);
merge(arr, l, r, mid);
}
①在上面的代码中,将[l,mid]和[mid+1,r]两个区间进行归并排序,紧接着无论上面的两个区间是否已经达成了有序不做判断,直接进行merge操作,这样在近乎有序的情况下,其性能会有影响.
②如果数组近乎有序,那么在元素个数小于一个常数的时候,利用插入排序性能反而更好,至于用哪个常数,在不同的应用场景,可以进行试验.
经过两次优化之后的代码:
// [l,r]
public static void sort(int[] arr, int l, int r) {
if (r - l + 1 <= 7) {// 优化②当需要进行归并排序的个数小于7的时候直接使用插入排序算法
// 插入排序算法
for (int i = l; i <= r; i++) {
int t = arr[i];
int j = i;
for (; j > 0 && arr[j - 1] > t; j--) {
arr[j] = arr[j - 1];
}
if (j != i) {
arr[j] = t;
}
}
return;
}
int mid = (r + l) / 2;
sort(arr, l, mid);
sort(arr, mid + 1, r);
if (arr[mid] > arr[mid + 1]) {// 优化① 通过上面的步骤可以保证左区间和右区间都是有序的序列了,因此, //只有当左区间的最后一个元素大于右区间第一个元素时,才进行merge操作
merge(arr, l, r, mid);
}
}
// [l,mid] [mid+1,r]是已经排好序的 直接进行merge操作
private static void merge(int[] arr, int l, int r, int mid) {
int[] aux = new int[r - l + 1];// 大小从l 到r
for (int i = l; i <= r; i++) {// 将arr中l到r的元素复制到aux中
aux[i - l] = arr[i];
}
int i = l;// 在arr中的 左边有序的下标
int j = mid + 1;// 在arr中的 右边有序的下标
// 进行归并操作
for (int k = l; k <= r;) {
if (i > mid) {// 说明左边已经比完了 直接将右边j下标对应的数字给arr
arr[k++] = aux[j++ - l];
} else if (j > r) {// 说明右边已经比完了,直接将左边i下标对应的数字给arr
arr[k++] = aux[i++ - l];
} else {// 否则比较值 直接用三目运算
arr[k++] = aux[i - l] < aux[j - l] ? aux[i++ - l] : aux[j++ - l];
}
}
}
通过上面的优化,这个归并排序就已经算是达到了比较好的性能了.
-----------------------------------------------------------------分水岭-----------------------------------
再附上一个不实用递归,使用迭代来完成的归并排序,这个比较好理解
/**
* 通过自底向上的迭代来完成归并排序
* @param arr
*/
public static void sortBU(int[] arr) {
for(int size = 1 ; size <= arr.length - 1 ; size += size) {//控制每次进行merge操作的元素的个数 2倍 关系 1->2->4
for(int i = 0 ; i + size <= arr.length - 1 ; i +=(size * 2)) {//控制每次merge操作的两个区间[i,i+size - 1],[i + size , i+size * 2 - 1]
merge(arr, i, Math.min(i + size * 2 - 1,arr.length - 1), i + size - 1);//为了避免i+size * 2 - 1 出现越界 使用Math.min 来取出 当前的right 和数组边界小的那一个
}
}
}