目录
一)归并排序法大致思想:
可以将该算法分为两部分,一部分是划分,另一部分是归并。
先用递归的方法将数据不断的递归细分,然后将最小细分单元(两个元素)变成有序,最后再结合归并方法将这些分别有序的数据进行合并。
归并的过程必须是将两个有序的数组进行归并
结合下图我们可以看到,归并排序法就是先利用递归将两两进行排序,然后再将两个作为一组与另外两个有序的数组进行四个元素的归并排序,然后再进行两个有序的四元素的数组进行归并排序,逐渐进行,直到数组最终为有序。
注:上面一直在强调,最终归并的数据一定是两个已经有序的数组
二)时间与空间复杂度分析:
1、归并排序法的时间复杂度是O(n*logn),主要是因为这种细分会将其分为一个递归树,如下图:
2、归并排序法需要借助额外的辅助空间(下面用的tmp数组)进行,不是原地排序,即空间复杂度为O(n)。
3、时间复杂度与元素的有序性无关,因为每次都是合并需要进行O(n),共logn层,所以与元素的有序性无关,
4、因为在小数据量时,插入排序的性能会高于归并排序,所以可以结合插入排序优化归并排序。
5、归并排序法是一种稳定的排序算法。
三)排序流程
以下图为例:
l-mid是有序的,mid+1-r是有序的,然后对这两个有序的数组进行归并,逐渐比较,将较小的元素赋值到原数组arr的对应位置中。
四)代码实现
泛型设计
import java.util.Arrays;
public class MergeSort {
public MergeSort(){};
//将起初的最大块逐步分小,最终分为两个元素一组,对两个元素进行排序,然后再逐步向上进行合并
public static <E extends Comparable<E>> void sort(E[] arr){
//节省内存空间,不用每次都去开辟新的数组,所以先开辟一个数组
E tmp[]=Arrays.copyOfRange(arr,0,arr.length);
sort(arr,0,arr.length-1,tmp);
}
private static <E extends Comparable<E>> void sort(E[] arr,Integer l,Integer r,E[] tmp){
//递归结束条件:只有一个元素以及错误下标直接退出
if (l>=r) return;
int mid=(l+r)/2;//取中间值对数组进行细分
//对左半部分进行排序
sort(arr,l,mid,tmp);
//对右半部分进行排序
sort(arr,mid+1,r,tmp);
//将两个已经排好序的数组进行合并
if (arr[mid].compareTo(arr[mid+1])>0)//这个if判断是性能优化部分,解释见下两行
/*因为两个数组都是有序的,如果前面的最后一个(第一个数组最大的元素)
*小于后面的第一个元素(第二个数组最小的元素)此不用再排序了*/
merge(arr,l,mid,r,tmp);//调用归并过程
}
//归并过程
private static<E extends Comparable<E>> void merge(E[] arr,Integer l,Integer mid, Integer r,E[] tmp){
//创建辅助空间,Arrays.copyOfRange是产生一个[start,end)的数组
//tmp数组存的是该区间的值,用于给arr数组赋值用,最终是改变arr的值,而不是改变tmp的值
// System.arraycopy将arr数组从l位置将数据拷贝到tmp数组的l位置开始,拷贝r-l+1个值
//tmp数组之前已经开辟,所以这里直接用即可,如果此处开辟新数组,就会每次递归都开辟一个,会非常占用内存空间
System.arraycopy(arr,l,tmp,l,r-l+1);
int i=l,j=mid+1;
for (int k = l; k <= r; k++) {
if (i>mid){
//前面元素已经排序完毕
arr[k]=tmp[j];
j++;
}else if (j>r){
//后面元素已经提前排序完毕
arr[k]=tmp[i];
i++;
}else if (tmp[i].compareTo(tmp[j])<0){
//左面数组的值小于右面数组的值
arr[k]=tmp[i];
i++;
}else{
//左面数组的值>右面数组的值
arr[k]=tmp[j];
j++;
}
}
}
//测试方法
public static void main(String[] args) {
Integer arr[]={5,3,7,9,1,2,6};
MergeSort.sort(arr);
for (Integer one : arr) {
System.out.print(one+" ");
//打印结果未:1 2 3 5 6 7 9
}
}
}
求解数组中逆序对问题:
class Solution {
public int cnt=0;
public int reversePairs(int[] arr) {
int tmp[]=new int[arr.length];
mergeSort(arr,0,arr.length-1,tmp);
return cnt;
}
private void mergeSort(int[] arr, int l, int r, int[] tmp) {
if (l>=r) return;
int mid=(l+r)/2;
mergeSort(arr,l,mid,tmp);
mergeSort(arr,mid+1,r,tmp);
if (arr[mid]>arr[mid+1])
merge(arr,l,mid,r,tmp);
}
private void merge(int[] arr, int l,int mid, int r, int[] tmp) {
System.arraycopy(arr,l,tmp,l,r-l+1);
int i=l,j=mid+1;
//此处用的for循环,因为要给arr在l~r这个区间进行赋值,所以用for循环。
for (int k=l;k<=r;k++){
if (i>mid){
arr[k]=tmp[j++];
}else if (j>r){
arr[k]=tmp[i++];
}else if (tmp[i]<=tmp[j]){
arr[k]=tmp[i++];
}else {
arr[k]=tmp[j++];
cnt+=mid-i+1;
}
}
}
}
2022年3月11日复习心得:
1、起初的tmp数组创建的目的:开辟与原数组一样大的一块空间,因为后面要给数组赋值,如果数组没有这么大,赋值就会出错,比如int arr[]={};arr[1]=10;此时就会报错。
2、忘记了当数组为有序时的if性能优化的判断。
3、在进入merge操作的时候,每次进入都需要拷贝一次数组,因为arr在每次判断之后的值已经发生改变,所以要及时更新tmp数组的值。
4、在最后的for循环中,k是从l开始的,一定记住,每次操作的起始值是l,终点值是r,而不是从0一直到arr.length。
5、赋值操作都是由tmp数组给arr数组,判断也要用tmp数组进行判断,因为arr数组的值已经被改变。
6、当i>mid或j>r时,不是直接break,而是将另一个部分的剩余元素拷贝过去。
7、递归结束的条件是l>=r而不是l>r在等于的时候也要跳出循环。
8、归并排序法的空间复杂度为O(n)需要借助额外的辅助空间才能进行排序。