求两个有序等长数组arr1,arr2,合成有序数组arr之后的上中位数是多少

求两个有序等长数组arr1,arr2,合成有序数组arr之后的上中位数是多少?

提示:本题是一个极其重要的算法原型


题目

求两个有序等长数组arr1,arr2,合成有序数组arr之后的上中位数是多少


一、审题

示例:
arr1=1 2 3 4
arr2=1 2 3 4

组合有序arr=1 1 2 2 3 3 4 4
上中位数,实际上就是第N/2=4那个数:2

示例:
arr1=1 2 3 4 5
arr2=1 2 3 4 5

组合有序arr=1 1 2 2 3 3 4 4 5 5
上中位数,实际上就是第N/2=5那个数:3

既然是等长的俩数组,组合长度一定是偶数,只能是求上中位数


笔试AC解:双指针谁小谁跳动,计数到N/2就返回结果

整俩指针p1,p2分别指向arr1和arr2,对比p1和p2处的大小,谁小谁移动,同时count计数到N/2就对比返回结果。
举例:
最开始p1p2各自指向1,count=1,本次跳动就算对比了1个数
(1)p1:1<=p2:1,p1++=2,count=2,
(2)p1:2>p2:1,p2++=2,count=3
(3)p1:2<=p2:2,p1++=3,count=4
(4)因为count=N/2=4,此时就可以对比,p1,p2谁小返回谁
在这里插入图片描述
这个思想,手撕代码非常非常容易的

   //复习
    //笔试AC
    public static int f(int[] arr1, int s1, int e1, int[] arr2, int s2, int e2){
        //整俩指针p1,p2分别指向arr1和arr2,对比p1和p2处的大小,谁小谁移动,同时count计数到N/2就对比返回结果。
        int p1 = s1;
        int p2 = s2;
        int N = e1 - s1 + 1;//融合后长度的一半那个数,求第N这个数,就是上中位数了
        //举例:
        int count = 1;
        int ans = -1;//结果
        while (p1 <= e1){
            //(4)因为count=N/2=4,此时就可以对比,p1,p2谁小返回谁
            if (count == N) {
                ans = arr1[p1] <= arr2[p2] ? arr1[p1] : arr2[p2];
                break;
            }
            //最开始p1,p2各自指向1,count=1,本次跳动就算对比了1个数
            //(1)p1:1<=p2:1,p1++=2,count=2,
            if (arr1[p1] <= arr2[p2]) p1++;
            //(2)p1:2>p2:1,p2++=2,count=3
            else p2++;
            //对比完count都得加1
            count++;
        }
        //一定会提前结束的
        return ans;
    }

测试一下,

    public static void test(){
        int[] arr1 = {1,2,3,4,5};
        int[] arr2 = {6,7,8,9,10};
        int ans = f(arr1, 0, arr1.length - 1, arr2, 0, arr2.length - 1);
        System.out.println(ans);
    }

    public static void main(String[] args) {
        test();
    }

结果:

5

问题不大

咱们看看复杂度是多少呢?
咋招都要把arr1和arr2遍历完,o(n)复杂度

有没有更快的办法呢?
比如**o(min(log(n1),log(n2)))**这样的速度呢???
那就是面试官要考你的了!!!
且看下面的面试最优解!!!


面试最优解:巧妙的递归迭代,整出来第N/2小那个数就是上中位数

不过学习左神的数据结构与算法,它有一个很好的idea,可以加速

先看方法,咱们再看时间复杂度

方法:二分查找的思想?一绝
直接看arr1和arr2的各自上中位数mid1和mid2位置
说不定arr1[mid1]=arr2[mid2]了呢?那直接就找到了整体的上中位数,不是吗?
arr1=1 2 3 4
arr2=1 2 3 4
4长度,自然我们要找第4小那个数,就是上中位数!
各自看看自己的4/2=mid1=mid2=2那个位置
2 2,俩相等,必然是整体的第4小
为啥呢?
因为mid1和mid2左边压着比他们小的数,至少是12排名位次
而mid1和mid2俩数就是3 4位次的排名(在8名中),那mid2=4位置的2就是结果呗!
在这里插入图片描述
那要是mid1并不等于mid2位置的数呢???
比如:
arr1=1 3 3 4
arr2=1 2 3 4
这就尴尬了,mid1和mid2是3与2
不等,下一次该咋对比呢?

我们想这么搞:
下一次对比,能否绕开数组起码一半的规模,只需要去俩数组各自某一边去寻找
这样的话,咱们可以舍弃很多不必要的对比,加速了寻找的过程
——这不就是典型的二分查找的优化思想吗?

比如上面的
arr1=1 3 3 4
arr2=1 2 3 4
mid1和mid2分别是3与2,3>2,下一次咱们应该去哪里找呢?

至少不可能是arr1的3 4,也不可能是arr2的12
在这里插入图片描述
为啥呢?
因为整体有序,我们要找整体第4小,左边起码压住了1 1 2仨数了,故arr2的1 2 不可能是结果
既然arr1的3大于arr2的2
那么arr1的 3 4和arr2的4,最少排在后面的即 3 4 4作为第 6 7 8名
因此arr1中的后面34不可能是结果
只能是arr1中的1 3和arr2中的3 4去对比了
在这里插入图片描述
因此,下一次直接去arr1的左边部分,和arr2的右边部分对比,
这么做就通过二分的方式,舍弃了arr1的右边部分和arr2的左边部分
达到加速的目的

当然了这是偶数长度的情况,奇数长度可能有点小区别
比如示例:
arr1=1 2 3 4 5
arr2=1 2 3 4 5
mid1=mid2=3位置,都是3,相等的话,mid1位置就是结果
因为mid1左边压了2数,mid2也是,故mid1=mid2时,他们俩必然是5 6排名的位置,找第10/2=5名,恰好就是mid1位置
在这里插入图片描述
但是如果mid1不等于mid2,我们怎么排除不可能的结果呢?
比如示例:
arr1=1 2 4 4 5
arr2=1 2 3 4 5

mid1那是4,mid2那是3,4>3
显然因为mid1左边压了2个,mid2左边也压了2个,排名1 2 3 4,故mid2左边的12不可能是结果
而且mid2右边有2个,mid1的4又比mid2大,故arr1中4 4 5和arr2中的45,一定是6 7 8 9 10这些名次,故arr1的4 4 5决不可能是结果
结果可能就是在arr1的左边部分和arr2的右边部分
但是吧,下一次找递归去找的话,1 2与3 4 5不等长,所以没法递归了……
在这里插入图片描述
这就是奇数情况不一样的原因
怎么搞呢?实际上就是手动判断一下此刻mid2和mid1-1的位置
mid2=3
mid1-1=2
因为3>2,所以在“mid1左边压了2个,mid2左边也压了2个,排名1 2 3 4”的基础下
mid2成功排序第5,它就是答案。

否则的话,我们其实把mid2也排除了
比如:
在这里插入图片描述
下一次咱们直接去arr1的左边部分,和arr2的右边部分继续递归寻找

大致思想,你明白了吧!

就是要想办法在本次mid1和mid2不相等的情况下,去舍弃不可能的那部分,然后下次递归去求可能的等长的部分
这就是涉及数组长度是奇数或者偶数的不同情况了,
偶数好说,直接舍弃,递归仍然是等长的,
但是奇数长度,还要手动判断mid2与mid1-1位置,才能最后递归等长的数组。

故:这里不得不分开说,下面咱们来一个抽象化的讲解:

下面开始,咱们不在认为1 2 3 4是具体数字了,而是有序的编号,比如1就是arr1的第1个数
1’就是arr2的第1个数
在这里插入图片描述

明白?

定义f(arr1,s1,e1,arr2,s2,e2)为:
在arr1的s1–e1和arr2的s2–e2俩等长数组,返回融合后有序数组的上中位数,即第N/2那个数

等长数组的长度N如果是偶数的话:

查找N/2那个上中位数
mid1=(s1+e1)/2
mid2=(s2+e2)/2

(1)如果arr1[mid1]=arr2[mid2],显然arr1[mid1]或者arr2[mid2]就是结果
在这里插入图片描述
(2)如果arr1[mid1]>arr2[mid2],则3 4不可能,1’ 2’不可能是结果
下一次让e1=mid1-1,s2=mid2+1
去递归求f(arr1,s1,e1,arr2,s2,e2)的结果就是咱们要的结果
比如下图粉色俩圈,省去arr1右边部分和arr2的左边部分
在这里插入图片描述
(3)如果arr1[mid1]<arr2[mid2],跟(2)类似,则1 2不可能,3’ 4’不可能是结果
下一次让e2=mid2-1,s1=mid1+1【反过来了哦】
去递归求f(arr1,s1,e1,arr2,s2,e2)的结果就是咱们要的结果
比如下图粉色俩圈,省去arr1右边部分和arr2的右边部分
在这里插入图片描述
懂?

等长数组的长度N如果是奇数的话:

这里与偶数长度唯一的区别,就是要手动判断mid1和mid2左侧的数,提前看看能不能得到结果,然后再去递归

查找N/2那个上中位数
mid1=(s1+e1)/2
mid2=(s2+e2)/2

(1)如果arr1[mid1]=arr2[mid2],显然arr1[mid1]或者arr2[mid2]就是结果
在这里插入图片描述

(2)如果arr1[mid1]>arr2[mid2],则3 4 5不可能,1’ 2’不可能是结果
手动判断 3’与2的关系,如果:
3’>2则必然3’就是结果
否则1’ 2’ 3’不可能是结果
下一次让e1=mid1-1,s2=mid2+1
去递归求f(arr1,s1,e1,arr2,s2,e2)的结果就是咱们要的结果
比如下图橘色俩圈,省去arr1右边部分和arr2的左边部分
在这里插入图片描述
(3)如果arr1[mid1]<arr2[mid2],则1 2不可能,3’ 4’ 5’不可能是结果
手动判断 3与2’的关系,如果:
3>2’则必然3就是结果
否则1 2 3不可能是结果
下一次让e2=mid2-1,s1=mid1+1
去递归求f(arr1,s1,e1,arr2,s2,e2)的结果就是咱们要的结果
比如下图橘色俩圈,省去arr1右边部分和arr2的左边部分
在这里插入图片描述
除了手动判断之外,实际上与偶数那没啥区别的

手撕代码:f(arr1,s1,e1,arr2,s2,e2)

你如果代码不知道怎么扣边界
就按照我上面那样,画个图,就知道下次要去递归对比哪辆部分了,也就是你明确知道要舍弃哪几部分

    //面试最优解
    //求两个有序等长数组arr1,arr2,合成有序数组arr之后的上中位数是多少
    public static double f2(int[] arr1, int s1, int e1, int[] arr2, int s2, int e2) {
        //base case:当长度就是1时,直接对比,看谁小,返回谁
        if (s1 == e1) return Math.min(arr1[s1], arr2[s2]);

        //长度不止1时,下面二分查找
        int mid1 = (s1 + e1) >> 1;
        int mid2 = (s2 + e2) >> 1;

        int N = e1 - s1 + 1;//单个数组长
        boolean isEvenNum = (N & 1) == 0;//如果与上00001之后,末尾是0,必然是偶数

        if (isEvenNum){
            //偶数长度
            //不会就自己画个图看看
            //(1)如果arr1[mid1]=arr2[mid2],显然arr1[mid1]或者arr2[mid2]就是结果
            if (arr1[mid1] == arr2[mid2]) return arr1[mid1];
            else if (arr1[mid1] > arr2[mid2]){
                //(2)如果arr1[mid1]>arr2[mid2],则3 4不可能,1' 2'不可能是结果
                //下一次让e1=mid1-1,s2=mid2+1
                //去递归求f(arr1,s1,e1,arr2,s2,e2)的结果就是咱们要的结果
                e1 = mid1;//舍弃arr1的右边部分--e1不能越界
                s2 = mid2 + 1 > e2 ? e2 : mid2 + 1;//舍弃arr2的左边部分--e2不能越界
                return f2(arr1, s1, e1, arr2, s2, e2);
            }else {
                //(3)如果arr1[mid1]<arr2[mid2],跟(2)类似,则1 2不可能,3' 4'不可能是结果
                //下一次让e2=mid2-1,s1=mid1+1【反过来了哦】
                //去递归求f(arr1,s1,e1,arr2,s2,e2)的结果就是咱们要的结果
                e2 = mid2;//舍弃arr2的右边部分--e2不能越界
                s1 = mid1 + 1 > e1 ? e1 : mid1 + 1;//舍弃arr1的左边部分--e1不能越界
                return f2(arr1, s1, e1, arr2, s2, e2);
            }

        }else {
            //奇数长度——除了手动判断,其余和偶数一样
            //由于手动判断了一个,所以你条整e的情况一定要把mid-1
            //(1)如果arr1[mid1]=arr2[mid2],显然arr1[mid1]或者arr2[mid2]就是结果
            if (arr1[mid1] == arr2[mid2]) return arr1[mid1];
            else if (arr1[mid1] > arr2[mid2]){
                //(2)如果arr1[mid1]>arr2[mid2],则3 4 5不可能,1' 2'不可能是结果
                //**手动判断** 3'与2的关系,如果:
                //3'>2则必然3'就是结果
                if (arr2[mid2] > arr1[mid1 - 1]) return arr2[mid2];
                //否则1' 2' 3'不可能是结果
                else {
                    //下一次让e1=mid1-1,s2=mid2+1
                    //去递归求f(arr1,s1,e1,arr2,s2,e2)的结果就是咱们要的结果
                    e1 = mid1 - 1;//舍弃arr1的右边部分--e1不能越界
                    s2 = mid2 + 1 > e2 ? e2 : mid2 + 1;//舍弃arr2的左边部分--e2不能越界
                    return f2(arr1, s1, e1, arr2, s2, e2);
                }

            }else {
                //(3)如果arr1[mid1]<arr2[mid2],则1 2不可能,3' 4' 5'不可能是结果
                //**手动判断** 3与2'的关系,如果:
                //3>2'则必然3就是结果
                if (arr1[mid1] > arr2[mid2 - 1]) return arr1[mid1];
                //否则1 2 3不可能是结果
                else {
                    //下一次让e2=mid2-1,s1=mid1+1
                    //去递归求f(arr1,s1,e1,arr2,s2,e2)的结果就是咱们要的结果
                    e2 = mid2 - 1;//舍弃arr2的右边部分--e2不能越界
                    s1 = mid1 + 1 > e1 ? e1 : mid1 + 1;//舍弃arr1的左边部分--e1不能越界
                    return f2(arr1, s1, e1, arr2, s2, e2);
                }
            }
        }
    }

最重要理解偶数长度时,e=mid
奇数长度时,由于手动判断了一个,所以e=mid-1
这里一定搞清楚了!!!

搞清楚了上面的二分查找舍弃的思想,我一遍就把代码撸通了!

测试一把:

    public static void test(){
//        int[] arr1 = {1,2,3,4,5};
//        int[] arr2 = {6,7,8,9,10};
        int[] arr1 = {1,2,3,4,5};
        int[] arr2 = {1,2,3,4,5};
        int ans = f(arr1, 0, arr1.length - 1, arr2, 0, arr2.length - 1);
        int ans2 = f2(arr1, 0, arr1.length - 1, arr2, 0, arr2.length - 1);
        System.out.println(ans);
        System.out.println(ans2);
    }

    public static void main(String[] args) {
        test();
    }

问题不大:

3
3

换一下:

    public static void test(){
        int[] arr1 = {1,2,3,4,5};
        int[] arr2 = {6,7,8,9,10};
//        int[] arr1 = {1,2,3,4,5};
//        int[] arr2 = {1,2,3,4,5};
        int ans = f(arr1, 0, arr1.length - 1, arr2, 0, arr2.length - 1);
        int ans2 = f2(arr1, 0, arr1.length - 1, arr2, 0, arr2.length - 1);
        System.out.println(ans);
        System.out.println(ans2);
    }

    public static void main(String[] args) {
        test();
    }

问题不大:

5
5

牛逼吧

复杂度分析:咋就变快了呢?二分法思想的妙处

既然是递归二分查找,咱们就有计算复杂度的方法
有了代码,你可以用master公式来求复杂度:
【1】递归思想求arr中最大值,递归函数先调用处理几部分,然后整理和归并信息返回

递归调用的复杂度master公式:
T(N)=aT(N/b)+o(N^d)
a是你在递归函数f2中调用了几次f2,本题就是条用了一次,所有的if和else条件,只会进一条
b是每次调用f2数据的规模,整体长度为N,咱们每次舍弃一半,二分查找呗!就是N/2,故b=2
除了递归调用,你还玩别的操作的复杂度o(n^d),本题,除了base case那一个min对比之外,没别的骚操作,即d=0,o(1)
故:
根据master公式:
在这里插入图片描述
故本题的复杂度是o(log(n))
比你双指针还是要快的吧(o(n)复杂度)


总结

提示:重要经验:

1)二分查找优化的目的就是每次从mid分隔,想办法舍弃另一半,达到加速的目的,最终复杂度o(log(n))
2)本题又是2个数组同时运用二分查找,是极其难,但是又非常经典的题目,互联网大厂年年考
3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰露可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值