如果你还在吃归并排序的亏,不妨看看这篇

1.前言

       大多数人吃归并排序的亏,并不是因为看不懂归并排序,也不是因为不会编写归并排序的代码,而是不知道怎么灵活使用归并排序。CSDN里有很多大佬用通俗易懂的语言详细介绍了归并排序,看完这些博客是能够彻底理解归并排序原理的,但是我觉得我的疑惑并没有得到解决,因为归并排序的算法我还是不能灵活的运用到题目中去。而下来,我将会解开这个疑惑。

2.浅谈原理

       虽然道理大家都懂,但在这里还是帮助大家再回顾一遍。
       归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法的一个非常典型的应用,且各层分治递归可以同时进行(这是降低时间复杂度的关键)。归并操作也叫归并算法,指的是将两个已经排序的序列合并成一个序列的操作。
原理:既然归并排序采用的是分治法,那么其思想肯定是分而治之。我们知道归并操作是将两个有序的数列合并到一个有序的序列,那么对于一个无序的长序列,可以把它分解成若干个有序的子序列,然后依次进行归并。如果我们说每个数字都是单独有序的序列,那么只要把原始长序列依次分解,直到每个子序列都只有一个元素的时候,再依次把所有的序列进行归并,直到序列数为1。基于大家对归并排序都有所了解,下面就直接给出代码了,毕竟这不是本篇文章的重点。

void Merge(int r[],int r1[],int s,int m,int t)
{
    
    
    int i=s;
    int j=m+1;
    int k=s;
    while(i<=m&&j<=t)
    {
    
    
        if(r[i]<=r[j])
            r1[k++]=r[i++];
        else
            r1[k++]=r[j++];
    }
    while(i<=m)
        r1[k++]=r[i++];
    while(j<=t)
        r1[k++]=r[j++];
    for(int l=0; l<8; l++)
        r[l]=r1[l];
}
 
void MergeSort(int r[],int r1[],int s,int t)
{
    
    
    if(s==t)
        return;
    else
    {
    
    
        int m=(s+t)/2;
        MergeSort(r,r1,s,m);
        MergeSort(r,r1,m+1,t);
        Merge(r,r1,s,m,t);
    }
}

3.一个例题教会你如何灵活使用归并排序

例题
在这里插入图片描述
       这题当然有很多做法,最普通的就是暴力求数了,两次遍历。现在我告诉你可以用归并排序来高效的解出这道题,你先想想,看看有没有思路。如果觉得还是挺简单的话。。。哥!别看了,它配不上宁!如果和我一样还是没啥思路的话,那或许这篇文章能帮到你。下面是用归并排序的方法来解出这题的代码,先不用着急看代码,往下看完分析,你就彻底懂了。

class Solution {
    
    
    public int reversePairs(int[] nums) {
    
    
        int temp=mergeBinary2(0,nums.length-1,nums);
        for(int i=0;i<nums.length;i++)
         System.out.println(nums[i]);
          return temp;
    }
    public int mergeBinary2(int start,int end,int [] arr){
    
    
        if(start>=end){
    
    
            return 0;
        }
        int mid = (start+end)/2;
        int cuount = mergeBinary2(start,mid,arr)+mergeBinary2(mid+1,end,arr);
        int j = mid + 1;
        for(int i=start;i<=mid;i++){
    
    
            while (j<=end&&arr[i]>2L * arr[j]){
    
    
                j++;
            }
            cuount += j - mid - 1;
        }
        int [] l_arr = new int[mid-start+1];
        for(int i=0;i<l_arr.length;i++){
    
    
            l_arr[i] = arr[i+start];
        }
        int [] r_arr = new int[end-mid];
        for(int i=0;i<r_arr.length;i++){
    
    
            r_arr[i] = arr[i+mid+1];
        }
        int l = 0;
        int r = 0;
        int i = start;
        for(;i<=end&&l<l_arr.length&&r<r_arr.length;i++){
    
    
            if(l_arr[l]<=r_arr[r]){
    
    
                arr[i] = l_arr[l++];
            }
            else {
    
    
                arr[i] = r_arr[r++];
            }
        }
        while (l< l_arr.length){
    
    
            arr[i++] = l_arr[l++];
        }
        while (r<r_arr.length){
    
    
            arr[i++] = r_arr[r++];
        }
        return cuount;
    }
}

分析

  • 依题目要求,这些数之间都应该两两比较一次,当然其中可以省去部分的比较次数,比如如下情形(a>b,b>c)我们就无须比较a和c的大小了。但这题要想在这方面占便宜并不可行,一是就算全部找到符合该匹配机制的数,效率也不高;二是,找到这些符合匹配机制的数并不容易。
  • 既然必要的比较没法省去,那么另一种减小时间复杂度的方法便是同一时间,多比较几次,而我们归并排序的分而治之思想不就符合了嘛。对,也许你还会有疑问,分而治之可以理解,但为何要排序呢?这题的求解跟排序有什么关系,不排序不可以嘛。下面通过绘图的方式给大家解答。

图解1:按照分而治之思想,将原序列分成四个单个的小块。也就是到了递归的终止条件,实现代码如下:

        if(start>=end){
    
    
            return 0;
        }

在这里插入图片描述

图解2:因为递归已经到了最低层,所以返回数据,并执行递归代码块下面的代码如下,也就是将两个子区域的数进行题目要求的比较,如下图红圈里的,每个红圈分为两个子区域,左边红圈的两个子区域由1和3构成,右边红圈的子区域由5和2构成。每个红圈都会执行该for循环,左边红圈比较了1和3,右边红圈比较了5和2并因为符合题目要求而使cuount+1。

        int cuount = mergeBinary2(start,mid,arr)+mergeBinary2(mid+1,end,arr);//这里已经到尽头,执行完了,接下来执行下面这块代码。
        int j = mid + 1;
        for(int i=start;i<=mid;i++){
    
    
            while (j<=end&&arr[i]>2L * arr[j]){
    
    
                j++;
            }
            cuount += j - mid - 1;
        }

在这里插入图片描述
图解3:如果按照归并排序的话,会是如下图所示,将2和5调换顺序。那么如果我们不调换顺序呢?换句话说,我只用归并的思想,但我不用排序可不可以呢?好,我们继续来看刚才那段代码,这里有个while循环,如果每个序列都是无序的话,用for循环来代替while也同样能得到正确解。But,还记得我们刚开始说提高效率的方法嘛如果a>b,b>c,我们是不是就可以不用比较a和c的大小了,从而通过减少比较次数来减小时间复杂度。而这其中的while便是减小比较次数的核心。同样要实现这种效果的前提是该序列有序,所以便有了之后那一大段通用的归并序列的代码。

        for(int i=start;i<=mid;i++){
    
    
            while (j<=end&&arr[i]>2L * arr[j]){
    
    
                j++;
            }
            cuount += j - mid - 1;
        }

就是下面这个,貌似每个归并排序算法里都是这么写的

 int [] l_arr = new int[mid-start+1];
        for(int i=0;i<l_arr.length;i++){
    
    
            l_arr[i] = arr[i+start];
        }
        int [] r_arr = new int[end-mid];
        for(int i=0;i<r_arr.length;i++){
    
    
            r_arr[i] = arr[i+mid+1];
        }
        int l = 0;
        int r = 0;
        int i = start;
        for(;i<=end&&l<l_arr.length&&r<r_arr.length;i++){
    
    
            if(l_arr[l]<=r_arr[r]){
    
    
                arr[i] = l_arr[l++];
            }
            else {
    
    
                arr[i] = r_arr[r++];
            }
        }
        while (l< l_arr.length){
    
    
            arr[i++] = l_arr[l++];
        }
        while (r<r_arr.length){
    
    
            arr[i++] = r_arr[r++];
        }

在这里插入图片描述
       你会发现必要的比较都必过了,而不必要的都省略了(看图解就能体会得到)。之后便是重复同样的操作直到最后回到最初序列。而这整个题目区别于其他递归题目的代码部分只有下面这一段,其它代码近乎通用。

        for(int i=start;i<=mid;i++){
    
    
            while (j<=end&&arr[i]>2L * arr[j]){
    
    
                j++;
            }
            cuount += j - mid - 1;
        }

       看到这里,再看看整体的完整代码是不是非常简单呢,用归并的目的是想让你在同一时间内多比较几次,排序的目的是减少不必要的比较,而不是为了排序而排序。这下再能体会得到归并排序的强大了吧!
       如果你会用归并排序解下面这题了,那么恭喜你!!!
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/Achenming1314/article/details/108474741