Leetcode04--给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的中位数。

题目

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

示例 3: 输入:nums1 = [0,0], nums2 = [0,0] 输出:0.00000

示例 4: 输入:nums1 = [], nums2 = [1] 输出:1.00000

示例 5: 输入:nums1 = [2], nums2 = [] 输出:2.00000

提示:
nums1.length == m
nums2.length == n
0 <= m <= 1000
0 <= n <= 1000
1 <= m + n <= 2000
-106 <= nums1[i], nums2[i] <= 106

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/median-of-two-sorted-arrays
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、归并算法

说明:第一次做这个题的时候用的是归并思想,但是归并的话时间和空间复杂度都是o(m+n),很简单,所以直接贴代码。

class Solution {
    
    
private static int[] merge(int[] a, int[] b) {
    
    
	// TODO Auto-generated method stub
	int[] temp=new int[a.length+b.length];
	int i=0;
	int j=0;
	int k=0;
	while(i<a.length&&j<b.length) {
    
    
		temp[k++]=a[i]<b[j]?a[i++]:b[j++];
	}
	while(i<a.length) {
    
    
		temp[k++]=a[i++];
	}
	while(j<b.length) {
    
    
		temp[k++]=b[j++];
	}
	return temp;
}

    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
    
    
	int[] c=merge(nums1,nums2);
	return c.length%2==1?c[c.length/2]:((double)c[c.length/2]+c[c.length/2-1])/2;
}
}

二、二分查找法

时间复杂度:o(log(m+n))

如何确定中位数呢?我们可以引入一个分隔线。
例如在一个数组的时候:
1)数组长度为偶数时
eg.
①长度为4的数组,分割线左边有(4+1)/2个数,中位数为分割线两边的数的平均值:(2+3)/2=2.5【说明:本应该是分割线前面有4/2,之所以(4+1)/2是为了和数组长度为奇数时形式一样,避免讨论】
在这里插入图片描述
②两个长度为2的数组,两数组分割线左边的数分别为:2和0,两数组分割线左边数的总数是(2+2+1)/2,中位数为分割线左边最大的数2和分割线右边最小的数的3平均值(2+3)/2=2.5
【说明:本应该是分割线位置(2+2)/2,(2+2+1)/2是为了和数组长度为奇数时形式一样,避免讨论】
在这里插入图片描述
分析:上面这个分割线的位置是唯一的。其要满足两个条件:
①分割线左边的数(图中分割线左边数为1、2)为两个数组的长度的一半
②分割线左边的数(上图中数为1、2)必须小于等于右边的数(上图中数为3、4)。

怎么将上面的逻辑分析转化成直观的数学表达式呢?
设数组为nums1[]、nums2[],分割线的位置在第一个数组为i,第二个数组为j。
①第一个条件很容易转化成一个数学表达式:
i+j=(m+n)/2
注:m+n为偶数,所以加1不影响整除结果,即是(m+n)/2 =(m+n+1)/2,i+j=(m+n+1)/2,之所以这样做是为了与后面的数组长度为奇数的结果相对应。
②因为nums1和nums2都是有序数组,所以在分割线左边的nums1的数肯定小于分割线右边nums1的数,同理,nums2亦然。所以,只需要如下两个数学表达式:

nums1[i-1]<nums2[j];
nums1[i]>nums2[j-1];

2)数组长度为奇数时
eg.
1.在有一个长度为3的数组时,分割线左边应该有(3+1)/2=2个数,分割线左边的第一个数则是中位数
在这里插入图片描述
2.下面有两个数组,长度总和为奇数(1+2=3),为了寻找中位数,我们作出了如下分割线,分割线左边有(3+1)/2=2个数,且要满足条件2(条件2见总结),很明显,分割线位置总和为2,且等于(数组总长度+1)/2即是(3+1)/2。

在这里插入图片描述
分析知道和1中奇数情况一模一样,所以总结出:
设数组为nums1[]、nums2[],分割线的位置在第一个数组为i,第二个数组为j。
i+j=(m+n+1)/2; nums1[i-1]<nums2[j]; nums1[i]>nums2[j-1];

那么我们如何用二分查找来寻找中位数呢?
i+j=(m+n+1)/2;知道,我们只需要确定在一个数组中的位置i,就可以找到另一个数组中的位置j。为了解决数组越界,我们选择在数组长度比较小的那个数组上进行二分查找。那么,当什么位置查找成功呢?这个位置是唯一的吗?根据中位数的性质,非常容易可以推断出这个分割线的位置是唯一的。那么,我们在查找到这个位置的时候如何停止呢?我们引入左右分割线左右边界(left=0,right=nums1.length,如下图),然后利用nums1[i-1]<nums2[j]; nums1[i]>nums2[j-1];逐步缩小搜索区间(具体思路在文末有详细描述),最终左右边界相等时搜索停止,即当left=right时搜索停止且left即为nums1中分割线i的位置。
在这里插入图片描述

class Solution {
    
    
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
    
    
    int m=nums1.length;
    int n=nums2.length;
    if((m+n)==1)return m==1?nums1[0]:nums2[0];
    int left=0;
	int right=m;
    if(m>n){
    
    
        right=n;
        int[] temp=nums1;//注意这种写法
        nums1=nums2;
        nums2=temp;
    }
	int total=(m+n+1)/2;
    while(left<right) {
    
    
    	int i=left+(right-left)/2;
    	int j=total-i;
    	if(nums1[i]>nums2[j-1]) {
    
    
    	//搜索区间[left,i],要注意区间只有两个数时是否会无限循环
    		right=i;
    	}
    	else {
    
    
    	//搜索区间[i+1,right],要注意区间只有2个数是否会无限循环
    		left=i+1;
    	}
    }    
    int i=left;
    int j=total-i;
    int lvalue,rvalue;
    if(i-1<0) {
    
    
     lvalue=nums2[j-1];
    }
    else if(j-1<0) {
    
    
    	lvalue=nums1[i-1];
    }
    else lvalue=nums1[i-1]>nums2[j-1]?nums1[i-1]:nums2[j-1];
    if(i>=nums1.length) {
    
    
    	rvalue=nums2[j];
    }
    else if(j>=nums2.length) {
    
    
    	rvalue=nums1[i];
    }
    else rvalue=nums1[i]<nums2[j]?nums1[i]:nums2[j];

    	return (m+n)%2==0?((double)(lvalue+rvalue)/2):lvalue;
}
}

值得强调的是:
if(nums1[i]>nums2[j-1]) {
//搜索区间[left,i]
right=i;
}
else {
//搜索区间[i+1,right]
left=i+1;
}
我们很容易理解,当nums1[i]<=nums2[j-1]时,分割线的搜索区间应该是[i+1,right],为什么在else的时候分割线的搜索区间一定是[left,i]呢?

我们用A1、A2模拟分割线左右两边nums1的数据,B1、B2为分割线左右两边nums2的数据,因为我们只能找到一条在符合i+j=(m+n+1)/2的同时,符合A1<B2,B1<A2的分割线,而且,如果nums1中分割线左移(由i+j=(m+n+1)/2可知nums2中分割线右移),A1、A2减小,B1、B2增大。我们在来回顾之前的问题,A1>B2时,分割线的搜索区间应该在哪里呢?很明显,为了使B1>A2,分割线应该尽量左移,直到符合条件的最左端,因为越左,B1就会增大,A2就会减小,就会趋近A1>B2的条件。
在这里插入图片描述
【注意】
1.在数组的有关问题中,一定要注意越界问题,在分割搜索区间的有关算法中,一定要注意栈溢出问题(也就是无限循环)。
2.代码还未进行优化,参考了leetcode的标准答案,发现可以用一些函数解决长串的if else逻辑判断,代码具有更好的可读性。有时间我会把优化后的代码补充的文章的末尾。

优化后:

class Solution {
    
    
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
    
    
    int m=nums1.length;
    int n=nums2.length;
    if((m+n)==1)return m==1?nums1[0]:nums2[0];
    int left=0;
	int right=m;
    if(m>n){
    
    
        right=n;
        int[] temp=nums1;//注意这种写法
        nums1=nums2;
        nums2=temp;
    }
	int total=(m+n+1)/2;
    while(left<right) {
    
    
    	int i=left+(right-left)/2;
    	int j=total-i;
    	if(nums1[i]>nums2[j-1]) {
    
    
    		right=i;
    	}
    	else {
    
    
    		left=i+1;
    	}
    }    
    int i=left;
    int j=total-i;
    int nums1LValue=i-1>=0?nums1[i-1]:Integer.MIN_VALUE;
    int nums2LValue=j-1>=0?nums2[j-1]:Integer.MIN_VALUE;
    int nums1RValue=i>=nums1.length?Integer.MAX_VALUE:nums1[i];
    int nums2Rvalue=j>=nums2.length?Integer.MAX_VALUE:nums2[j];
  return (m+n)%2==0?(double)(Math.min(nums1RValue, nums2Rvalue)+Math.max(nums1LValue, nums2LValue))/2:Math.max(nums1LValue, nums2LValue);
}
}

猜你喜欢

转载自blog.csdn.net/m0_51801058/article/details/113819230