题目描述
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)).
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
解题思路
我们首先想到的解法可能是,将这两个有序数组先整合为一个有序数组,因为两个数组原本就是有序的,我们可以使用归并排序合并两个数组,然后找到它的中位数,算法复杂度为:O(m+n)。但是要把时间复杂度降到O(log(m+n)),就需要对两个数组同时做二分查找,排除掉不可能出现中位数的区间,最后找到所求的中位数。
第一种方法
预备知识
为了解决这个问题,我们首先要知道什么是中位数?在统计中,中位数用于:
将一个集合划分为两个相等长度的子集,其中一个子集总是大于另一个子集。
首先我们先以数组A为例,随机的在位置i把数组A分割成两部分:
left_A | right_A
A[0], A[1], ..., A[i-1] | A[i], A[i+1], ..., A[m-1]
因为 A 有 m 个元素,所以总共有 m+1 种切割位置(i = 0 ~ m)
我们知道:
len(left_A) = i, len(right_A) = m - i
Note: 当 i = 0,left_A为空;当i = m, right_A为空
对数组B做同样的操作,在随机位置 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]
如果我们可以满足下面两个条件:
1. len(left_part) = len(right_part)
2. max(left_part) ≤ min(right_part)
那么,我们便将集合 A 和集合 B 中所有的元素分成了相同长度的两部分,并且其中一部分总是大于另一部分,所以:
为了确保上面两个条件,我们只需满足:
i+j=m−i+n−j i + j = m − i + n − j
如果 n≥m n ≥ m 我们仅需要使 i = 0 ~ m, j=m+n+12−i j = m + n + 1 2 − i
B[j−1]≤A[i] B [ j − 1 ] ≤ A [ i ] 和 A[i−1]≤B[j] A [ i − 1 ] ≤ B [ j ] ,j 和 i 关系为: j=m+n+12−i j = m + n + 1 2 − i
备注:
- 为了简单,我们假设 A[i−1],B[j−1],A[i],B[j] 都是有效的,即时 i = 0、i = m、 j = 0、j = n,后面会讲解对于如何处理这些边缘值。
- 因为我们要确保 j 为非负值 (j≥0 ( j ≥ 0 ), j=m+n+12−i j = m + n + 1 2 − i ,所以当 m≤n m ≤ n 可以保证 j≥0 j ≥ 0 。(其实就是让第一个数组A的长度要不能大于第二个数组B的长度)
所以我们需要做的是:
在[0,m]中搜索找到一个 i 使得满足下面条件:
算法流程
我们可以按照以下步骤进行二分法搜索:
- 设置两个变量imin = 0、imax = m,然后开始在[imin , imax]中搜索
- 设置变量 i=imin+imax2 i = i m i n + i m a x 2 , j=m+n+12−i j = m + n + 1 2 − i
现在我们已经有了长度相同的两部分:len(left_part) = len(right_part),而我们可能遇到的只有以下三种情况:
* B[j−1]≤A[i] B [ j − 1 ] ≤ A [ i ] 和 A[i−1]≤B[j] A [ i − 1 ] ≤ B [ j ]意味着我们已经找到了这个位置 i,这样就可以得到max(left_part)、min(right_part),所以我们就可以得到所求的中位数,就可以停止搜索。
B[j−1]>A[i] B [ j − 1 ] > A [ i ]
这表明A[i]太小了,我们必须调整i使满足 B[j−1]≤A[i] B [ j − 1 ] ≤ A [ i ]
那么我们如何调整 i,是增大 i 的值还是降低 i 的值?答案是增加i的值,因为我们知道数组是有序的,那么增加 i 的值,则根据 j=m+n+12−i j = m + n + 1 2 − i ,j 就会下降,则 i 变大,A[i]变大,B[j-1]变小。这样就有可能满足 B[j−1]≥A[i] B [ j − 1 ] ≥ A [ i ] 。
我们知道我么需要增加 i 的值使其满足条件,那么我们必须调整搜索范围为 [i+1, imax],所以使 imain = i+1,然后跳到第 2 步骤。
A[i−1]>B[j] A [ i − 1 ] > B [ j ]
这表示 A[i-1] 太大了,我们需要降低 i 的值,这样才能满足 A[i−1]≤B[j] A [ i − 1 ] ≤ B [ j ] 。所以我们需要调整搜索范围为: [imin, i-1],所以设置 imax = i-1。然后跳到第 2 步骤。
当我们按照以上三步骤,找到所需的 i 时,这个中位数就是:
* 当 m+n 的和为奇数时,中位数=max(A[i-1], B[j-1])
* 当 m+n 的和为偶数时,中位数=( max(A[i−1],B[j−1])+min(A[i],B[j]) )/ 2
边界值问题
现在我们考虑边界值问题:i=0、j=0、i=m、j=n时,A[i−1],B[j−1],A[i],B[j]可能不存在,其实这种情况更简单。
现在我们知道要确保 max(left_−part≤min(right−part) m a x ( l e f t _ − p a r t ≤ m i n ( r i g h t − p a r t ) ,所以当 i 和 j 不是上面的边界值时,意味着A[i−1],B[j−1],A[i],B[j]都存在,此时我们需要检查 B[j−1]≤A[i] B [ j − 1 ] ≤ A [ i ] 和 A[i−1]≤B[j] A [ i − 1 ] ≤ B [ j ] 。但是如果A[i−1],B[j−1],A[i],B[j]中有一些不存在的话,那么我们可能不需要检查 B[j−1]≤A[i] B [ j − 1 ] ≤ A [ i ] 和 A[i−1]≤B[j] A [ i − 1 ] ≤ B [ j ] 一个或者全部。
例如,i = 0时,A[i-1]是不存在的,那么我们不需要检查 (A[i−1]≤B[j] ( A [ i − 1 ] ≤ B [ j ] 。所以我们需要做的是:
- 在[0,m]中搜索找到一个 i ,使得:
- (j = 0 or i = m or B[j−1]≤A[i] B [ j − 1 ] ≤ A [ i ] ) and (i = 0 or j = n or A[i−1]≤B[j] A [ i − 1 ] ≤ B [ j ] ), j=m+n+12−i j = m + n + 1 2 − i
算法总结
在搜索循环中,我们只会遇到三种情况:
- (j = 0 or i = m or B[j−1]≤A[i] B [ j − 1 ] ≤ A [ i ] ) and (i = 0 or j = n or A[i−1]≤B[j] A [ i − 1 ] ≤ B [ j ] )。这种情况意味着我们找到的 i 是正好的,此时停止搜索。
- j > 0 and i < m and B[j−1] > A[i]。意味着 i 太小,我们需要增大它
- i > 0 and j < n and A[i−1] > B[j]。意味着 i 太大,我们需要降低它
注:因为 i < m ⟹ j > 0; i > 0 ⟹ j < n。所以2、3两种情况可以变为:
2. i < m and B[j−1] > A[i]。意味着 i 太小,我们需要增大它
3. i > 0 and A[i−1] > B[j]。意味着 i 太大,我们需要降低它
代码实现
public static double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
if (m > n) {
return findMedianSortedArrays(nums2, nums1);
}
int iMin = 0, iMax = m, halfLen = (m + n + 1) / 2;
while (iMin <= iMax) {
int i = (iMin + iMax) / 2;
int j = halfLen - i;
if (i < iMax && nums2[j - 1] > nums1[i]) {
// the i is too small
iMin = i + 1;
} else if (i > 0 && nums1[i - 1] > nums2[j]) {
// the i is too big
iMax = i - 1;
} else {
// the result is perfect
int maxLeft = 0;
if (i == 0) {
maxLeft = nums2[j - 1];
} else if (j == 0) {
maxLeft = nums1[i - 1];
} else {
maxLeft = Math.max(nums1[i - 1], nums2[j - 1]);
}
if ((m + n) % 2 == 1) {
return maxLeft;
}
int minRight = 0;
if (i == m) {
minRight = nums2[j];
} else if (j == n) {
minRight = nums1[i];
} else {
minRight = Math.min(nums1[i], nums2[j]);
}
return (maxLeft + minRight) / 2.0;
}
}
return 0.0;
}
总结
此算法对我来说比较难理解,该也是LeetCode上难度系数为hard的题目,我这个也是根据LeetCode提供的英文版Solution改编而来,大部分一样,只不过我写成了中文版,当然其中也有我的一些理解。如果大家阅读时,感觉理解起来比较困难,可以先尝试多读几次,然后把代码debug一遍,仔细的查看每一个步骤。这里也附上LeetCode提供的原版Solution连接。
后面还会接着发布另外一个算法,也是二分法思想,只不过代码实现起来不同。
注:如果文章有任何不足或错误,请留言!看到后我会尽快回复,谢谢!