LeetCode - 004.寻找两个正序数组的中位数

2022-04-12

题目

来源:004.寻找两个正序数组的中位数

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

算法的时间复杂度应该为 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(m+n),空间复杂度为 O(m+n)

或者是将两个数组合并后再排序,合并数组的时间复杂度为 O(m+n),排序过程如果用快速排序的话平均时间复杂度为 O((m+n)log(m+n))

这种方法虽然简单,但是无法满足题目要求的 O(log(m+n)) 的时间复杂度。

我们想一想,两个数组 AB 的中位数,就是他们合并后那个大数组中间位置的数,而且这个大数组是正序的,那么我们可以把问题转化为寻找这个大数组中第 k 位置的数。

如果大数组的长度为奇数,则 k = (m + n) / 2,中位数就是位置为 k 的数;如果长度是偶数,k = (m + n) / 2,中位数是位置为 kk + 1 两个数的平均值。

题目要求时间复杂度为 O(log(m+n)),很像是二分查找的时间复杂度,我们就从这个角度来考虑。

对于这个大数组来说,位置 k = (m + n) / 2 将他分割为左右两部分,左侧元素的数量为 k 个,这 k 个元素必然一部分在数组 A 中,一部分在数组 B 中。假设各占一半,那么就有 k/2 个元素在 A 中,k/2 个元素在 B 中。

我们来比较 A, B 两个数组中这 k/2 个元素中最大的值,也就是位于 k/2 - 1 这个位置的值。

如果 A[k/2 - 1] < B[k/2 - 1],那么说明在数组 A 中,他的位置 0k/2 - 1 的这些元素,在大数组中都位于中位数的左侧。想象一下,如果在大数组中去除掉这些元素,那么寻找第 k 大的元素这个问题就变成了寻找第 k - k/2 的元素

好,去除这些元素后,数组 A 变成了他的 k/2 - 1 位置右侧所有元素的子数组,k 变成了 k - k/2,我们要做的就是在这个子数组和数组 B 中,继续寻找第 k 大的元素。

这样每次 k 都会变成他的一半大小,直到最终变成寻找第 0 大或第 1 大的元素,这个时候就很方便的找到了。这样看起来符合题目要求的 O(log(m+n)) 的时间复杂度,我们写代码来实现一下。

解答

import java.util.Arrays;

public class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        // 奇数,找第 k 个数;偶数,找第 k 个数和第 k+1 个数,计算平均值
        int k = (nums1.length + nums2.length) / 2;
        boolean isEven = (nums1.length + nums2.length) % 2 == 0;

        int offset1 = 0; // 数组 nums1 去除元素后的偏移量
        int offset2 = 0; // 数组 nums2 去除元素后的偏移量

        while (k > 1) {
            // 比较 k/2 - 1 位置的元素
            int curIdx = k / 2 - 1;

            if (curIdx + offset1 >= nums1.length || curIdx + offset2 >= nums2.length) { // 超出其中一个数组的边界
                int[] nums1Remain = Arrays.copyOfRange(nums1, offset1, nums1.length); // nums1 去除部分元素后的子数组
                int[] nums2Remain = Arrays.copyOfRange(nums2, offset2, nums2.length); // nums2 去除部分元素后的子数组
                return isEven ? (findKthElem(nums1Remain, nums2Remain, k - 1) + findKthElem(nums1Remain, nums2Remain, k)) / 2.0 : findKthElem(nums1Remain, nums2Remain, k);
            }

            if (nums1[curIdx + offset1] < nums2[curIdx + offset2]) {
                offset1 += curIdx + 1; // 增加 nums1 的偏移量,相当于去除了该位置左侧的元素
            } else {
                offset2 += curIdx + 1; // 增加 nums2 的偏移量,相当于去除了该位置左侧的元素
            }
            k -= curIdx + 1; // 问题由 寻找第 k 大的值 转为 寻找第 k - k/2 大的元素
        }

        // 此时中位数左侧的数基本已去除完毕,k = 0 或 k = 1,相当于寻找第 0 大或第 1 大的元素
        // 如果大数组长度为偶数,在找到第 k 大的元素后,还需要找到第 k + 1 大的元素,计算二者的平均值

        int[] nums1Remain = Arrays.copyOfRange(nums1, offset1, nums1.length);
        int[] nums2Remain = Arrays.copyOfRange(nums2, offset2, nums2.length);
        return isEven ? (findKthElem(nums1Remain, nums2Remain, k - 1) + findKthElem(nums1Remain, nums2Remain, k)) / 2.0 : findKthElem(nums1Remain, nums2Remain, k);
    }

    /**
     * 辅助函数,寻找两个数组中第 k 大的元素
     * @param nums1 数组1
     * @param nums2 数组2
     * @param k 第 k 大
     * @return 找到的元素值
     */
    public int findKthElem(int[] nums1, int[] nums2, int k) {
        if (nums1.length == 0) return nums2[k]; // 若 nums1 为空,则第 k 大的数就是 nums2 中位置为 k 的元素
        if (nums2.length == 0) return nums1[k]; // 若 nums2 为空,则第 k 大的数就是 nums1 中位置为 k 的元素
        if (k == 0) return Math.min(nums1[0], nums2[0]); // 第 0 大的数,返回两个数组中的最小值
        // 去除两个数组中最小的元素后,相当于寻找第 k - 1 大的元素
        if (nums1[0] < nums2[0]) return findKthElem(Arrays.copyOfRange(nums1, 1, nums1.length), nums2, k - 1);
        return findKthElem(nums1, Arrays.copyOfRange(nums2, 1, nums2.length), k - 1);
    }

    public void test1() {
        int[] nums1 = {1, 3};
        int[] nums2 = {2};

        double result = findMedianSortedArrays(nums1, nums2);
        System.out.println(result == 2);
    }

    public void test2() {
        int[] nums1 = {1, 2};
        int[] nums2 = {3, 4};

        double result = findMedianSortedArrays(nums1, nums2);
        System.out.println(result == 2.5);
    }

    public void test3() {
        int[] nums1 = {1, 3, 4, 9};
        int[] nums2 = {1, 2, 3, 4, 5, 6, 7, 8, 9};

        double result = findMedianSortedArrays(nums1, nums2);
        System.out.println(result == 4);
    }

    public void test4() {
        int[] nums1 = {1};
        int[] nums2 = {2, 3, 4, 5, 6, 7, 8};

        double result = findMedianSortedArrays(nums1, nums2);
        System.out.println(result == 4.5);
    }

    public void test5() {
        int[] nums1 = {};
        int[] nums2 = {1, 2};

        double result = findMedianSortedArrays(nums1, nums2);
        System.out.println(result == 1.5);
    }

    public static void main(String[] args) {
        Solution solution = new Solution();

        solution.test1();
        solution.test2();
        solution.test3();
        solution.test4();
        solution.test5();
    }
}

上述代码中还加入了几个测试用例。

个人而言,这种解法很难想明白 :P,我也是看了题解后思考很久才搞懂并写代码实现出来,哈哈。

LeetCode

Jeff Liu

LeetCode - 0053.最大子数组和

LeetCode - 003.无重复字符的最长子串