@author stormma
@date 2017/11/05
生命不息,奋斗不止!
** 题目
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
思路1(翻译自Leetcode大神分析)
要解决这个问题,我们需要理解什么是中位数,在统计学中,中位数用于:
切分一个集合为两个长度相等的子集合,并且第一个子集合总是小于第二个子集合。
如果我们理解了这个概念,那么我们就很接近答案了(在我看来并不是)。首先,让我们用一个随机的位置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
注: 当i = 0, left_A是空,当i = m right_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
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]
如果我们可以确保:
- len(left_part) == len(right_part)
- max(left_part) <= min(right_part)
然后我们把A+B中所有的元素切分成两个相同的长度的子集合, 并且左集合所有的元素不大于右集合的所有元素,其次我们可以得到:
中位数 = (max(left_part) + min(right_part)) / 2
要确保上面的两种情况,我们只需确保:
- i + j == m - i + n - j (or: m - i + n - j + 1)
如果n >= m,我们需要取i = 0 ~ m, j = (m + n + 1) / 2 - i- B[j - 1] <= A[i] 并且 A[i - 1] <= B[j]
注:
1. 为了简便起见,我们现在假设A[i - 1], A[i], B[j - 1], B[j]都是有效的,后面我们再处理边界条件
2. 为什么n >= m ? 因为我们必须确保j是非负的,0 <= i <= m 并且j = (m + n + 1) / 2 - i, 如果n < m, 那么j必定是负数,这并不能让我们得到正确的答案。
所以,总之,我们只需做:
在[0, m]范围寻找一个i的值满足
B[j - 1] <= A[i] 并且 A[i - 1] <= B[j], j = (m + n + 1) / 2 - i
我们可以用二分查找法来模拟这个过程:
1. 初始化 iMin = 0, iMax = m, 然后在[iMin, iMax]这个范围查找
2. 设 i = (iMin + iMax) / 2,j = (m + n + 1) / 2 -i
3. 现在,我们得到abs (len(left_part) - len(right_part)) <= 1(此处是我修改的,作者原表达式为len(left_part) == len(right_part))。然后,我们会遇到以下三种情况:
- B[j - 1] <= A[i] 并且 A[i - 1] <= B[j],这种情况表明我们得到了我们想要的答案
- B[j - 1] > A[i]这种情况表明,我们A[i]的值过小了,我们应该调整i的值得到B[j - 1] <= A[i],我们怎么调整i?
- 增加i?,是的,这种方式很正确,因为增加i,j肯定是减小的,所以B[j - 1]减小, A[i]增加,所以肯定能找到我们需要的B[j - 1] <= A[i]
- 减少i? 错啦老铁,i 减少,j增加,所以B[j - 1]继续增加,A[i]继续减少,那么一辈子也看不到B[j - 1] <= A[i]。
所以我们必须增加i,我们可以调整范围为[i + 1, iMax],所以设置iMin = i + 1并重复2.
- A[i - 1] >= B[j]: 这个情况表明A[i - 1]过大,我们应该减小i的值,保证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, i = m, j = 0, j = n,当A[i - 1], B[j - 1], A[i], B[j]不存在时,实际上,这种情况要比你想象得更简单
。
我们应该怎么做来保证max(left_part) <= min(right_part)。如果i 和 j 都不是边界条件时候,那么表明A[i - 1], A[i], B[j - 1], B[j]都存在,然后我们必须检查B[j - 1] <= A[i]并且A[i - 1] <= B[j]。但是如果A[i - 1], A[i], B[j - 1], B[j]不存在,我们不需要检查这两个情况或其中任意一个。举个例子,i = 0, A[i - 1]不存在,我们不需要检查A[i - 1] <= B[j],所以我们应该做以下事情:
在[0, m]查找一个i的值,满足
(j == 0 or i == m or B[j - 1] <= A[i]) 并且
(i = 0 or j == n or A[i - 1] <= B[j]),j = (m + n + 1) / 2 - i
在循环中,我们将会遇到以下三种情形:
- (j == 0 or i == m or B[j -1] <= A[i]) and
(i == 0 or j == n or A[i - 1] <= B[j])
这个情况表明我们找到了合适的i- i < m and B[j - 1] > A[i]
表明i的值太小,我们应该增加i的值,通过修改查找范围的下限- j < n and A[i - 1] > B[j]
表明i的值过大,我们应该减小i,通过修改查找范围的上限
补充:
i < m ⇒ j > 0
m <= n, i < m => j = (m + n + 1) / 2 -i > (m + n + 1) / 2 -m >=(2m + 1) / 2 - m >= 0
i > 0 => j < n
m <= n, i > 0 => j = (m + n + 1) / 2 - i < (m + n + 1) / 2 < (2n + 1) / 2 <= n
代码实现
时间复杂度 log(min(m, n))
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
if (nums1.length > nums2.length) {
int[] temp = nums1; nums1 = nums2; nums2 = temp;
}
int m = nums1.length, n = nums2.length;
int iMin = 0, iMax = m, halfLen = (m + n + 1) >> 1;
while (iMin <= iMax) {
int i = (iMin + iMax) >> 1;
int j = halfLen - i;
if (i > iMin && nums1[i - 1] > nums2[j]) { // i is too big
iMax = i - 1;
} else if (i < iMax && nums2[j - 1] > nums1[i]) { // i is too small
iMin = i + 1;
} else { // find correct i
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 (minRight + maxLeft) / 2.0;
}
}
return 0.0;
}
思路2(递归方式)
其实问题可以转换为在a,b数组中寻找第k大数,那么我们可以先比较a[k / 2 - 1]和b[k / 2 - 1],那么可以得到以下三种情况:
- a[k / 2 - 1] < b [k / 2 - 1],这个表明,a数组的k / 2个元素小于ab合并之后的k小元素,为啥呢,因为a[k / 2 - 1] < b k / 2 - 1
- a[k / 2 - 1] > b [k / 2 - 1]同理b数组的k / 2个元素小于ab合并之后的k小元素,所以答案不可能出现在这。
- a[k / 2 - 1] = b [k / 2 - 1],此时的这个数就是第k小元素
那么我们来看一下边界条件
如果A或者B为空,则直接返回B[k - 1]或者A[k - 1];
如果k为1,我们只需要返回A[0]和B[0]中的较小值;
如果A[k / 2 - 1] = B[k / 2 - 1],返回其中一个;
代码实现
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length, n = nums2.length;
// 1 2 3 4
if ((m + n) % 2 == 0) {
return (findKth(nums1, 0, nums2, 0, (m + n) / 2) + findKth(nums1, 0, nums2, 0,
(m + n) / 2 + 1)) / 2.0;
} else {
return findKth(nums1, 0, nums2, 0, (m + n + 1) / 2);
}
}
/**
* 寻找合并数组中第k大元素
* @param a
* @param aStart
* @param b
* @param bStart
* @param k
* @return
*/
private int findKth(int[] a, int aStart, int[] b, int bStart, int k) {
if (aStart >= a.length) {
return b[bStart + k - 1];
}
if (bStart >= b.length) {
return a[aStart + k - 1];
}
if (k == 1) {
return Math.min(a[aStart], b[bStart]);
}
int aMid = Integer.MAX_VALUE, bMid = Integer.MAX_VALUE;
if ((aStart + k / 2 - 1) < a.length) {
aMid = a[aStart + k / 2 - 1];
}
if ((bStart + k / 2 - 1 ) < b.length) {
bMid = b[bStart + k / 2 - 1];
}
// a前k / 2个数大于b 前k / 2个元素,
if (aMid > bMid) {
return findKth(a, aStart, b, bStart + k / 2, k - k / 2);
} else {
return findKth(a, aStart + k / 2, b, bStart, k - k / 2);
}
}