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

78 阅读7分钟

题目描述

给定两个大小分别为 mn 的正序(从小到大)数组 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

解题思路

简单粗暴的解法

如果不考虑限制的话,最简单的想法就是将两个数组合并,排序,并找到中间的点。 代码如下 :

class Solution {

    public double findMedianSortedArrays(int[] nums1, int[] nums2) {

        int n=nums1.length,m=nums2.length;

        int res[] = new int[m+n];

        int k=0;

        for(int i=0,j=0;i<n||j<m;i++,j++){

            if(k/2<n){

                res[k]=nums1[i];

                k++;

            }

            if(k/2<m){

                res[k]=nums2[j];

                k++;

            }        

        }

        Arrays.sort(res);

        if((n+m)%2==0){

            return (float)(res[(n+m)/2]+res[(n+m)/2-1])/2;

        }

        return res[(n+m)/2];

    }

}

显然,虽然可以执行,但是完全满足要求;那么我们思考一下,中位数是什么,就是在这个数据前面的数和在这个数据后面的数的数目是相等的即为([x个数据]中位数[x个数据]),在看一下,两个 正序数组 ,这个条件很重要,我们只需要排除掉两个数组中前面合计x个数据即可不需要进行数组的合并、排序,直接获取到中位数; 所以我们接下来的操作是: 找到前x+1个数据

不合并数组的解法

我们只需要找到在两个数组中找到第[X+1]个数据;不过我们要先把特殊情况列出来,确定好测试数据的边界;

测试数据的边界

  • 第一个数组为空:我们只操作第二个数据;
  • 第二个数组为空: 我们只操作第一个数组;
  • 两个数据都为空: 返回0;
  • 两个数组都不为空:正常处理;

针对以上四种情况,我们可以尝试解题,为了确定我们已经找到了第x+1个数据,我们就要排除掉前x个数据, 这个x是两个数组中一起被排除的,我们可以尝试在找到第x+1个数据前每次只去掉数组中的一个数据,这个无线循环的操作,显然用递归实现比较容易,,我们可以这样写:

class Solution {

    public double findMedianSortedArrays(int[] nums1, int[] nums2) {

        int size = nums1.length + nums2.length;

        int k = size / 2;

        if (size % 2 != 0) {

            return findMid(nums1, nums2, (k + 1));

        } else {

            return (findMid(nums1, nums2, k) + findMid(nums1, nums2, k + 1)) / 2.0;

        }

    }

 
    int findMid(int[] nums1, int[] nums2, int k) {

        if (k == 1) {

            if (nums1.length == 0 && nums2.length == 0) {

                return 0;

            }

            if (nums1.length == 0) {

                return nums2[0];

            }

            if (nums2.length == 0) {

                return nums1[0];

            }

            return Math.min(nums1[0], nums2[0]);

        } else {

            if (nums1.length == 0) {

                return findMid(nums1, Arrays.copyOfRange(nums2, 1, nums2.length), k - 1);

            }

            if (nums2.length == 0) {

                return findMid(Arrays.copyOfRange(nums1, 1, nums1.length), nums2, k - 1);

            }

            if (nums1[0] < nums2[0]) {

                if (nums1.length == 1) {

                    return findMid(new int[] {}, nums2, k - 1);
                }
                return findMid(Arrays.copyOfRange(nums1, 1, nums1.length), nums2, k - 1);

            } else {

                if (nums2.length == 1) {

                    return findMid(nums1, new int[] {}, k - 1);
                }
            }
             return findMid(nums1, Arrays.copyOfRange(nums2, 1, nums2.length), k - 1);
        }
    }
}

代码完成,提交,额…………运行时间太慢了,看看原因,奥奥原来是这个 Arrays.copyOfRange(nums2, 1, nums2.length) 因为偷懒,所以每次递归执行都执行了创建和复制的,频繁的IO操作;大大的减慢了速度, 换个思路想一下,我们需要排除掉前x个数据,并不是真的要排除出去,只需要记录下来下标位置,下次执行时,这个下标以前的都不处理,只从当前下标进行处理即可,那就请出我们的好兄弟 int i, int j,于是下面的递归方法变成了这样:

//使用 i ,j来标记已经废弃的数据的下标
int findMid(int[] nums1, int[] nums2, int k, int i, int j) {

        if (nums1.length == i && nums2.length == j) {

            return 0;

        }
       //当数组一下标已经全废弃时,已经无需对数组二进行重复操作,直接找到对应下标的数据即可
        if (nums1.length == i) {
            // 这个k 就是 上文说的x, K-1就是取消掉的前k的文件 +j是因为前j个数据已经被我们排除掉了
            return nums2[ j + (k - 1)];

        }

        if (nums2.length == j) {

            return nums1[i +(k - 1) ];

        }

        if (k == 1) {

            return Math.min(nums1[i], nums2[j]);

        } else {

            if (nums1[i] < nums2[j]) {

                return findMid(nums1, nums2, k - 1, i + 1, j);

            } else {

                return findMid(nums1, nums2, k - 1, i, j + 1);

            }

        }
    }

代码提交,哦哦哦, 运行时间已经变为了1ms,完美…………但是,我们来算一下时间复杂度;这个很简单, 假设 数组一没有数据,数组2,有9个数据,我们需要执行几次方法呢: 每次排除一个,需要执行4次, n个数据就是n/2次,所以时间复杂度为O(n),

afa84da0d130c5eabd23bbbfcc654206.png

看起来时间复杂度还是没有满足要求, 要满足它的要求O(log(m+n)),很明显我们要处理的激进点,通过常见的时间复杂度,那就是二分法,那我们再改改,每次排除一个太慢了,我们每次排除一半,于是递归代码变成了这样:

int findMid(int[] nums1, int[] nums2, int k, int i, int j) {

        if (nums1.length == i && nums2.length == j) {

            return 0;

        }

        if (nums1.length == i) {

            return nums2[k - 1 + j];

        }

        if (nums2.length == j) {

            return nums1[k - 1 + i];

        }

        if (k == 1) {

            return Math.min(nums1[i], nums2[j]);

        } else {

            if (nums1[i] < nums2[j]) {

                return findMid(nums1, nums2, k / 2, i + (k / 2), j);

            } else {

                return findMid(nums1, nums2, k / 2, i, j + (k / 2));

            }

        }

    }

执行………………错啦,哪里出问题了? 奥奥,原来是这个i+(k/2),j+(k/2) 下标越界了,修复一下,多一层判断,貌似越写越复杂了………………;思路是对的,那么删掉上面的东西,重新盘点一下逻辑,首先,我们用两个整数,i,j代表舍弃了的中位数前面的一半的数,用k代表中位数,所以 k-1 = i+j 时,满足条件,由于使用了二分法,所以我们每次都折半舍弃,既然第k个值是我们要的,所以前k/2个值都可以舍弃;每次 i 和j舍弃的值都是 k/2 ,但是,如果舍弃的超过了我们的边界怎么办?所以我们要在每次进入递归进行边界判定,防止我们下标出界,综上代码就成了下面这样

class Solution {

    public double findMedianSortedArrays(int[] nums1, int[] nums2) {

        int size = nums1.length + nums2.length;

        // 奇数

        if (size % 2 != 0) {

            return findMid(nums1, nums2, 0, 0, (size + 1) / 2);

        } else {

            return (findMid(nums1, nums2, 0, 0, (size + 1) / 2) + findMid(nums1, nums2, 0, 0, (size + 2) / 2)) / 2;

        }

    }

  


    // 第k个数就是我们要找的中位数,i 和j使是我们要舍弃的数,当i+j = k-1时,我们就找到了第k个数;

    private double findMid(int[] nums1, int[] nums2, int i, int j, int k) {

  


        if (nums1.length == i && nums2.length == j) {

            return 0;

  


        }

        if (nums1.length == i) {

            // 去除掉前面k-1个数,剩下的就是第k个数;由于之前还舍弃了j个数,所以要加上j;

            return nums2[(k - 1) + j];

        }

        if (nums2.length == j) {

            // 同上

            return nums1[i + k - 1];

        }

        // 当k = 1 时,我们就找到了中位数

        if (k == 1) {

            // return Math.min(nums1[i + k - 1], nums2[j + k - 1]);

            return Math.min(nums1[i], nums2[j]);

  


        } else {

            // 在进入下次递归之前,需要进行边界判定;

            int m = k / 2;

            // 由于是要取消的较小的数,所以当下次的值大于数组长度时,我们就取最大值;

            int value1 = i + m - 1 >= nums1.length ? Integer.MAX_VALUE : nums1[i + m - 1];

            int value2 = j + m - 1 >= nums2.length ? Integer.MAX_VALUE : nums2[j + m - 1];

            if (value1 < value2) {

                return findMid(nums1, nums2, i + m, j, k - m);

            } else {

                return findMid(nums1, nums2, i, j + m, k - m);

            }

        }

    }
}

提交运行,执行成功,完美