一、介绍
这是Leetcode中的第4个题目,可以说,很多按照顺序刷题的同学,刷着刷着,突然就碰到了这个题目,然后就懵逼了。特别是看到了上面说的时间复杂度限制为O(log (m+n))的时候,感觉难以下手。其实我也是哈哈哈,但我没有按照顺序做题,而是在顺序的基础上,做完一个题目之后,根据accepted页面下,Next challenges来做,如图所示:
这样可以延续你做题的思路,是很好的不断巩固的一种方式。所以,我到了做完60题之后,才碰都这个题目。所以,哈哈哈,经验告诉我,实在不行,就先参考下别人的代码。题目如下:
There are two sorted arrays nums1 and nums2 of size m and n respectively.
Find the median of the two sorted arrays. The overall run time complexity should be O(log (m+n)).
You may assume nums1 and nums2 cannot be both empty.
Example 1:
nums1 = [1, 3]
nums2 = [2]
The median is 2.0
Example 2:
nums1 = [1, 2]
nums2 = [3, 4]
The median is (2 + 3)/2 = 2.5
二、解题思路
首先,我们考虑两个数组的长度len1+len2为偶数的情况。由于nums1和nums2都是已经排好了序的数组,所以,我们很明显想要找到在一个长度为len1+len2的有序数组,例如:1 2 3 4 5 6 7 8中,去找中间的两个元素4 5,然后取平均值4.5,就是中位数了。但很明显,将两个有序数组合成一个有序数组,其时间复杂度太大了。
之后,可以想到,既然只要中位数,就是说和之前或者之后的元素,其实关系没有特别大,至少不至于非要像1 2 3 4 5 6 7 8那样排地整整齐齐(这里可别说什么一家人就要整整齐齐的骚话了)。如果是:1 3 2 4 5 7 6 8这样的情况,其实也可以行。这样我们就可以想到,似乎并不需要将两个有序数组合成一个有序数组。 例如下面的例子:
[1, 2, 4 ] | [6*, 8, 9 ] [a1, a2] | [a3, a4, a5, a6]
[2, 3, 5*] | [10, 11, 12] [b1, b2, b3, b4, b5] | [b6, b7, b8]
我们只要能够保证:分隔符左边的元素都小于右边的元素,且两边的元素各占整体的一半就可以了。从左边那个例子推广到右边那个稍微一般一点的例子,可以发现,只要满足 a2、b5 < a3、b6就可以了。其中a2 < a3 和 b5 < b6是题设中有序数组给的。所以,我们只需要找到这样的一个分割方案,使得:1。 a2 < b6 2。b5 < a3就可以了。当然分割线两边的元素得一样多。
有了这样的思路,我们做下面一些变量的声明:
int start = 0; //指向最左边的切割位置
int end = len1; //指向最右边的切割位置
int cut1 = 0; //代表nums1中分割线左边有cut1个元素
int cut2 = 0; //代表nums2中分割线左边有cut2个元素
int half = (len1 + len2) / 2; //代表两个分割线左侧元素的个数和。
// 当len1+len2为奇数的时候,右边的数量多一个,所以最终返回右边两侧中较小的那个数就可以
所以,其中最后一条注释,其实也就交代了当len1+len2为奇数时的处理方法。
现在,问题就转化为了找cut1的值,然后cut2的值,就是half - cut1。再看看时间复杂度的要求:O(log (m+n))。有没有觉得很熟悉,时间复杂度为取对数的查找方法是哪个?
很明显,每次折中一下,然后继续查找,就是折半查找——二分查找。那么,我们再想一下,你是愿意去在较长的数组,还是较短的数组中去做查找,和明显是较短的嘛。所以,我们开始的时候,可以将nums1设置为两者中较短的那个,然后从中找cut1,然后half - cut1就是cut2的值了。所以时间复杂甚至都变成了O(log (min(m, n)))。从而我们也得到了最优解。
三、附加说明
为了方便起见,我们将切割线两侧的元素设置为了L1,L2,R1,R2,它们的位置如下。当出现切割位置在数组最前面或者最后的时候,就需要特别处理一下:
[......L1] (cut1) [R1......]
[......L2] (cut2) [R2......]
举个例子:如果cut1在nums1的最前面了,很明显,L1的值找不到了。由于这样的情况也是满足条件的,毕竟不能再把cut1往左侧走了,所以我们反过来为了满足L1<R2的条件,一不做二不休,干脆给L1赋上Int的最小值。当然,如果cut2在nums1的最右边了,我们就给R1赋上Int的最大值。
好了,下面就是最精彩的源码环节,还有其他的注释,包括测试用例,也都在里面了。
四、源码
/**
* @author lei
* @date 2019.9.26 17:26
* @description: 找两个排好序的数组中所有元素的中位数
*/
public class MedianOfTwoSortedArrays {
public static void main(String[] args) {
System.out.println("test: ");
//int [] nums1 = {9, 10, 11, 12, 22};
//int [] nums2 = {0, 3, 5, 6, 8, 11, 12};
int [] nums1 = {2};
int [] nums2 = {2};
double res = findMedianSortedArrays(nums1, nums2);
System.out.println("res = " + res);
}
public static double findMedianSortedArrays(int[] nums1, int[] nums2) {
if(nums1.length == 0 && nums2.length == 0)
return 0;
//我们为了让时间复杂度变成O(min(log(m), log(n))),选择用二分法遍历nums1和nums2中较短的那个
if(nums1.length > nums2.length){
int [] temp = nums1;
nums1 = nums2;
nums2 = temp;
}
int len1 = nums1.length, len2 = nums2.length; //获取两个数组新的长度
boolean even = ((len1 + len2) % 2 == 0); //记录两个数组的长度和是否是偶数,当是偶数的时候,就需要取两数的平均值。
int start = 0; //指向最左边的切割位置
int end = len1; //指向最右边的切割位置
int cut1 = 0; //代表nums1中分割线左边有cut1个元素
int cut2 = 0; //代表nums2中分割线左边有cut2个元素
int half = (len1 + len2) / 2; //代表两个分割线左侧元素的个数和。
//当len1+len2为奇数的时候,右边的数量多一个,所以最终返回右边两侧中较小的那个数就可以
//使用二分查找,在start 到 end 之间找到cut1的值。很明显,cut2的值就是: half - cut1
while(start <= end) {
cut1 = start + (end - start) / 2;
cut2 = half - cut1;
int L1 = (cut1 == 0) ? Integer.MIN_VALUE : nums1[cut1 - 1];//[...L1](cut1)[R1...]
int L2 = (cut2 == 0) ? Integer.MIN_VALUE : nums2[cut2 - 1];//[...L2](cut2)[R2...]
int R1 = (cut1 == len1) ? Integer.MAX_VALUE : nums1[cut1];
int R2 = (cut2 == len2) ? Integer.MAX_VALUE : nums2[cut2];
//因为有相等的情况,所以先排除需要移动end 或者start的情况
if (L1 > R2)
end = cut1 - 1;
else if(L2 > R1)
start = cut1 + 1;
else {
if (even)
return (double) (Math.max(L1, L2) + Math.min(R1, R2)) / 2;
else
return Math.min(R1, R2);
}
}
return 0;
}
}//class end