java小白刷题日记——寻找两个正序数组的中位数

129 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第24天,点击查看活动详情

题目描述

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

废话不多说,还是先自己做了一遍,我是把两个数组里面的数边比较大小边插入到新数组里面,然后看其长度是奇数还是偶数来获得中位数。思路很简单,解法很粗暴。其实刚开始我不是这么想的,我想先把两个数组直接前后拼接合并,然后冒泡排序,但是那样更麻烦

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int []nums=new int[nums1.length+nums2.length];
        for(int i=0;i<nums1.length;i++){
            nums[i]=nums1[i];
        }
        for(int i=0;i<nums2.length;i++){
            nums[i+nums1.length]=nums2[i];
        }
        int n=nums.length;
        int count=0,i=0,j=0;
        while(count<n){
            if(i==nums1.length){
                while(j!=nums2.length){
                    nums[count++]=nums2[j++];
                }
                break;
            }
            if(j==nums2.length){
                while(i!=nums1.length){
                    nums[count++]=nums1[i++];
                }
                break;
            }
            if(nums1[i]<nums2[j]){
                nums[count++]=nums1[i++];
            }else{
                nums[count++]=nums2[j++];
            }
        }
        if(n%2==0){
            return (double)(nums[n/2-1]+nums[n/2])/2;
        }else{
            return nums[n/2];
        }
    }
}

但是题目要求的时间复杂度是O(log(m+n),显然暴力解法是无法满足的,那么我们就要想更巧妙的方法。显然,看到log,自然而然就能想到二分查找。

那么我们的思路就可以是这样的,我们不用合并数组,像刚才最后的思路一样,我们要先明确要找的是哪个数,如果总长度是奇数,就找第K小和第k+1小的数,总长为偶数则找第k小的数,此处k=nums.length/2。假设刚开始找第7小的数,我们就比较两个数组第三个数(7/2向下取整),假如nums1的小于nums2的,就把nums1的前三个都排除掉了,那么他们就是前三小的数,接下来我们只需要在剩余的数中找第四小(7-3=4)的数就可以了,那就比较两个数组第二个数,假如nums2的小于nums1的,那么nums2前两个数就被排除了,接下来在剩下的数里找第2小的数(4-2=2),也就是比较第一个,以此类推。

public double findMedianSortedArrays(int[] nums1, int[] nums2) {
    int n = nums1.length;
    int m = nums2.length;
    int left = (n + m + 1) / 2;
    int right = (n + m + 2) / 2;
    //将偶数和奇数的情况合并,如果是奇数,会求两次同样的 k 。
    return (getKth(nums1, 0, n - 1, nums2, 0, m - 1, left) + getKth(nums1, 0, n - 1, nums2, 0, m - 1, right)) * 0.5;  
}
    
    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;
        //让 len1 的长度小于 len2,这样就能保证如果有数组空了,一定是 len1 
        if (len1 > len2) return getKth(nums2, start2, end2, nums1, start1, end1, k);
        if (len1 == 0) return nums2[start2 + k - 1];

        if (k == 1) return Math.min(nums1[start1], nums2[start2]);

        int i = start1 + Math.min(len1, k / 2) - 1;
        int j = start2 + Math.min(len2, k / 2) - 1;

        if (nums1[i] > nums2[j]) {
            return getKth(nums1, start1, end1, nums2, j + 1, end2, k - (j - start2 + 1));
        }
        else {
            return getKth(nums1, i + 1, end1, nums2, start2, end2, k - (i - start1 + 1));
        }
    }

后来看了题解,发现还有一种很巧妙的方法,我们只需要将数组进行切。

一个长度为 m 的数组,有 0 到 m 总共 m + 1 个位置可以切。我们把数组 A 和数组 B 分别在 i 和 j 进行切割。将 i 的左边和 j 的左边组合成「左半部分」,将 i 的右边和 j 的右边组合成「右半部分」。

  • 当 A 数组和 B 数组的总长度是偶数时,如果我们能够保证

*左半部分的长度等于右半部分

i + j = m - i + n - j , 也就是 j = ( m + n ) / 2 - i

*左半部分最大的值小于等于右半部分最小的值 max ( A [ i - 1 ] , B [ j - 1 ])) <= min ( A [ i ] , B [ j ]))

那么,中位数就可以表示如下

(左半部分最大值 + 右半部分最小值 )/ 2。

(max ( A [ i - 1 ] , B [  j  - 1 ])+ min ( A [ i ] , B [ j ])) /  2
  • 当 A 数组和 B 数组的总长度是奇数时,如果我们能够保证

*左半部分的长度比右半部分大 1

i + j = m - i + n - j + 1也就是 j = ( m + n + 1) / 2 - i

*左半部分最大的值小于等于右半部分最小的值 max ( A [ i - 1 ] , B [ j - 1 ])) <= min ( A [ i ] , B [ j ]))

那么,中位数就是 左半部分最大值,也就是左半部比右半部分多出的那一个数。 max ( A [ i - 1 ] , B [ j - 1 ]

想了一圈,发现这个方法是最巧妙的,写起来也不费劲,这样相比第二个方法反倒复杂了很多。