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

554 阅读3分钟

「这是我参与2022首次更文挑战的第35天,活动详情查看:2022首次更文挑战

前言

笔者除了大学时期选修过《算法设计与分析》和《数据结构》还是浑浑噩噩度过的(当时觉得和编程没多大关系),其他时间对算法接触也比较少,但是随着开发时间变长对一些底层代码/处理机制有所接触越发觉得算法的重要性,所以决定开始系统的学习(主要是刷力扣上的题目)和整理,也希望还没开始学习的人尽早开始。

系列文章收录《算法》专栏中。

力扣题目链接

问题描述

给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数

算法的时间复杂度应该为 O(log (m+n)) 。

 

示例 1:

输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2

示例 2:

输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5

提示:

  • nums1.length == m
  • nums2.length == n
  • 0 <= m <= 1000
  • 0 <= n <= 1000
  • 1 <= m + n <= 2000
  • -10^6 <= nums1[i], nums2[i] <= 10^6

剖析

笔者看到这个题的第一反应是利用已经是有序数组的特点,把较长的数组m那个放进链表里面,然后把较短的数组n按个插入,最后求中位数。时间复杂度为O(m+n*logm),很明显没有达到题目的要求。

那么我们平时再求一个有序数组的时候是怎么实现的呢?如果数组m长度是奇数,假设长度为i,那么中位数的下标就是i/2,中位数两边的元素个数相等。如果数组长度是偶数,那么中位数为(m[i/2]+m[i/2-1])/2,中位数两边的元素个数相等。不管是奇数还是偶数,本质都是下标的确定。

那么两个数组我们怎么处理呢?

  • 我们可以感观点把两个数组使用一条细进行切分,为了方便切分奇数的情况下,选择左边的个数比右边的个数大1,偶数的情况下相等。
  • 同时紧贴左边分割线的最大值应该比紧贴右边分割线的最小值小。(其实同一个数组当然左边小于等于右边,那么只需要注意上下交叉的左右即可)

如下图:

m+n个数为奇数时:
image.png

这个时候的中位数直接取分割线左边最大的一个就行,如上面的7。

m+n个数为偶数数时:
image.png

这个时候的中位数直接取分割线左边最大和右边最小的元素进行求平均数就行。如上图的8和9,中位数为(8+9)/2。

我们可以对一个数组进行二分,满足左右元素个数的同时,再满足紧贴左边分割线的最大值应该比紧贴右边分割线的最小值小,即可。因为左右元素个数确定所以进行二分的时候能从二分数组左边的个数和最终合并时左边需要的元素个数推算出另一个数组的分割线,所以只需要选择一个较小数组进行二分即可。时间复杂度:O(log min(m,n))

具体见代码

代码

/**
 * 寻找两个正序数组的中位数
 * https://leetcode-cn.com/problems/median-of-two-sorted-arrays/
 */
public class FindMedianSortedArrays {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        //对小的进行二分时间复杂度更小
        if (nums1.length > nums2.length) {
            return findMedianSortedArrays(nums2, nums1);
        }

        int m = nums1.length;
        int n = nums2.length;
        int left = 0, right = m;
        // median1:前一部分的最大值
        // median2:后一部分的最小值
        int median1 = 0, median2 = 0;

        while (left <= right) {
            // 前一部分包含 nums1[0 .. i-1] 和 nums2[0 .. j-1]
            // 后一部分包含 nums1[i .. m-1] 和 nums2[j .. n-1]
            //i为上面数组分割线紧贴右边的第一个元素的下标,也是左边元素的个数
            int i = (left + right) / 2;
            // 当m+n的等于偶数的时候(m + n) / 2就是左边的元素个数,m+n为奇数的时候(m + n + 1) / 2就是左边元素的个数,但是+1对偶数不会影响所以可以统一
            //j为下面数据分割线紧贴右边的第一个元素的下标,也是左边元素的个数
            int j = (m + n + 1) / 2 - i;

            // nums_im1, nums_i, nums_jm1, nums_j 分别表示 nums1[i-1], nums1[i], nums2[j-1], nums2[j]
            //i-1为上面数组分割线紧贴左边的第一个元素的下标,上面数组左边的元素应该小于下面数组紧贴分割线右边的元素,防止上面左边没有元素就直接取Integer.MIN_VALUE
            int nums_im1 = (i == 0 ? Integer.MIN_VALUE : nums1[i - 1]);
            //i为上面数组分割线紧贴右边的第一个元素的下标,应该大于下面数组紧贴分割线左边的元素,防止上面右边没有元素就直接取Integer.MAX_VALUE
            int nums_i = (i == m ? Integer.MAX_VALUE : nums1[i]);
            int nums_jm1 = (j == 0 ? Integer.MIN_VALUE : nums2[j - 1]);
            int nums_j = (j == n ? Integer.MAX_VALUE : nums2[j]);

            //如果上面数组左边元素小于等于下面数组右边元素的值是正常的,我们需要往右移动left指针继续二分,找到不能再大的为止
            if (nums_im1 <= nums_j) {
                //左边取最小的
                median1 = Math.max(nums_im1, nums_jm1);
                //右边取最大的
                median2 = Math.min(nums_i, nums_j);
                left = i + 1;
            }
            //如果上面数组左边元素大于下面数组右边元素的值是不正常的,我们需要左移动right指针继续进行二分,找到不能再小为止。如果上面已经不能再大转到下面right就会进行减1直到推出循环对结果没有影响,反之下面的亦然。
            else {
                right = i - 1;
            }
        }

        //奇数时直接取左边最大的
        return (m + n) % 2 == 0 ? (median1 + median2) / 2.0 : median1;
    }

    public static void main(String[] args) {
        FindMedianSortedArrays findMedianSortedArrays = new FindMedianSortedArrays();
        findMedianSortedArrays.findMedianSortedArrays(new int[]{1}, new int[]{});
    }
}