题目
给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。
请你找出这两个正序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。
你可以假设 nums1 和 nums2 不会同时为空。
题解
方法一、数组合并(归并)
在两个有序的数组中要找到某个数,估计大家第一想到的就是归并的方法。这也是归并排序的基本思想。开辟一个新的数组help,长度为两个数组的长度和。使用两个指针分别指向两个数组的开头,比较两个指针所指的元素,将比较小的元素放入到新的数组中。之后继续上述的操作,直到将所有的数都放入到help中。举一个简单的例子:将两个有序数组[1,3,5,6,7]
与[2,4,8]
合并为一个数组:
本题是要求找中位数,中位数就是两个数组长度和的一半的位置。但是这里要注意长度奇数偶数的情况:
如果总长度为奇数的话,那么合并后中间的那个数就是结果
如果总长度为偶数的话,那合并后中间两个数的平均数就是结果
这里由于只需要找中位数,所以数组归并到中位数的位置就可以了,可以声明一个变量k作为跳出条件。
归并的方法简单易懂,时间复杂度与空间复杂度均为线性复杂度O(m+n)
。并不符合题目要求。但是这种方法也是接下来方法的关键。
class Solution { public double findMedianSortedArrays(int[] nums1, int[] nums2) { int length = nums1.length + nums2.length; double result = 0; //分别进行奇数偶数处理 if(length % 2 != 0){ result = getNum(nums1,nums2,length/2); }else{ result = getNum(nums1,nums2,length/2-1)/2 + getNum(nums1,nums2,length/2)/2; } return result; } public double getNum(int[] nums1, int[] nums2, int k){ int[] result = new int[nums1.length+nums2.length]; int i = 0, j = 0; int cur = 0; while(i < nums1.length && j <nums2.length && cur <= k){ if(nums1[i] < nums2[j]) result[cur++] = nums1[i++]; else result[cur++] = nums2[j++]; } while(i < nums1.length && cur <=k) result[cur++] = nums1[i++]; while(j < nums2.length && cur <=k) result[cur++] = nums2[j++]; return result[cur-1]; }}
方法二、二分查找
基本问题分析
如何让时间复杂度降为O(log(m+n))
呢?一般涉及到log的时间复杂度,我们都会想到二分查找的方法。其实这道题也不例外。我们仔细观察下面的例子:
题目要求找到中位数,通过计算我们知道中位数应当是4,5两个数的平均值,因为合并后数组的总长为8,那么中位数就是位置4,5两处的数的均值。上图中蓝色的分割线将数组分为了两部分,现在我们只看左部分,左部分中数组A贡献了两个数(2,4),数组B贡献了两个数(1,3)。两个数组一共贡献了4个数。这四个数正好是中位数的位置。这也就说明了数组A和数组B两个数组贡献的元素个数的和应该为数组的一半。
但是这样还是不够的,比如说:
如图所示,上图的情况一与情况二都满足数组A和数组B两个数组贡献的元素个数的和应该为数组的一半(3+1)(0+4)。但是都不符合要求:
情况一所构成的序列为:
[1,2,4,8]
,但是中间少了3,而多了8情况二所构成的序列为:
[1,3,5,6]
,中间少了2,4,而多了5,6
将上述问题更一般化为:
如果不想出现上述的问题,我们需要保证L1
L2
。由于题目中给的数组是有序的,故L1必然小于R1,L2必然小于R2。这样才能构成一个正确的有序的序列,而不至于出现情况一与情况二的问题。
所以我们的序列必须满足以下条件:
A贡献的元素数量 + B贡献的元素数量 = 要求的元素数量(本题是求中位数,故要求的为总数量的一半)
L1 < R2
L2 < R1
整个问题基本就是这个样子,思路依旧来源于之前的方法一,只是将其细化进而转换思维方式。所以解任何题目,甚至是在科研学习当中,都不应该放弃最简单的方法,可能效率并不是特别高,但是却提供了一个能让我们继续思考下去的台阶。
如何快速的找到 L1 / R1 和 L2/ R2 的位置
这里就要用到二分查找了。由于A贡献的元素数量 + B贡献的元素数量 = 要求的元素数量的限制,只要找到了L1 / R1的位置,也就找到的了 L2/ R2 。为了书写方便,将在数组A中的位置表示为curA,在数组B中位置表示为curB,(本题中curA + curB = (A.length + b.length +1)/2)。
这样只需要在一个数组中进行二分查找了。我们选择长度比较短的数组作为查找数组:
//初始化二分查找的边界int L_edge = 0, R_edge = A.length;//数组A中的位置表示为curA,在数组B中位置表示为curBint curA,curB;double result = 0;while(L_edge <= R_edge){ curA = L_edge + (R_edge - L_edge)/2; curB = (length+1)/2 - curA; //计算出L1,R1,L2,R2 double L1 = curA == 0 ? Integer.MIN_VALUE:A[curA-1]; double R1 = curA == A.length ? Integer.MAX_VALUE:B[curA]; double L2 = curB == 0 ? Integer.MIN_VALUE:B[curB-1]; double R2 = curB == B.length ? Integer.MAX_VALUE:B[curB]; //二分查找,重新划定边界 if(L1 > R2) R_edge = curA-1; else if( L2 > R1) L_edge = curA+1; else{ //注意长度为奇数偶数的问题,奇数取中间的那个值,偶数则取两边的和的一半 if(length % 2 != 0) result = Math.max(L1,L2); else result = (Math.max(L1,L2) + Math.min(R1,R2))/2.0; break; }}
问题:为什么要选择较短的数组作为查找数组呢?
其实选择哪个数组作为查找数组都可以。选择短的那个能够使时间复杂度变为Olog(min(m,n))
。
代码
class Solution { public double findMedianSortedArrays(int[] nums1, int[] nums2) { int length = nums1.length + nums2.length; //选择长度较小的那个数组进行查找 if(nums1.length > nums2.length) return findMedianSortedArrays(nums2,nums1); if(nums1.length == 0){ if(nums2.length % 2 != 0) return nums2[length/2]; else return (nums2[length/2-1] + nums2[length/2])/2.0; } 初始化二分查找的边界 int L_edge = 0, R_edge = nums1.length; int cur1 = 0,cur2 = 0; double result = 0; while(L_edge <= R_edge){ cur1 = L_edge + (R_edge - L_edge)/2; cur2 = (length+1)/2 - cur1; //计算出L1,R1,L2,R2 double L1 = cur1 == 0 ? Integer.MIN_VALUE:nums1[cur1-1]; double R1 = cur1 == nums1.length ? Integer.MAX_VALUE:nums1[cur1]; double L2 = cur2 == 0 ? Integer.MIN_VALUE:nums2[cur2-1]; double R2 = cur2 == nums2.length ? Integer.MAX_VALUE:nums2[cur2]; //二分查找,重新划定边界 if(L1 > R2) R_edge = cur1-1; else if( L2 > R1) L_edge = cur1+1; else{ //注意长度为奇数偶数的问题,奇数取中间的那个值,偶数则取两边的和的一半 if(length % 2 != 0) result = Math.max(L1,L2); else result = (Math.max(L1,L2) + Math.min(R1,R2))/2.0; break; } } return result; }}