归并排序及常见面试题

归并排序及常见面试题_第1张图片
⭐️前言⭐️

本篇文章主要介绍归并排序,以及与之相关的改写问题,将该类问题总结归纳,便于后续遇见类似题目时候的解答。

欢迎点赞 收藏留言评论 私信必回哟

博主将持续更新学习记录收获,友友们有任何问题可以在评论区留言

博客中涉及源码及博主日常练习代码均已上传GitHub


请添加图片描述

内容导读

  • 1.归并排序的实现
  • 2.数组小和
  • 3.数组的降序对
  • 4.翻转对
  • 5.区间和的个数
  • 6.总结归纳

1.归并排序的实现

思想

其中心思想就是合并两个有序数组。先将序列拆解使得每个子序列有序,再二路归并使子序列段间有序,最后得到有序序列。

归并排序及常见面试题_第2张图片
代码实现:

public class MergeSort {
    public static void mergeSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        process(arr,0,arr.length-1);
    }

    public static void process(int[] arr, int L, int R) {
        if (L == R) {
            return;
        }
        int mid = L + ((R - L) >> 1);
        process(arr, L, mid);
        process(arr, mid + 1, R);
        merge(arr, L, mid, R);
    }

    public static void merge(int[] arr, int L, int M, int R) {
        int[] help = new int[R - L + 1];
        int index = 0;
        int p1 = L;
        int p2 = M + 1;
        while (p1 <= M && p2 <= R) {
            help[index++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
        }
        //要么p1越界,要么p2越界,两个while只会进去一个
        while (p1 <= M) {
            help[index++] = arr[p1++];
        }
        while (p2 <= R) {
            help[index++] = arr[p2++];
        }
        for (int i = 0; i < help.length; i++) {
            arr[L + i] = help[i];
        }
    }
}

2.数组小和

题目:

在一个数组中,一个数左边比它小的数的总和,叫该数的小和
所有数的小和累加起来,叫数组小和
例子: [1,3,4,2,5]
1左边比1小的数:没有
3左边比3小的数:1
4左边比4小的数:1、3
2左边比2小的数:1
5左边比5小的数:1、3、4、 2
所以数组的小和为1+1+3+1+1+3+4+2=16
给定一个数组arr,求数组小和

题解思路:

如果使用两层for循环的暴力方法,操作结果为等差数列次,所以时间复杂度为O(N^2),可以利用归并排序的方法来降低时间复杂度。

求数组小和,就是需要求出数组中每个数的左边比该数小的所有数的和,这些和加起来就是数组小和;

同样地,这个数组小和的求法可以转换为数组中每个数的右边,有几个数比该数大,有几个则数组小和中加N个该数。

然后就可以利用归并排序的方法,在按区域进行归并排序的时候,也就按区域求出了比该数大的数的个数,也就间接求出了数组小和。

代码实现:

public class SmallSum {
    public static int smallSum(int[] arr) {
        if(arr==null||arr.length<2) {
            return 0;
        }
        return process(arr,0,arr.length-1);
    }
    public static int process(int[] arr,int L,int R) {
        if(L==R) {
            return 0;
        }
        int M=L+((R-L)>>1);
        return process(arr,L,M)
                +process(arr,M+1,R)
                +merge(arr,L,M,R);
    }
    public static int merge(int[] arr,int L,int M,int R) {
        int[] help=new int[R-L+1];
        int index=0;
        int p1=L;
        int p2=M+1;
        int res=0;
        while (p1<=M&&p2<=R) {
            //当左组的数比右组的数小时,右组指针所指的数的右边的所有的数都比左组数大
            //当两者相等时,先拷贝右组的数,因为不知道右组有多少个数比左组当前数大
            res+=arr[p1]<arr[p2]?(R-p2+1)*arr[p1]:0;
            help[index++]=arr[p1]<arr[p2]?arr[p1++]:arr[p2++];
        }
        while (p1<=M) {
            help[index++]=arr[p1++];
        }
        while (p2<=R) {
            help[index++]=arr[p2++];
        }
        for (int i = 0; i < help.length; i++) {
            arr[L+i]=help[i];
        }
        return res;
    }
}

3.数组的降序对

题目:
在一个数组中,任何一个前面的数a,和任何一个后面的数b,如果(a,b)是降序的,就称为降序对
给定一个数组arr,求数组的降序对总数量

题解思路:

类似这种左边比右边的数大或者小的问题,都可以往归并排序上来想,在上道题9.3中求的是右边比该数大的数的个数,这道题求的就是右边比该数小的个数,可以通过使得归并排序时排逆序就可转化成和上一道题一样的解法了。

代码实现:

public class ReversePair {
    public static int reversePair(int[] arr) {
        if(arr==null||arr.length<2) {
            return 0;
        }
        return process(arr,0,arr.length-1);
    }

    public static int process(int[] arr,int L,int R) {
        if(L==R) {
            return 0;
        }
        int M=L+((R-L)>>1);
        return process(arr,L,M)
                +process(arr,M+1,R)
                +merge(arr,L,M,R);
    }

    public static int merge(int[] arr,int L,int M,int R) {
        int[] help=new int[R-L+1];
        int index=0;
        int p1=L;
        int p2=M+1;
        int res=0;
        while (p1<=M&&p2<=R) {
            res+=arr[p1]>arr[p2]?R-p2+1:0;
            help[index++]=arr[p1]>arr[p2]?arr[p1++]:arr[p2++];
        }
        while (p1<=M) {
            help[index++]=arr[p1++];
        }
        while (p2<=R) {
            help[index++]=arr[p2++];
        }
        for (int i = 0; i < help.length; i++) {
            arr[L+i]=help[i];
        }
        return res;
    }
}

4.翻转对

题目:493. 翻转对 - 力扣(Leetcode)

在一个数组中,对于任何一个数num,求有多少个(后面的数*2)依然 比如:[3,1,7,0,2]
3的后面有:1,0
1的后面有:0
7的后面有:0,2
0的后面没有
2的后面没有
所以总共有5个

题解思路:

仍然是利用归并排序的方法,需要在归并之前,把符合要求的数统计下来,因为即使是在二路合并之前,左组中和右组中的数都是分别有序的,所以可以直接完成统计。

代码实现:

public class BiggerThanRightTwice {
   public int reversePairs(int[] arr) {
        if(arr==null||arr.length<2) {
            return 0;
        }
        return process(arr,0,arr.length-1);
    }

    public int process(int[] arr, int L, int R) {
        if(L==R) {
            return 0;
        }
        int M=L+((R-L)>>1);
        return process(arr,L,M)
                +process(arr,M+1,R)
                +merge(arr,L,M,R);
    }

    public int merge(int[] arr,int L,int M,int R) {
        int ans=0;
        int windowR=M+1;
        for (int i = L; i <=M ; i++) {
            //因为左右数组都是有序的,所以指针windowR不需要回退
            while (windowR<=R&&(long)arr[i]>(long)2*arr[windowR]) {
                windowR++;
            }
            ans+=windowR-M-1;
        }

        int[] help=new int[R-L+1];
        int index=0;
        int p1=L;
        int p2=M+1;
        while (p1<=M&&p2<=R) {
            help[index++]=arr[p1]<arr[p2]?arr[p1++]:arr[p2++];
        }
        while (p1<=M) {
            help[index++]=arr[p1++];
        }
        while (p2<=R) {
            help[index++]=arr[p2++];
        }
        for (int i = 0; i < help.length; i++) {
            arr[L+i]=help[i];
        }
        return ans;
    }
}

5.区间和的个数

题目:
给定一个数组arr,两个整数lower和upper,
返回arr中有多少个子数组的累加和在[lower,upper]范围上

Leetcode题目:https://leetcode.cn/problems/count-of-range-sum/

题解思路:
1.i到j的子数组和位于范围内,可转换为两个前缀和数组的差位于范围内,所以可以先将原数组转换为前缀和数组,再通过前缀和数组差求解,以降低时间复杂度。
2.假设i位置的前缀和为X,求必须以i位置结尾的子数组,有多少个在[Lower,Upper]范围内,等同于去求i之前的所有前缀和数组中,有多少个前缀和在[X-Upper,X-Lower]上。
3.利用归并排序的有序分组,分别针对右组的每个数,来左组查看多少个数符合要求,并且因为数组的有序性,可以保证指针不回退。

代码实现:

class Solution {
    public int countRangeSum(int[] nums, int lower, int upper) {
        if(nums==null||nums.length==0) {
            return 0;
        }
        long[] sum=new long[nums.length];
        sum[0]=nums[0];
        for(int i=1;i<sum.length;i++) {
            sum[i]=sum[i-1]+nums[i];
        }
        return process(sum,0,sum.length-1,lower,upper);
    }

    public int process(long[] sum,int L,int R,int lower,int upper) {
        if(L==R) {
            return sum[L]>=lower&&sum[L]<=upper?1:0;
        }
        int M=L+((R-L)>>1);
        return process(sum,L,M,lower,upper)+
        process(sum,M+1,R,lower,upper)+
        merge(sum,L,M,R,lower,upper);
    }

    public int merge(long[] sum,int L,int M,int R,int lower,int upper) {
        int ans=0;
        int windowL=L;
        int windowR=L;
        // [windowL,windowR)
        for(int i=M+1;i<=R;i++) {
            long min=sum[i]-upper;
            long max=sum[i]-lower;
            while(windowR<=M&&sum[windowR]<=max) {
                windowR++;
            }
            while(windowL<=M&&sum[windowL]<min) {
                windowL++;
            }
            ans+=windowR-windowL;
        }
        long[] help=new long[R-L+1];
        int index=0;
        int p1=L;
        int p2=M+1;
        while(p1<=M&&p2<=R) {
            help[index++]=sum[p1]<=sum[p2]?sum[p1++]:sum[p2++];
        }
        while(p1<=M) {
            help[index++]=sum[p1++];
        }
        while(p2<=R) {
            help[index++]=sum[p2++];
        }
        for(int i=0;i<help.length;i++) {
            sum[L+i]=help[i];
        }
        return ans;
    }
}

6.总结归纳

后边的几道题都是归并排序的改写,其本质都是把比较信息变成了有序的东西,这个有序的东西可以帮助我们很快的求很多事。

当再遇见某些数的左组与右组相比符合某些条件,或者右组与左组相比符合某些条件时,可以把问题往归并排序上靠拢,并且借助归并排序的组内数据有序性,可以实现指针的不回退。


⭐️最后的话⭐️
总结不易,希望uu们不要吝啬你们的哟(^U^)ノ~YO!!如有问题,欢迎评论区批评指正

请添加图片描述

你可能感兴趣的:(算法集锦,java,算法,排序算法)