二分查找进阶

版本说明发布日期
1.0发布文章第一版2021-06-03

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

题目链接

寻找算法

  • 这道题不考虑时间复杂度的话,本身很简单,用一个归并排序就出来了,时间复杂度是O(m+n),空间复杂度是O(m+n)
  • 但是这道题难在需找时间复杂度为O(log(m+n))的解。
  • 数组、有序、O(log(m+n)),这三个特点组合在一起,就很容易可以联想到二分查找。这道题如果没有时间复杂度的提示,能想到二分查找确实非常困难。

思路

总体思路

  1. 首先来思考一个有序数组:
    • 可以用一个分割线把数组分为左右两个“尽可能均匀”的部分。所谓“尽可能平均”,在本题解中定义为:当元素个数为奇数时,让左边的元素比右边元素个数多一个。
      • 元素个数为偶数时,中位数为分割线左右两个元素的平均值;
      • 元素个数为奇数时,中位数为分割线左边的元素值。
        一个有序数组
  2. 按照这种思路,可以思考一下如果是两个有序数组,怎么找到他们共同的中位数?此时我们得想象这两个数组被排成一个数组的样子:
    • 可以用一个分割线把两个数组分为左右两个“尽可能均匀”的部分。但是因为是两个数组,这种分法会有很多种,那哪一种是正确的呢?其实多画几个例子,就可以发现,正确的分割线,一定满足下面的条件:
      • nums1在分割线左边的数 <= nums2在分割线右边的数;
      • nums2在分割线左边的数 <= nums1在分割线右边的数。
    • 为什么是这个样子?其实像下面这样思考一下就知道了:满足上面这个条件,实际上代表着将分割线划在了使一个数组“尽可能均匀”地被分割成两个部分的地方。
      一个数组转两个数组
  3. 此时怎么计算中位数呢?
    • 如果个数为偶数,一个数组的时候,左右两个元素的平均值,其实可以理解为分割线左边最大值和右边最小值的平均值。那两个数组的时候也是一样的:分割线左边的最大值与右边的最小值的平均值。
    • 如果个数为奇数,以同样的思考方式,可以得出结论:分割线左边的最大值就是中位数。
      两个有序数组
  4. 特殊情况
    • 从上面这张图就能看出一种特殊情况:分割线可能会划在数组的边缘。此时会对大小的比较产生一定的难度。
      • 这个问题很好解决,如图,因为在nums1的右边缘,所以分割线右边的值肯定是不会被选中的(因为不存在),所以我们可以假定这个值为正无穷大;
      • 其余边缘的情况同理。

寻找分割线

  • 道理我都懂,但是怎么才能找到这个分割线呢?
  • 其实把上面的三个条件拎出来,就能发现,这其实就是一个在一维有序数组里面找一个满足条件的值的过程,也就是说,二分查找就这么用上了:
    • nums1在分割线左边的数 <= nums2在分割线右边的数;
    • nums2在分割线左边的数 <= nums1在分割线右边的数。
    • 该分割线始终让左右两边的数保持“尽可能均匀”。
      • 怎么保持“尽可能均匀”?因为int运算的向下取整特性,不论数组是奇数个还是偶数个,我们只需要计算int sizeLeft = (m + n + 1) / 2即可。
  • 因此具体步骤如下:
    1. 对于nums1,采用二分查找,来选中一个分割线的位置。由于sizeLeft的限制,nums2的分割线位置自然也就确定了;
    2. 对该分割线进行条件满足性校验,直到两个条件同时满足:
      • 如果nums1左边的数大了,那么就在左半区间进行查找;
      • 如果nums1右边的数小了,那么就在右半区间进行查找。
        查找分割线

代码

  • 有两个小细节:
    • 当折半查找的left等于right的时候,此时不需要再判断了,此时的位置一定是目标分割线。至于原因小伙伴们可以自行验证一下。这样做的好处是可以避免nums1[wire1]访问越界。
    • 我们始终将最短的数组作为nums1,可以避免nums2的访问越界。
  • 该解的时间复杂度为折半查找效率:O(log(m + n))。空间复杂度为temp数组O(max(m, n))
    public static double findMedianSortedArrays(int[] nums1, int[] nums2) {
        // 是否是偶数
        boolean isEven = (nums1.length + nums2.length) % 2 == 0;

        // 左边的大小
        int sizeLeft = (nums1.length + nums2.length + 1) / 2;

        // 将短的数组作为num1。这样做的好处是:nums2的访问一定不会越界。
        if (nums1.length > nums2.length) {
            int[] temp = nums1;
            nums1 = nums2;
            nums2 = temp;
        }

        // 寻找分割线。
        // left == right时,一定是目标分割线,所以不用判断。跳过该情况的好处是:nums1[wire1]一定不会越界。
        int wire1 = 0, wire2 = 0;
        for (int left = 0, right = nums1.length; left <= right; ) {
            wire1 = (left + right) / 2;
            wire2 = sizeLeft - wire1;

            if(left == right){
                break;
            }

            // 如果wire1左边的值大于wire2右边的值,则分割线需要左移
            if (wire1 != 0 && nums1[wire1 - 1] > nums2[wire2]) {
                right = wire1 - 1;
                continue;
            }

            // 如果wire2左边的值大于wire1右边的值,则分割线需要右移
            if (nums1[wire1] < nums2[wire2 - 1]) {
                left = wire1 + 1;
                continue;
            }

            // 如果都满足,则结束循环
            break;
        }

        int nums1Left = wire1 == 0 ? Integer.MIN_VALUE : nums1[wire1 - 1];
        int nums1Right = wire1 == nums1.length ? Integer.MAX_VALUE : nums1[wire1];
        int nums2Left = wire2 == 0 ? Integer.MIN_VALUE : nums2[wire2 - 1];
        int nums2Right = wire2 == nums2.length ? Integer.MAX_VALUE : nums2[wire2];

        // 如果是偶数,则为平均值
        if (isEven) {
            return (Integer.max(nums1Left, nums2Left) + Integer.min(nums1Right, nums2Right)) / 2.0;
        }

        // 如果是奇数,则为左边的最大值
        else {
            return Integer.max(nums1Left, nums2Left);
        }
    }
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值