遇到一个比较复杂的算法题,记录一下,内容如下。
1.问题
假设有两个有序数组nums1和nums2,它们的长度分别为n和m。请找出这两个有序数组组成的序列中的中位数,并且整体的时间复杂度不大于log(m+n)。你可以假设这两个数组都不为空。
例子
nums1 = [1, 3]
nums2 = [2]
The median is 2.0
复制代码
2.分析
解决这个问题可以采用递归的方法,而寻找中位数的问题,可以理解为:将一个集合划分为两个等长的子集,其中一个子集的任意元素值永远小于等于另一个子集的任意元素值。
首先,我们可以通过随机位置i将数组A划分为两部分(可以将i理解为数组元素之间的间隔位置,间隔i左边有i个元素,右边有m-i个元素):
left_A | right_A
A[0], A[1], ..., A[i-1] | A[i], A[i+1], ..., A[m-1]
复制代码
因为A有m个元素,因此共有m+1种切分方式。我们可以得出:len(left_A)=i,len(right_A)=m-i。当i=0时(第一个位置为0),left_A为空,当i=m时,right_A为空。(注:len(left_A)代表left_A部分的元素个数,其他的同理)
同理,我们也可以通过随机位置j将数组B划分为两部分:
left_B | right_B
B[0], B[1], ..., B[j-1] | B[j], B[j+1], ..., B[n-1]
复制代码
因此,我们可以将left_A和left_B合并为一个集合,将right_A和right_B合并为一个集合,分别命名为left_part和right_part:
left_part | right_part
A[0], A[1], ..., A[i-1] | A[i], A[i+1], ..., A[m-1]
B[0], B[1], ..., B[j-1] | B[j], B[j+1], ..., B[n-1]
复制代码
如果我们能够确保(注:max(left_part)代表left_part集合的最大值,min(rigth_part)代表right_part集合的最小值):
1.len(left_part)=len(right_part);
2.max(left_part)<=min(rigth_part)
复制代码
那么我们就将{A,B}集合中的元素划分为具有相同长度的两个子集了,其中一个子集的元素永远大于等于另外一个子集的元素。那么 median(中位数)=(max(left_part)+min(right_part))/2。(以上的讨论基于m,n都是偶数,但不失一般性)
因此为了保证以上的两个条件成立,我们可以保证(当m+n为奇数时,左边部分会比右边部分多一个元素):
在这里,我们假设:1.为了方便讨论,我们假设当i=0, i=m,j=0, j=n 时,A[i−1], B[j−1], A[i], B[j]的值都存在。我们在最后再讨论这些边界值的情况。
2.假设n≥m,因为我们必须确保j不为负数,当0≤i≤m,j=(m+n+1)/2-i。
因此我们需要做的就是,遍历i(i属于[0,m]),找到一个目标i使得:
我们可以通过以下的步骤进行二分查找:
1.令imin=0,imax=m,从[imin,imax]开始查找。
2.令i=(imin+imax)/2, j=(m+n+1)/2-i。
3.现在我们已经使得len(left_part)=len(right_part)。我们可能会遇到两种情况:
- B[j−1]≤A[i] 和A[i−1]≤B[j]这意味着我们找到了目标i因此查询结束。
- B[j−1]>A[i],这意味着A[i]太小了,因此我们需要增大i,使得B[j−1]≤A[i]。因为i增大了j就会减小,A[i]就会增大,B[j−1]就会减小。因此,我们将查询范围调整为[i+1,imax],返回第2步。
- A[i−1]>B[j]这意味着A[i−1]太大了,因此我们需要减小i,所以我们将查询范围调整为[imin,i-1],返回第2步。
当我们获得目标i时,我们可以得到中位数:
1.当m+n时奇数时,中位数为max(A[i−1],B[j−1])。
2.当m+n时偶数时,中位数为(max(A[i−1],B[j−1])+min(A[i],B[j]))/2
最后,我们来考虑一下边缘值。当i=0, i=m, j=0, j=n时,A[i−1], B[j−1], A[i], B[j]不存在。当i,j不是边缘值时,我们需要判断B[j−1]≤A[i] 和A[i−1]≤B[j]这两个条件,当i, j是边缘值时,这两个条件就不需要都进行判断了,比如,当i=0时,A[i-1]不存在,因此我们就不需要判断A[i−1]≤B[j]这个条件了。所以我们需要做的是,遍历i(i属于[0,m]),找到目标值i使得:
比如j=0 则 A[i−1]≤B[j]必然成立;i=m 则 A[i−1]≤B[j] 必然成立。因此,总得来说,当我们在循环时,只会碰到三种情况:
第一种情况:找到目标 i,当:
第三种情况:A[i−1]>B[j]这意味着A[i−1]太大了,因此我们需要减小i。
3.具体代码
class Solution {
public double findMedianSortedArrays(int[] A, int[] B) {
int m = A.length;
int n = B.length;
if (m > n) { // to ensure m<=n
int[] temp = A; A = B; B = temp;
int tmp = m; m = n; n = tmp;
}
int iMin = 0, iMax = m, halfLen = (m + n + 1) / 2;//i
while (iMin <= iMax) {//每次循环长度都变为原来的1/2
int i = (iMin + iMax) / 2;
int j = halfLen - i;
//调整i
if (i < iMax && B[j-1] > A[i]){
iMin = i + 1; // i is too small
}
else if (i > iMin && A[i-1] > B[j]) {
iMax = i - 1; // i is too big
}
else { // i is perfect
int maxLeft = 0;//得到左边部分的最大值
if (i == 0) { maxLeft = B[j-1]; }
else if (j == 0) { maxLeft = A[i-1]; }
else { maxLeft = Math.max(A[i-1], B[j-1]); }
//当两个数组总长度为奇数时,只需返回左边的最大值即可
if ( (m + n) % 2 == 1 ) { return maxLeft; }
int minRight = 0;//得到右边部分的最小值
if (i == m) { minRight = B[j]; }
else if (j == n) { minRight = A[i]; }
else { minRight = Math.min(B[j], A[i]); }
//当两个数组总长度为偶数时,只需返回左右两边最大值的平均值
return (maxLeft + minRight) / 2.0;
}
}
return 0.0;
}
}
复制代码
4.复杂度分析
1.时间复杂度:O(log(min(m,n)))。在最开始查找范围是[0,m],每次查询时查找的范围都会减少为原来的一半,因此我们需要log(m)次循环,而每次循环的时间都是常数,因此时间复杂度为:O(log(m))。因为m≤n,所以时间复杂度为O(log(min(m,n)))。
2.空间复杂度:O(1)。我们只需要固定的内存去存储9个本地变量,因此时间复杂度为O(1)。
5.其他:
我们可以将这两个有序数组进行归并,找到中位数,但这种方法的时间复杂度为O(n+m)。