LeetCode刷题笔记:4. 寻找两个正序数组的中位数

451 阅读4分钟

题目描述

给定两个大小分别为 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
  • -106 <= nums1[i], nums2[i] <= 106

解题思路

题目要求算法的时间复杂度为O(log(m+n)),因此我们想到采用二分法。

假设数组nums1、nums2的长度分别为m、n,那么两者的总长度为(m+n)。由于两者都是升序排列,假设两数组合并后的升序数组为nums,如果总长度为奇数,那么中位数为nums[(m+n)/2],如果为偶数则为(nums[(m+n)/2-1]+nums[(m+n)/2])/2.0,因此实际上是需要找到两数组合并后第(m+n)/2+1小(长度为偶数时还有第(m+n)/2小)的数。

设总长度(m+n) = totalLen,那么中位数的位置大概就是第totalLen/2小的位置,我们设为k。在两个数组中查找第k/2小的数,分别找到num1[k/2-1]、num2[k/2-1],那么这两个数分别至多大于在它们之前的k/2-1个数,对于其中较小的数,至多大于两数之前的k-2个数,即最多只有可能是第k-1小的数,不可能是第k小的数,而两者中较大的数可能是第k小的数,由于两数组都为升序数组,因此较小的数组中尚未被排除的从0到k/2-1个数都可以排除。之后k需要更新,减去被排除掉的数的个数。

之后进行相同操作,每一轮都排除num1[k/2-1]和num2[k/2-1]中较小的数以及在同一数组中位于之前的数。直到k=1时,num1[k/2-1]和num2[k/2-1]中较小的数即为所有未排除的数中第1小的数,即为所有数的中位数。

特殊情况:如果num1的长度小于k/2,那么排除num1中的数时,只能排除num1的长度个数,此时说明num1中的数全部被排除,那么num2中的第(num1的长度+num2中被排除的个数)个数就是所有数的中位数;num2的长度小于k/2时同理。

根据以上思路我们可以设计如下代码:

最终代码

public double findMedianSortedArrays(int[] nums1, int[] nums2) {
    // 总长度
    int totalLen = nums1.length + nums2.length;
    if (totalLen % 2 == 1) {
        // 总长度为奇数,寻找第totalLen / 2 + 1小的数
        return getKeythElement(nums1, nums2, totalLen / 2 + 1);
    }
    else {
        // 总长度为偶数,寻找第totalLen / 2和第totalLen / 2 + 1小的数,取平均数
        return (getKeythElement(nums1, nums2, totalLen / 2) + getKeythElement(nums1, nums2, totalLen / 2 + 1)) / 2.0;
    }
}
//寻找第k小的数,k为两数组总长度的一半
private double getKeythElement(int[] nums1, int[] nums2, int k) {
    int len1 = nums1.length, len2 = nums2.length;
    int index1 = 0, index2 = 0;
    while (true) {
        // num1中的数全部排除,num2中未被排除的第k小的数即为中位数
        if (index1 == len1) return nums2[index2 + k - 1];
        // num2中的数全部排除,num1中未被排除的第k小的数即为中位数
        if (index2 == len2) return nums1[index1 + k - 1];
        // k为1时,两数中较小者即为中位数,即未被排除的第1小的数
        if (k == 1) return Math.min(nums1[index1], nums2[index2]);
        // 分别寻找两数组中未被排除的第k/2小的数
        int half = k / 2;
        int newIndex1 = Math.min(index1 + half, len1) - 1;
        int newIndex2 = Math.min(index2 + half, len2) - 1;
        // 两数中较小者至多为第k/2-1个数,不可能为中位数,故排除;
        // 由于数组升序,因此在其之前的数也一并排除
        if (nums1[newIndex1] <= nums2[newIndex2]) {
            k -= (newIndex1 - index1 + 1);
            index1 = newIndex1 + 1;
        }
        else {
            k -= (newIndex2 - index2 + 1);
            index2 = newIndex2 + 1;
        }
    }
}

运行结果

image.png