归并排序
目录
目录
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n log n)的时间复杂度。代价是需要额外的内存空间。
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
1、算法描述
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
2、算法图解
分而治之
合并相邻有序子序列
3、算法性能分析
算法 | 平均时间 | 最优时间 | 最坏时间 | 额外空间 | 稳定性 |
简单选择排序 | O(nlog n) | O(nlog n) | O(nlog n) | n | 稳定 |
4、算法实现
/**
* 归并排序实现
*/
public class MergeSort {
public static int[] sort(int[] nums){
if(nums.length == 1)
return nums;
int mid = nums.length / 2;
int[] leftNums = Arrays.copyOfRange(nums , 0 , mid);
int[] rightNums = Arrays.copyOfRange(nums , mid , nums.length);
return Merge(sort(leftNums) , sort(rightNums));
}
private static int[] Merge(int[] left , int[] right) {
int[] nums = new int[left.length + right.length];
int i = 0;
int j = 0;
int index = 0;
while (i < left.length && j < right.length){
if(left[i] <= right[j]){
nums[index++] = left[i++];
}else{
nums[index++] = right[j++];
}
}
if(i < left.length){ //合并剩下的
while (i < left.length)
nums[index++] = left[i++];
}
if(j < right.length){ //合并剩下的
while (j < right.length)
nums[index++] = right[j++];
}
return nums;
}
public static void main(String args[]){
int[] nums = new int[]{5,69,54,21,5,36,7,84};
nums = MergeSort.sort(nums);
for(int num : nums){
System.out.print(num + " ");
}
}
}
运行结果:
5 5 7 21 36 54 69 84
5、优化
(1)当递归到规模足够小时,利用插入排序或者选择排序的速度可能会比归并排序更快。所以我们给定一个阈值,当数组长度小到阈值时,改为用插入排序来解决小规模子数组的排序问题。
(2)测试数组是否已经有序,我们可以添加一个判断条件,如果array[mid] <= array[mid+1]的话,就可以认为数组已经是有序的了,并跳过归并的过程,这个改动不会影响排序的递归调用,但是任意有序的子数组算法的运行时间就变为线性的了。
(3)不将元素复制到辅助数组中,节约将数组元素复制到用于归并的辅助数组的时间与重复创建数组的时间,但不能节省空间。
public class MergeSort2 {
private static int[] array = {1,56,5,48,931,42,2,,,45,78,36,12,4};
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++];
}
}
}
}
6、总结
归并排序是稳定排序,它也是一种十分高效的排序,能利用完全二叉树特性的排序一般性能都不会太差。java中Arrays.sort()采用了一种名为TimSort的排序算法,就是归并排序的优化版本。从上文的图中可看出,每次合并操作的平均时间复杂度为O(n),而完全二叉树的深度为|log2n|。总的平均时间复杂度为O(nlogn)。而且,归并排序的最好,最坏,平均时间复杂度均为O(nlogn)。