题目链接:力扣
题解:
- 因为题目要求时间复杂度为O(log(m+n)),就需要使用一种折半的方式进行查找,因此可以使用二分查找来求中位数,根据中位数的定义:当数组长度为偶数时,中位数为第有两个,也就是第和第个(数组长度为n,下标从0开始的第i个),当数组长度为奇数时,中位数有一个,也就是第个(数组长度为n,下标从0开始的第i个)。中位数就是数组最中间的一个(数组长度为奇数)或两个数(数组长度为偶数)
- 在一个数组中,求中位数时,可以对数组进行划分,将数组分成左半部分L[]和右半部分R[],左右两部分的元素个数相等(数组长度为偶数)或者相差1(数组长度为奇数)
- 当数组长度为偶数时:L和R的长度相等,中位数有两个,因为数组有序,所以L中的最后一个元素和R中的第一个元素就是所求的中位数。
- 当数组长度为奇数时:规定将中位数划分到L中(也可以划分到R中,后续分析将中位数划分到L中),此时L中的元素个数比R中的元素个数多一个。
- 在两个有序数组中,也可以进行划分:
- 将nums1划分成左半部分L1[]和右半部分R1[],将nums2划分成左半部分L2[]和R2[],L1,R1,L2,R2中元素个数不确定。每一个都可能为空。解题的关键也就是找到nums1的一个划分和nums2的一个划分。
- 可以将两个数组看成一个新数组,新数组的左半部分newL由L1和L2组成,右半部分newR由R1和R2组成。对于新数组来说newL中的元素个数与newR中的元素个数相等(两个数组长度之和为偶数),或者newL中的元素个数比newR中的元素个数多1个(两个数组长度为奇数,将中位数划分到newL中),如果newL中所有元素小于等于newR中的所有元素,那么:
- 两个有序数组长度之和为奇数时,中位数为newL中的最大值。
- 两个有序数组长度之和为偶数时,其中一个中位数为newL中的最大值,另一个中位数为newR中的最小值
- 如果能够找到步骤3描述的划分方式,就可以求得中位数。这道题的求解关键为:
- 对num1和num2分别划分为左右两部分
- newL中所有元素小于等于newR中的所有元素。
- newL中的元素个数与newR中的元素个数相等(两个数组长度之和为偶数),或者newL中的元素个数比newR中的元素个数多1个(两个数组长度为奇数,将中位数划分到newR中)
- newL中所有元素小于等于newR中的所有元素,如何保证这一个条件?
- newL由L1和L2组成,newR由R1和R2组成。
- 对于同一个有序数组有L1<=R1,L2<=R2
- 所以只需要保证L1<R2&&L2<R1即可,即L1的最大值小于等于R2的最小值,L2的最大值小于等于R1的最小值。对应的代码实现为:
num1[mid1-1]<=num2[mid2]&&num2[mid2-1]<=num1[mid1]
- 对num1和num2分别划分两部分:num1和num2两个数组只需要划分一个就可以了,另一个数组的划分位置就会自动确定,原因如下:
- 假设num1的长度为m,num2的长度为n
- 当m+n为偶数时,newL的长度totalLeft=,左右两部分元素个数相等
- 当m+n为奇数时,newL的长度totalLeft=,左边元素个数比右边多一个
- 又因为整数除法是向下取整,所以当m+n为偶数时totalLeft=。这样就可以将m+n为偶数或者为奇数时计算newL的长度等同处理,即totalLeft=
- 假设对num1进行划分后L1的长度为mid1,那么num2划分后的左边部分L2的长度为mid2=totalLeft-mid1(因为newL由L1和L2组成)。而totalLeft是一个确定的值,求得mid1,就可以得到mid2,可见,只需要找到其中一个划分方式,另一个划分可以通过计算得到。因此:
- num1的左半部分L1为nums1[0,1,...,mid1-1],右半部分R1为num1[mid1,...,m-1]
- num2的左半部分L2为nums2[0,1,...,mid2-1],右半部分R2的为num2[mid2,...,n-1]
- newL由L1和L2组成,newR由R1和R2组成。mid1的值既是R1中的最小值的下标,又是L1中的元素个数,mid2也是如此
- 当m+n为奇数时,中位数为newL中的最大值,最大值在num1[mid1-1]和num2[mid2-1]中产生(因为每个数组内部有序)
- 当m+n为偶数时,其中一个中位数为newL中的最大值,另一个中位数为newR中的最小值,最小值从num1[mid1]和num2[mid2]中产生
- 假设num1的长度为m,num2的长度为n
- 经过以上分析,我们只需要找到一种划分方式,假设找到nums1的划分mid1(这里nums1指向两个有序数组中长度较小的那一个,如果nums1的长度较大,就交换nums1和nums2,这样缩短二分查找的范围,以及减少临界情况),mid1将nums1划分成左右两部分L1和R1,满足条件:num1[mid1-1]<=num2[mid2]&&num2[mid2-1]<=num1[mid1]即可,这个时候,就可以保证newL中的所有元素小于等于newR中的所有元素。剩下的问题是如何求得mid1的值(mid2的值可以由newL的长度)。因为nums1是有序的,所有可以通过二分查找来得到mid1,这个时候因为mid2是通过totalLeft-mid1计算得到,所以当求得正确的mid1时,newL的长度一定等于newR的长度(当m+n为偶数),或者newL的长度比newR的长度多1。在通过二分查找确定mid1时,mid2的值也会自动更新,以保证newL的长度满足条件。当满足条件num1[mid1-1]<=num2[mid2]&&num2[mid2-1]<=num1[mid1]时,更新结束,当不满足时,就继续查找,也就是num1[mid1-1]>num2 || num2[mid2-1] > num1[mid1]时,更新查找范围,所以可以使用其中一个不满足的条件进行更新范围,以下分析使用条件num2[mid2-1] > num1[mid1]进行更新区间:初始值left=0,right=m,totalLeft=(m+n+1)/2
-
需要在区间[0,m]左闭右闭区间,找到划分mid1,所以循环的条件是while(left<right):循环终止时,left==right,(虽然是左闭右闭,循环条件使用的是left<right而不是left<=right,原因是当mid等于right时,在循环内部更新会出现索引越界,所以使用<而不是<=,这样只有在循环结束是才会出现mid1=m)。这时有两种特殊情况:1、left=right=m,nums1全部被划分到newL中。2、left=right=0,nums1全部被划分到newR中(因为num1是长度较短的数组)。注意循环条件是left<right。而在求mid的时候是向下取整,所以mid1在循环体内永远取不到right,也就是说在循环体内mid1<m,那么mid2=totalLeft-mid1=-mid1。因为n>=m(nums1是较短的数组),所以mid2=-mid1>=-m>=1。所以mid2的最小值只能为1(这时,nums2全部被划分到newR中),条件num2[mid2-1]中不会出现索引越界异常。
循环体内更新:- mid1=,mid2=totalLeft-mid1。
- 当num2[mid2-1] > num1[mid1]成立时,L2中的最大值大于R1中的最小值,nums1的划分太靠右了,当前mid1不合适,需要在右半部分区间继续寻找mid1,更新left=mid+1,(最终left的值就是要找正确的划分mid1)解释如下:
- 最终区间大小为两个元素[a,b],left指向a,right指向b,mid1是向下取整的,会指向a,当num2[mid2-1] > num1[mid1],a不是我们要找的的划分,那么可能正确的划分是b,所以left=mid1+1,left也就指向了一个可能正确的划分b,如何b是一个正确的划分,那么left就是答案。如果b也不是一个正确的划分,那么再下一次更新区间时,left就是指向继续下一个可能正确的划分,如果一直不满足,left=right,会一直更新到nums1数组的末尾,显然,这个时候num1全部被划分到newL中(极限情况),此时也是要找的答案,由于left=right循环结束。所以最终left的值就是一个正确的划分mid1
- 否则,说明当前是mid1一个可能的划分,至少对于num2[mid2-1]<=num1[mid1]成立,只需要在满足num1[mid1-1]<=num2[mid2]找到了一个成功的划分。
- 如果不满足num1[mid1-1]<=num2[mid2],即,L1中的最大值大于R2中的最小值,说明nums1的划分太靠右了。当前mid1不合适,需要在左半部分继续寻找mid1,更新right,这里因为while循环中的条件是<,并且取mid1是时是向下取值,在循环体内mid1永远取不到right,所以这里更新right=mid(而不是right=mid-1,这样更新的话mid-1就取不到了,而实际上mid-1可能是一个正确的划分)
- 如果满足num1[mid1-1]<=num2[mid2],说明这个时候已经找到了正确的划分,我们也可以更新right=mid,因为left的值才是可能正确的划分,更新right并不印象最终结果
- 将right更新为right=mid,可以将上述两种情况一起讨论。
-
- 循环结束时,left的结果就是要找的一种划分:
- mid1=left,mid2=totalLeft-mid2,也就找到了num1和num2的划分。
- 由步骤6可知,中位数仅与num1[mid1-1],num2[mid2-1],num1[mid1],num2[mid2]四个值相关。
- 当m+n为奇数时,中位数为newL中的最大值,最大值在num1[mid1-1]和num2[mid2-1]中产生(因为每个数组内部有序)。令nums1LeftMax=num1[mid1-1],nums2LeftMax=num2[mid2-1],那么中位数就是
Math.max(num1LeftMax,num2LeftMax)
- 当m+n为偶数时,其中一个中位数为newL中的最大值,另一个中位数为newR中的最小值,最小值从num1[mid1]和num2[mid2]中产生,令nums1RightMin=num1[mid1],nums2RightMin=num2[mid2],那么中位数为
(double) (Math.max(num1LeftMax,num2LeftMax)+Math.min(num1RightMin,num2RightMin))/2;
- 当m+n为奇数时,中位数为newL中的最大值,最大值在num1[mid1-1]和num2[mid2-1]中产生(因为每个数组内部有序)。令nums1LeftMax=num1[mid1-1],nums2LeftMax=num2[mid2-1],那么中位数就是
因为只在最短的数组中进行了划分,所以算法的时间复杂度为O(log(min(m,n))),比题目要求的O(log(m+n))还要低
-
AC代码
public double findMedianSortedArrays(int[] nums1, int[] nums2) { if (nums1.length > nums2.length) { int[] temp = nums1; nums1 = nums2; nums2 = temp; } int m = nums1.length; int n = nums2.length; int totalLeft = (m + n + 1) / 2; int left = 0; int right = m; while (left < right) { int mid1 = (left + right) / 2; int mid2 = totalLeft - mid1; //num1[mid1-1]<=num2[mid2]&&num2[mid2-1]<=num1[mid1] if (nums2[mid2 - 1] > nums1[mid1]) { left = mid1 + 1; } else { right = mid1; } } int mid1 = left; int mid2 = totalLeft - mid1; int num1LeftMax = mid1 == 0 ? Integer.MIN_VALUE : nums1[mid1 - 1]; int num1RightMin = mid1 == m ? Integer.MAX_VALUE : nums1[mid1]; int num2LeftMax = mid2 == 0 ? Integer.MIN_VALUE : nums2[mid2 - 1]; int num2RightMin = mid2 == n ? Integer.MAX_VALUE : nums2[mid2]; if ((m+n)%2==0){ return (double) (Math.max(num1LeftMax,num2LeftMax)+Math.min(num1RightMin,num2RightMin))/2; }else { return Math.max(num1LeftMax,num2LeftMax); } }