热题100 - 4. 寻找两个正序数组的中位数

62 阅读6分钟

题目描述:

给定两个大小分别为 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

思路:

困难题确实不一样。能O(1)获得的是两个数组分别的长度。一个数组的中位数怎么算?

应该是 mid = (0 + n)/2。对吗?不完全对。奇数的情况是数组中的一个值。偶数的情况是两个值的平均。

倘若合并了,能快速找到吗?可以。但是合并的时间复杂度最快也是O(nlogn) - 快速排序

很明显这个是O(log(m+n)),二分查找

假设我要找的是合并后排序数组的第k个值(奇数情况)。k = (0 + 3)/2 = 1。位置正确,这意味着我要找的是两个数组“合并”之后的第k个值。

没有思路了。看看题解吧。

DS提供的思路:

首先确定较小的数组。找到一个分割点i,对较长的数组,找到分割点j。对于i和j,需要满足:

  1. i + j = (n + m)/2 - 这意味着找到的分割点刚好是一半

  2. 这两个分割点的关系:需要满足

    • i的左边最大值要小于j的右边最小值
    • j的左边最大值要小于i的右边最大值
    • 且i + j = (n + m)/2
  3. 如果分割成功:

    • 如果m+n是奇数,则取min(num1[i], num2[j])
    • 如果m+n是偶数,则取(max(left) + min(right))/2
  4. 边界条件处理:

    • 可能一个数组完全比另一个数组大。这种情况就是需要判断越界。然后判断哪个更大。然后根据奇偶性找到位置。

太难了,放弃了。

DS的解答

以下是满足要求的完整代码实现,已处理所有边界条件并添加详细注释:

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        // 确保nums1是较短的数组以优化时间复杂度
        if (nums1.length > nums2.length) {
            int[] temp = nums1;
            nums1 = nums2;
            nums2 = temp;
        }
        int m = nums1.length, n = nums2.length;
        
        // 计算中位数的分割点
        int totalLeft = (m + n + 1) / 2;
        
        int left = 0, right = m;
        while (left < right) {
            // 二分法确定nums1的分割点i
            int i = left + (right - left + 1) / 2;
            int j = totalLeft - i;
            
            // 关键比较逻辑(处理边界)
            if (nums1[i - 1] > nums2[j]) {
                right = i - 1; // i需要减小
            } else {
                left = i;      // i可以增大
            }
        }
        
        // 最终确定分割点
        int i = left;
        int j = totalLeft - i;
        
        // 计算左半部分最大值(处理边界)
        int nums1LeftMax = (i == 0) ? Integer.MIN_VALUE : nums1[i - 1];
        int nums2LeftMax = (j == 0) ? Integer.MIN_VALUE : nums2[j - 1];
        int actualLeftMax = Math.max(nums1LeftMax, nums2LeftMax);
        
        // 计算右半部分最小值(处理边界)
        int nums1RightMin = (i == m) ? Integer.MAX_VALUE : nums1[i];
        int nums2RightMin = (j == n) ? Integer.MAX_VALUE : nums2[j];
        int actualRightMin = Math.min(nums1RightMin, nums2RightMin);
        
        // 根据总长度奇偶性返回结果
        if ((m + n) % 2 == 1) {
            return actualLeftMax;
        } else {
            return (actualLeftMax + actualRightMin) / 2.0;
        }
    }
}

关键点说明:

  1. 数组交换优化:通过确保nums1始终是较短的数组,将时间复杂度优化到O(log(min(m,n)))

  2. 分割点计算: • totalLeft = (m + n + 1) / 2 统一处理奇偶情况 • 使用left + (right - left + 1) / 2避免死循环

  3. 边界处理技巧: • 使用Integer.MIN_VALUE/MAX_VALUE处理分割点在数组端点的情况 • 通过i == 0/i == m等判断处理空数组的情况

  4. 中位数计算逻辑: • 奇数长度时直接取左半最大值 • 偶数长度时取左右部分平均值

复杂度分析:

• 时间复杂度:O(log(min(m,n))) • 空间复杂度:O(1)

该实现通过了LeetCode所有测试用例,可直接提交使用。建议结合二分查找示意图理解分割点的移动逻辑。

再战一次

先上代码:

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        if (nums1.length > nums2.length) {
             int[] temp = nums1;
             nums1 = nums2;
             nums2 = temp;
        }
        int m = nums1.length, n = nums2.length;
        int le = 0, ri = m;
        while (le < ri) {
            int i = (le + ri + 1) >> 1; // dont use min to compare
            int j = ((n + m + 1) >> 1) - i;

            int numsi_1 = (i == 0) ? Integer.MIN_VALUE : nums1[i-1];
            int numsj = (j == n) ? Integer.MAX_VALUE : nums2[j];
            if (numsi_1 > numsj) {
                ri = i - 1; // try i first.
            } else {
                le = i;
            }
        }
        int i = le, j = ((n + m + 1) >> 1) - i;
        int numsi_1 = (i == 0) ? Integer.MIN_VALUE : nums1[i-1];
        int numsi = (i == m) ? Integer.MAX_VALUE : nums1[i];
        int numsj_1 = (j == 0) ? Integer.MIN_VALUE : nums2[j-1];
        int numsj = (j == n) ? Integer.MAX_VALUE : nums2[j];

        if (((n + m) % 2) == 1) {
            return Math.max(numsi_1, numsj_1);
        }
        return (Math.max(numsi_1, numsj_1) + Math.min(numsi, numsj)) / 2.0;
    }
}

什么?这次竟然成功了?原因呢?是直接写出来的吗?非也。

这道题很复杂,大体可以分为以下几部分:

  1. 确保nums1是较短的数组,如果较长,则交换。
  2. 对较短的nums1进行二分查找
    • 计算i的值,这里会纠结是(left + right + 1)/2 还是 (left + right)/2,对吗。类似的,计算j也在考虑要不要+1的问题。
    • 判断条件, 这里要清楚,找的是分割点,所以判断条件肯定是i-1和j比,或者i和j-1比。
    • 边界的更新,对right,是更新成i-1,还是更新成i?对left,更新成i+1还是i?
  3. 二分查找结束了,i是left?还是+1或者-1?
  4. 奇数的情况,这个要固定成i-1和j-1的较大值
  5. 偶数的情况,类似的,较大值+较小值的和,再除以二。

可见即便是了解了这道题的基本思路,但是细节上依旧很容易犯错,导致无法AC。

我的建议:

举例子。一定要在纸上画,靠小例子来验证你的思路。为了节约思维负担,这里有个小技巧,只需要无脑记忆left+right+1和n+m+1的部分就好了。剩下需要额外推导的就是right = i - 1还是i,left = i还是i+1了。无非是四种情况。

而我推荐你使用下面的几个例子来画图验证(左侧是nums1,右侧是nums2):

[1], [2, 3]

[2], [1, 3]

[1, 2], [3, 4]

[3, 4], [1, 2]

是用以上的例子去研究结束之后的i和j,看看是怎么划分的。正确的划分应该如下:

[1 /], [2,/ 3] 奇数,取i-1和j-1较大值。/左边是x-1,右边是x。

[2 /], [1,/ 3] 奇数,取i-1和j-1较大值。/左边是x-1,右边是x。

[1, 2 /], [/3, 4] 偶数,取平均。左边没数字,就是Integer.MIN_VALUE。右边同理。

[/3, 4], [1, 2/] 偶数,取平均。i是3, i-1是最小值,j-1是2,j是最大值。左边取大=2, 右边取小=3.取平均。

知道了什么是“正确”之后就好办了,就去验证right和left分别的设定就好了。

希望对你有帮助。