4. 寻找两个正序数组的中位数

题目链接:力扣

 题解:

  1. 因为题目要求时间复杂度为O(log(m+n)),就需要使用一种折半的方式进行查找,因此可以使用二分查找来求中位数,根据中位数的定义:当数组长度为偶数时,中位数为第有两个,也就是第\frac{n}{2}和第\frac{n+1}{2}个(数组长度为n,下标从0开始的第i个),当数组长度为奇数时,中位数有一个,也就是第\frac{n}{2}个(数组长度为n,下标从0开始的第i个)。中位数就是数组最中间的一个(数组长度为奇数)或两个数(数组长度为偶数)
  2. 在一个数组中,求中位数时,可以对数组进行划分,将数组分成左半部分L[]和右半部分R[],左右两部分的元素个数相等(数组长度为偶数)或者相差1(数组长度为奇数)
    1. 当数组长度为偶数时:L和R的长度相等,中位数有两个,因为数组有序,所以L中的最后一个元素和R中的第一个元素就是所求的中位数。
    2. 当数组长度为奇数时:规定将中位数划分到L中(也可以划分到R中,后续分析将中位数划分到L中),此时L中的元素个数比R中的元素个数多一个。
  3. 两个有序数组中,也可以进行划分:
    1. 将nums1划分成左半部分L1[]和右半部分R1[],将nums2划分成左半部分L2[]和R2[],L1,R1,L2,R2中元素个数不确定。每一个都可能为空。解题的关键也就是找到nums1的一个划分和nums2的一个划分。
    2. 可以将两个数组看成一个新数组,新数组的左半部分newL由L1和L2组成,右半部分newR由R1和R2组成。对于新数组来说newL中的元素个数与newR中的元素个数相等(两个数组长度之和为偶数),或者newL中的元素个数比newR中的元素个数多1个(两个数组长度为奇数,将中位数划分到newL中),如果newL中所有元素小于等于newR中的所有元素,那么:
      1. 两个有序数组长度之和为奇数时,中位数为newL中的最大值
      2. 两个有序数组长度之和为偶数时,其中一个中位数为newL中的最大值,另一个中位数为newR中的最小值
  4. 如果能够找到步骤3描述的划分方式,就可以求得中位数。这道题的求解关键为:
    1. 对num1和num2分别划分为左右两部分
    2. newL中所有元素小于等于newR中的所有元素。
    3. newL中的元素个数与newR中的元素个数相等(两个数组长度之和为偶数),或者newL中的元素个数比newR中的元素个数多1个(两个数组长度为奇数,将中位数划分到newR中)
  5. newL中所有元素小于等于newR中的所有元素,如何保证这一个条件?
    1. newL由L1和L2组成,newR由R1和R2组成。
    2. 对于同一个有序数组有L1<=R1,L2<=R2
    3. 所以只需要保证L1<R2&&L2<R1即可,即L1的最大值小于等于R2的最小值,L2的最大值小于等于R1的最小值。对应的代码实现为:
      num1[mid1-1]<=num2[mid2]&&num2[mid2-1]<=num1[mid1]
  6. 对num1和num2分别划分两部分:num1和num2两个数组只需要划分一个就可以了,另一个数组的划分位置就会自动确定,原因如下:
    1. 假设num1的长度为m,num2的长度为n
      1. 当m+n为偶数时,newL的长度totalLeft=\frac{m+n}{2},左右两部分元素个数相等
      2. 当m+n为奇数时,newL的长度totalLeft=\frac{m+n+1}{2},左边元素个数比右边多一个
      3. 又因为整数除法是向下取整,所以当m+n为偶数时totalLeft=\frac{m+n}{2}=\frac{m+n+1}{2}。这样就可以将m+n为偶数或者为奇数时计算newL的长度等同处理,即totalLeft=\frac{m+n+1}{2}
    2. 假设对num1进行划分后L1的长度为mid1,那么num2划分后的左边部分L2的长度为mid2=totalLeft-mid1(因为newL由L1和L2组成)。而totalLeft是一个确定的值,求得mid1,就可以得到mid2,可见,只需要找到其中一个划分方式,另一个划分可以通过计算得到。因此:
      1. num1的左半部分L1为nums1[0,1,...,mid1-1],右半部分R1为num1[mid1,...,m-1]
      2. num2的左半部分L2为nums2[0,1,...,mid2-1],右半部分R2的为num2[mid2,...,n-1]
      3. newL由L1和L2组成,newR由R1和R2组成。mid1的值既是R1中的最小值的下标,又是L1中的元素个数,mid2也是如此
    3. 当m+n为奇数时,中位数为newL中的最大值,最大值在num1[mid1-1]和num2[mid2-1]中产生(因为每个数组内部有序)
    4. 当m+n为偶数时,其中一个中位数为newL中的最大值,另一个中位数为newR中的最小值,最小值从num1[mid1]和num2[mid2]中产生
  7. 经过以上分析,我们只需要找到一种划分方式,假设找到nums1的划分mid1(这里nums1指向两个有序数组中长度较小的那一个,如果nums1的长度较大,就交换nums1和nums2,这样缩短二分查找的范围,以及减少临界情况),mid1将nums1划分成左右两部分L1和R1,满足条件:num1[mid1-1]<=num2[mid2]&&num2[mid2-1]<=num1[mid1]即可,这个时候,就可以保证newL中的所有元素小于等于newR中的所有元素。剩下的问题是如何求得mid1的值(mid2的值可以由newL的长度)。因为nums1是有序的,所有可以通过二分查找来得到mid1,这个时候因为mid2是通过totalLeft-mid1计算得到,所以当求得正确的mid1时,newL的长度一定等于newR的长度(当m+n为偶数),或者newL的长度比newR的长度多1。在通过二分查找确定mid1时,mid2的值也会自动更新,以保证newL的长度满足条件。当满足条件num1[mid1-1]<=num2[mid2]&&num2[mid2-1]<=num1[mid1]时,更新结束,当不满足时,就继续查找,也就是num1[mid1-1]>num2 || num2[mid2-1] > num1[mid1]时,更新查找范围,所以可以使用其中一个不满足的条件进行更新范围,以下分析使用条件num2[mid2-1] > num1[mid1]进行更新区间:初始值left=0,right=m,totalLeft=(m+n+1)/2
    1. 需要在区间[0,m]左闭右闭区间,找到划分mid1,所以循环的条件是while(left<right)循环终止时,left==right,(虽然是左闭右闭,循环条件使用的是left<right而不是left<=right,原因是当mid等于right时,在循环内部更新会出现索引越界,所以使用<而不是<=,这样只有在循环结束是才会出现mid1=m)。这时有两种特殊情况:1、left=right=m,nums1全部被划分到newL中。2、left=right=0,nums1全部被划分到newR中(因为num1是长度较短的数组)。注意循环条件是left<right。而在求mid的时候是向下取整,所以mid1在循环体内永远取不到right,也就是说在循环体内mid1<m,那么mid2=totalLeft-mid1=\frac{m+n+1}{2}-mid1。因为n>=m(nums1是较短的数组),所以mid2=\frac{m+n+1}{2}-mid1>=\frac{m+m+1}{2}-m>=1。所以mid2的最小值只能为1(这时,nums2全部被划分到newR中),条件num2[mid2-1]中不会出现索引越界异常。

      循环体内更新:
      1. mid1=\frac{left+right}{2},mid2=totalLeft-mid1。
      2. num2[mid2-1] > num1[mid1]成立时,L2中的最大值大于R1中的最小值,nums1的划分太靠右了,当前mid1不合适,需要在右半部分区间继续寻找mid1,更新left=mid+1,(最终left的值就是要找正确的划分mid1)解释如下:
        1. 最终区间大小为两个元素[a,b],left指向a,right指向b,mid1是向下取整的,会指向a,当num2[mid2-1] > num1[mid1],a不是我们要找的的划分,那么可能正确的划分是b,所以left=mid1+1,left也就指向了一个可能正确的划分b,如何b是一个正确的划分,那么left就是答案。如果b也不是一个正确的划分,那么再下一次更新区间时,left就是指向继续下一个可能正确的划分,如果一直不满足,left=right,会一直更新到nums1数组的末尾,显然,这个时候num1全部被划分到newL中(极限情况),此时也是要找的答案,由于left=right循环结束。所以最终left的值就是一个正确的划分mid1
      3. 否则,说明当前是mid1一个可能的划分,至少对于num2[mid2-1]<=num1[mid1]成立,只需要在满足num1[mid1-1]<=num2[mid2]找到了一个成功的划分。
        1. 如果不满足num1[mid1-1]<=num2[mid2],即,L1中的最大值大于R2中的最小值,说明nums1的划分太靠右了。当前mid1不合适,需要在左半部分继续寻找mid1,更新right,这里因为while循环中的条件是<,并且取mid1是时是向下取值,在循环体内mid1永远取不到right,所以这里更新right=mid(而不是right=mid-1,这样更新的话mid-1就取不到了,而实际上mid-1可能是一个正确的划分)
        2. 如果满足num1[mid1-1]<=num2[mid2],说明这个时候已经找到了正确的划分,我们也可以更新right=mid,因为left的值才是可能正确的划分,更新right并不印象最终结果
        3. 将right更新为right=mid,可以将上述两种情况一起讨论。
  8. 循环结束时,left的结果就是要找的一种划分:
    1. mid1=left,mid2=totalLeft-mid2,也就找到了num1和num2的划分。
    2. 由步骤6可知,中位数仅与num1[mid1-1],num2[mid2-1],num1[mid1],num2[mid2]四个值相关。
      1. 当m+n为奇数时,中位数为newL中的最大值,最大值在num1[mid1-1]和num2[mid2-1]中产生(因为每个数组内部有序)。令nums1LeftMax=num1[mid1-1],nums2LeftMax=num2[mid2-1],那么中位数就是
        Math.max(num1LeftMax,num2LeftMax)
      2. 当m+n为偶数时,其中一个中位数为newL中的最大值,另一个中位数为newR中的最小值,最小值从num1[mid1]和num2[mid2]中产生,令nums1RightMin=num1[mid1],nums2RightMin=num2[mid2],那么中位数为
        (double) (Math.max(num1LeftMax,num2LeftMax)+Math.min(num1RightMin,num2RightMin))/2;

    因为只在最短的数组中进行了划分,所以算法的时间复杂度为O(log(min(m,n))),比题目要求的O(log(m+n))还要低

  9. AC代码

    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
            if (nums1.length > nums2.length) {
                int[] temp = nums1;
                nums1 = nums2;
                nums2 = temp;
            }
    
            int m = nums1.length;
            int n = nums2.length;
    
            int totalLeft = (m + n + 1) / 2;
    
            int left = 0;
            int right = m;
    
    
            while (left < right) {
                int mid1 = (left + right) / 2;
                int mid2 = totalLeft - mid1;
                //num1[mid1-1]<=num2[mid2]&&num2[mid2-1]<=num1[mid1]
                if (nums2[mid2 - 1] > nums1[mid1]) {
                    left = mid1 + 1;
                } else {
                    right = mid1;
                }
            }
    
            int mid1 = left;
            int mid2 = totalLeft - mid1;
    
            int num1LeftMax = mid1 == 0 ? Integer.MIN_VALUE : nums1[mid1 - 1];
            int num1RightMin = mid1 == m ? Integer.MAX_VALUE : nums1[mid1];
            int num2LeftMax = mid2 == 0 ? Integer.MIN_VALUE : nums2[mid2 - 1];
            int num2RightMin = mid2 == n ? Integer.MAX_VALUE : nums2[mid2];
    
            if ((m+n)%2==0){
                return (double) (Math.max(num1LeftMax,num2LeftMax)+Math.min(num1RightMin,num2RightMin))/2;
            }else {
                return Math.max(num1LeftMax,num2LeftMax);
            }
    
        }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值