算法题:寻找两个正序数组的中位数

题目描述

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

思路

首先看到题目,先不考虑时间复杂度,第一个想到的思路就是把两个数组合并,合并以后直接找出中位数。这里找中位数需要分奇偶两种情况,但是这里有个小技巧,用len表示数组长度,只要找第(len+1)/2和第(len+2)/2两个位置的数,然后取平均就行了,奇数时两个取到的是同一个数,偶数就是中位数的定义。

首先最简单粗暴的代码:

    public static double findMid(int[] nums1, int[] nums2){
        int m = nums1.length;
        int n = nums2.length;
        int len = m + n;
        int[] nums = new int[len];
        int count = 0;
        int i = 0, j = 0;
        while (count != len){
            // nums1数组已经遍历完
            if (i == m) {
                while (j != n) {
                    nums[count++] = nums2[j++];
                }
                break;
            }
            // nums2数组已经遍历完
            if (j == n) {
                while (i != m) {
                    nums[count++] = nums1[i++];
                }
                break;
            }

            if (nums1[i] < nums2[j]){
                nums[count ++ ] = nums1[i ++ ];
            }else {
                nums[count ++ ] = nums2[j ++];
            }
        }
        return ((nums[(len+1)/2-1]+nums[(len+2)/2-1]))/2.0;
    }

力扣的结果
虽然时间很短,但其实按照这种方法的时间复杂度是O(m+n),很明显达不到要求
开辟了一个数组,长度是m+n,所以空间复杂度也是O(m+n)

改进方法

我们只需要知道中间的元素,其实并不需要生成一个新数组,我们只需要对两个数组进行遍历,然后淘汰比较小的那个,然后将那个元素的指针往后移动。

    public static double findMid1(int[] nums1, int[] nums2){
        int m = nums1.length;
        int n = nums2.length;
        int len = m + n;
        // 保存循环的结果
        int right = 0,left = 0;
        int nums1Start = 0, nums2Start = 0;
        for (int i = 0 ;  i <= len/2 ; i ++){
        	// 每次将上一次循环的结果给left,保证这两个值是我们筛选的最后两个值
            left = right;
            // 这里是为了防止数组遍历完了产生越界
            if (nums1Start < m && (nums2Start >= n ||nums1[nums1Start] < nums2[nums2Start])){
                right = nums1[nums1Start ++];
            }else {
                right = nums2[nums2Start ++];
            }
        }
        if ((len & 1) == 0){
            return (left + right)/2.0;
        }else{
            return right;
        }
    }

在这里插入图片描述
这里时间复杂度其实是没变的,但是空间复杂度简化了不少,需要的变量数和m,n都无关,所以空间复杂度是O(1)。

二分法

其实根据O(log(m+n))的时间复杂度,应该要想到二分法。上一个思路中,我们是一个一个的筛选淘汰,那么这道题,换个说法就是找到第k个元素,那么我们把k二分,每次淘汰k/2个元素。
我们找到两个数组分别的k/2的位置,然后比较他们的大小,将比较小的那个和他之前的元素全部淘汰。这些元素一定不是第k个元素。然后就是寻找第k-k/2个元素是哪个,并且重复上述的工作,这里很容易想到递归。
但是在开始递归之前,我们需要考虑一些极端情况

  1. 如果k/2大于某个数组的最大长度,我们将无法判断这个数组中元素的位置,这个时候我们直接淘汰另一个数组的k/2之前的所有元素,因为这些元素一定在k之前。
  2. 如果起始位置已经超过了数组长度,那么说明整个数组中所有的数字都已经被淘汰,那么直接从另一个数组中找就行了
  3. 如果k=1,那么直接比较两个数组的第一个元素大小即可
    public static double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int m = nums1.length;
        int n = nums2.length;
        int right = (m + n + 2)/2;
        int left = (m + n + 1)/2;
        return (findkth(nums1,0,nums2,0,right) + findkth(nums1,0,nums2,0,left))/2.0;
    }
    public static int findKth(int[] nums1,int i,int[] nums2,int j,int k){
        if (nums1.length <= i){
            return nums2[ j + k - 1];
        }
        if (nums2.length <= j){
            return nums1[i + k - 1];
        }
        if (k == 1){
            return Math.min(nums1[i],nums2[j]);
        }
        int midNums1 = (i + k/2 - 1 < nums1.length)?nums1[ i + k/2 - 1]:Integer.MAX_VALUE;
        int midNums2 = (j + k/2 -1 < nums2.length)?nums2[j + k/2 -1]:Integer.MAX_VALUE;
        if (midNums1 < midNums2){
            return findKth(nums1,i + k/2,nums2,j,k-k/2);
        }else {
            return findKth(nums1,i,nums2,j+k/2,k-k/2);
        }
    }

在这里插入图片描述
这里明显能够看出来,内存消耗少了不少,虽然是递归的操作,但由于使用的是尾递归,所以空间复杂度与m和n也无关是O(1)
而每次递归都会淘汰点k/2个元素,k=m+n,所以时间复杂度是O(log(m+n)),符合题目的要求。

改进二分法

其实还有一种将时间复杂度压缩到更小的方法比较复杂
首先我们来看一下百度百科中对中位数的定义

中位数(Median)又称中值,统计学中的专有名词,是按顺序排列的一组数据中居于中间位置的数

然后我们将数组进行切分
如果有 m 个元素

下面的数字代表下标
,0,1,2,3,4,5,.....,m,

每个逗号代表一个切分的位置,总共有 m + 1 种切分方式
那么我们将两个数组分别进行切分

A[0],A[1],A[2]...A[i-1] | A[i] .... A[m-1]
B[0],B[1],B[3]...B[j-1] | B[j]...B[n-1]

对 A 数组在 i 的位置切分, B 数组在 j 的位置切分,将 A 的左半部分和 B 的左半部分合并,右半部分合并并且满足一些条件,就能找出中位数

  1. 如果 m+n 是偶数,那么我们只需要满足左半部分等于右半部分即 i + j == m - i + n - j 和左半的最大值小于等于右半的最小值即 max(A[i-1],B[j-1]) <= min (A[i],B[j]) 就能轻松的找出中位数 mid = ( max(A[i-1],B[j-1]) + min (A[i],B[j]) ) / 2.0
  2. 如果 m+n是奇数,那么我们满足左半部分比右半部分多一位即 i + j == m - i + n - j + 1,并且左半的最大值小于等于右半的最小值,就能找出中位数是左半部分多出来的那一个元素即 mid = max( A[ i-1 ], B[ j - 1 ])
    那么下面的问题就是如何求解出 i 和 j 的值了,因为 m 和 n 的值都是固定的,我们可以计算出 i 和 j 的关系:
m + n 为偶数的时候
	j = ( m + n ) / 2 - i
m + n 为奇数的时候
	j = ( m + n + 1) / 2 - i
又因为都是 int 类型的数那么 i 和 j 的关系就可以统一成
	j = ( m + n + 1) / 2 - i

因为 0 ≤ i ≤ m 且 0 ≤ j ≤ n
按照 j = ( m + n + 1) / 2 - i 和 0 ≤ i ≤ m 为了可以计算出 j 的取值范围 [ ( m + n + 1 ) / 2 - m , ( m + n + 1) / 2 ] 为了保证这个数在 [ 0 , j ] ,必须使 m < n,所以在程序中 , 我们需要进行判断。
而为了保证 max(A[ i - 1 ],B[ j - 1 ]) <= min (A[ i ],B[ j ]), 我们来看一下

A数组  2  4  6  |  8  10
B数组     1  3  |  4  9  12

这种情况因为是A数组的最大值更大所以就需要将 i 向左移动来平衡一下

A数组        2  4  |  6  8  10
B数组     1  3  4  |  9  12

另一种情况就是B数组的最大值更大即将 i 向右移动来平衡
然后我们来考虑一下极端的情况即数组切到了最边界,这时候其实和前面的方法是一样的,只是某个最大值和最小值就是没有切到边界的数组的值
例如当 i = 0 时

A数组                     |  2  4  6  8  10
B数组     1  3  4  9  12  |

左半部分最大值就是 B[ j - 1 ] 右半部分最小值就是 A [ i ]
这些问题解决了就是代码部分了,依然是用二分法寻找 i 的值

    public static double findI(int[] nums1, int[] nums2){
        int m = nums1.length, n = nums2.length;
        if ( m > n ){
            return findI(nums2,nums1);
        }
        int begin = 0 , over = m;
        int leftMax = 0 , rightMin = 0;
        while (begin <= over){
            int i = ( begin + over ) / 2;
            int j = (m + n + 1) / 2 - i;

            int nums1i_1 = (i == 0 ? Integer.MIN_VALUE : nums1[i - 1]);
            int nums1i = (i == m ? Integer.MAX_VALUE : nums1[i]);
            int nums2j_1 = (j == 0 ? Integer.MIN_VALUE : nums2[j - 1]);
            int nums2j = (j == n ? Integer.MAX_VALUE : nums2[j]);

            leftMax = Math.max(nums1i_1,nums2j_1);
            rightMin = Math.min(nums1i,nums2j);

            if (leftMax <= rightMin){
                break;
            }else if (nums1i_1 > nums2j){
                over = i -1;
            }else if (nums2j_1 > nums1i){
                begin = i + 1;
            }

        }
        return (m + n) % 2 == 0 ? (leftMax + rightMin) / 2.0 : leftMax;    
	}

在这里插入图片描述
这个方法的时间复杂度是O( log ( min ( m , n ) ) ),只需要对较短的数组进行遍历。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值