归并排序
归并排序(merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法的一个非常典型的应用。作为一种典型的分而治之思想的算法应用,归并排序的实现有两种方法:
- 自上而下的递归(所有递归的方法都可以迭代重写,所以有了下面第二种)
- 自下而上的迭代
和选择排序一样,归并排序性能不受输入数据的影响,但表现比选择排序好得多,因为始终都是O(nlogn)的时间复杂度,代价是需要额外的空间。归并排序的核心思想是将两个有序的数列合并成一个大的有序的序列。通过递归,层层合并,即为归并。算法描述:
把长度为n的输入序列分为两个长度为n/2的子序列。
对这两个子序列分别采用归并排序。
将两个排序好的子序列合并为一个最终的排序序列。
动图演示:
代码实现:
public class MergeSort {
private static int[] array = {666,1,100,32,22,2,6,321,25,99,54,33,11,54,23,1,5};
public static void main(String[] args) {
mergeSort(array,0,array.length-1);
System.out.println(Arrays.toString(array));
}
/**
* 归并排序
* @param array
* @param L 指向数组的第一个元素
* @param R 指向数组的最后一个元素
*/
public static void mergeSort(int[] array,int L,int R){
if(L == R){
return;
}else {
//取中间的数,进行拆分
int M = (L+R)/2;
//左边的数不断进行拆分
mergeSort(array,L,M);
//右边的数不断拆分
mergeSort(array,M+1,R);
//合并
merge(array,L,M+1,R);
}
}
/**
* 合并数组
* @param array
* @param L 指向数组的第一个元素
* @param M 指向数组分隔的元素
* @param R 指向数组最后的元素
*/
public static void merge(int[] array,int L,int M,int R){
//左边数组的大小
int[] leftArray = new int[M-L];
//右边数组的大小
int[] rightArray = new int[R-M+1];
//往两个数组中填充数据
for(int i=L;i<M;i++){
leftArray[i-L] = array[i];
}
for(int i=M;i<=R;i++){
rightArray[i-M] = array[i];
}
int i=0,j=0;
//array数组的第一个元素
int k=L;
//比较两个数组的值,哪个小,就往数组上放
while (i<leftArray.length && j<rightArray.length){
//谁比较小,谁将元素放入大数组中,移动指针,继续比较下一个
if(leftArray[i]<rightArray[j]){
array[k] = leftArray[i];
i++;
k++;
}else{
array[k] = rightArray[j];
j++;
k++;
}
}
//如果左边的数组还没比较完,右边的数已经完了,那将左边的数抄到大数组中
while (i<leftArray.length){
array[k] = leftArray[i];
i++;
k++;
}
//同理,右边数组还没比较完,左边的数已经完了
while (j<rightArray.length){
array[k] = rightArray[j];
k++;
j++;
}
}
}
对归并排序进行优化:
1.当递归到规模足够小时,利用插入排序或者选择排序的速度可能会比归并排序更快。所以我们给定一个阈值,当数组长度小到阈值时,改为用插入排序来解决小规模子数组的排序问题。
2.测试数组是否已经有序,我们可以添加一个判断条件,如果array[mid] <= array[mid+1]的话,就可以认为数组已经是有序的了,并跳过归并的过程,这个改动不会影响排序的递归调用,但是任意有序的子数组算法的运行时间就变为线性的了。
3.不将元素复制到辅助数组中,节约将数组元素复制到用于归并的辅助数组的时间与重复创建数组的时间,但不能节省空间。
代码:
public class MergeSort2 {
private static int[] array = {666,1,100,32,22,2,6,321,25,99,54,33,11,54,23,1,5};
public static void main(String[] args) {
int[] array2 = array.clone();
mergeSort(array,array2,0,array.length-1);
//System.out.println(Arrays.toString(array));
System.out.println(Arrays.toString(array2));
}
private static void mergeSort(int arr1[],int arr2[],int l,int r){
//数组为空
if (l == r){
return ;
}
//阈值小于7时使用插入排序 暂时注释掉
/*if(r-l <= 7){
InsertionSort.insertionSort(arr1);
}*/
int m = (l+r)/2;
//交换参数,以使用两个数组分别保存值
mergeSort(arr2,arr1,l,m);
mergeSort(arr2,arr1,m+1,r);
//判断数组是否有序
if(arr1[m] <= arr1[m+1]){
System.arraycopy(arr1,l,arr2,l,r-l+1);
return;
}
merge(arr1,arr2,l,m,r);
}
private static void merge(int arr1[],int arr2[],int l,int m,int r){
//定义左边开始的索引,与右边开始的索引
int i=l,j=m+1;
for (int k=l;k<=r;k++){
//如果左边的数组已经全部遍历完了
if(i>m){
arr2[k] = arr1[j++];
}
//说明右边的数组全部遍历完了
else if(j>r){
arr2[k] = arr1[i++];
}
//如果左边数组中的数大于右边数组中的数,则k=右边的数
else if(arr1[j] < arr1[i]){
arr2[k] = arr1[j++];
}else{
arr2[k] = arr1[i++];
}
}
}
}
归并排序比较占用内存,但却是一种效率高且稳定的算法。
改进归并排序在归并时先判断前段序列的最大值与后段序列最小值的关系再确定是否进行复制比较。如果前段序列的最大值小于等于后段序列最小值,则说明序列可以直接形成一段有序序列不需要再归并,反之则需要。所以在序列本身有序的情况下时间复杂度可以降至O(n)
TimSort可以说是归并排序的终极优化版本,主要思想就是检测序列中的天然有序子段(若检测到严格降序子段则翻转序列为升序子段)。在最好情况下无论升序还是降序都可以使时间复杂度降至为O(n),具有很强的自适应性。
最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 | 空间复杂度 | 稳定性 | |
传统归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | T(n) | 稳定 |
改进归并排序 [1] | O(n) | O(nlogn) | O(nlogn) | T(n) | 稳定 |
TimSort | O(n) | O(nlogn) | O(nlogn) | T(n) | 稳定 |
总结:
归并排序是稳定的排序,他也是一种十分高效的排序,归并排序中每次合并操作的平均时间复杂度是O(n),而完全二叉树深度是log2n。总的平均时间复杂度为O(nlogn),而且归并排序的最好、最坏、平均时间复杂度都是O(nlogn)
Java中的Array.sort()有时采用的就是归并排序的优化版本TimSort(其实就是collection.sort时),其余的采用快速排序的改进版本DualPivotQuicksort(双轴快排)
List.sort源码:
调用的Arrays类的源码: