二分法求两个正序数组合并后的中位数

114 阅读4分钟

这是LeetCode上第一个"困难"题目. 自己求解时, 走进个误区, 一直不能求解.

于是网搜了一些解法, 挺多看起来是对的, 但实际跑几个case就发现错误了.

后来找到这篇正确解法. 分析也很全面, 再补充一些细节在这里.

列一下关键点:

  1. 使用二分法. 求正序数组长度为len的中位数, 就是求这个数组第k小的数, k=(len+1)/2, 假设len为奇数.
  2. 求的是合并后的中位数, 而不是每个裁剪后数组的中位数. 我自己的误区就在这里.
  3. 求第k小的值, 这个k的值在数组每次二分裁剪后, 是会变化的. 因为裁剪后, 整体数组长度变了. k的变化方式参看代码注释.
  4. 长度为len的数组, 取中位数的index的公式 (len+1)/2 和 (len+2)/2

可以在稿纸上划出合并后的正序大数组, 方便推演二分进程. 以此来应对脑力不足, 或者数学力弱的情况.

不知道有多久没做这类题了, 很多细节都退步了, 细细的做吧, 先不图快.

感觉这个用时1ms不太正常, 没准是新人刷题鼓励吧.. image.png

直接上源码吧, 在原来的基础上补充了更完整的注释.

public double findMedianSortedArrays(int[] nums1, int[] nums2) {
    int n = nums1.length;
    int m = nums2.length;

    // 求中值所在的index, 两个数组总长度为偶数时有两个值, 左和右. 奇数时只有一个, 或者说左和右是同一个index.
    int left = (n + m + 1) / 2;
    int right = (n + m + 2) / 2;

    // 将偶数和奇数的情况合并,如果是奇数,会求两次同样的 k 。
    // 奇数时可以优化, 单独做个if处理, 减少运算次数
    return (
            getKth(nums1, 0, n - 1, nums2, 0, m - 1, left) +
            getKth(nums1, 0, n - 1, nums2, 0, m - 1, right)
    ) * 0.5; //这里 乘以0.5 也可以是除以2f, 但不能是除以2.
}

/**
 * 找到两个数组里第k小的值.
 */
private int getKth(int[] nums1, int start1, int end1, int[] nums2, int start2, int end2, int k) {
    int len1 = end1 - start1 + 1;
    int len2 = end2 - start2 + 1;

    if (len1 > len2){
        //让 len1 的长度小于 len2,这样就能保证如果有数组空了,一定是 len1
        return getKth(nums2, start2, end2, nums1, start1, end1, k);
    }

    // 这两个if是递归的终点, 最终的值从这里返回
    if (len1 == 0){
        // 数组1的长度为0, 取数组2的第k小值即可
        return nums2[start2 + k - 1];
    }

    // 第1小值, 直接比较两个数组起始点所在的值, 并返回最小值就可以了. 不用再裁剪数组了
    if (k == 1){
        // 因为是求第k"小"值, 所以这里用min().
        return Math.min(nums1[start1], nums2[start2]);
    }

    // 递归准备:
    // 两个数组, 分别取第k/2小的值, 作为分界线. 注意这里求得是数组index的值, 需要-1
    int halfOfk1 = start1 + Math.min(len1, k / 2) - 1;
    int halfOfk2 = start2 + Math.min(len2, k / 2) - 1;

    if (nums1[halfOfk1] <= nums2[halfOfk2]) {
        // 分支 数组1第(k/2)小的值 小于等于 数组2第(k/2)的值.
        /**
         * 我们假设两个数组合并形成了一个"正序大数组".
         * 二分法并不会真的实现这个数组, 所以我们假设这个大数组在我们的草稿纸上.
         *
         * 因为在正序数组里, 任意第(k/2)小的值肯定小于等于第(k)小的值.
         * 同时在当前if分支里, 数组1第(k/2)小的值又 小于等于 数组2第(k/2)的值,
         * 说明 正序数组1[k/2]的左边, 肯定不包含"大数组"中第k小的值,
         * 所以可以抛弃 数组1(k/2)左边的所有数据. 进入递归
         */
        return getKth(nums1,
                // 抛弃操作, 将(halfOfk1 + 1)作为数组起始值, 也就是抛弃了halfOfk1左边的数据
                halfOfk1 + 1,
                end1,
                nums2, start2, end2,
                /**
                 * k值更新操作:
                 * (halfOfk1+1 - start1)是大数组左边被抛弃的长度,
                 * 如果大数组"第k小值"的左边被减掉x个值, 形成一个新的短数组,
                 * 那么原本求的"第k小值", 在这个新数组里, 就变成"第(k-x)小值"了.
                 */
                k - (halfOfk1 + 1 - start1)
        );
    } else {
        //数组1的分界值 比 数组2的分界值大, 同理, 抛弃数组2分界值的左边.
        return getKth(nums1, start1, end1, nums2, halfOfk2 + 1, end2, k - (halfOfk2 - start2 + 1));
    }
}