题目
给定两个大小分别为 m 和 n 的正序数组 nums1 和 nums2,返回两个数组合并后的中位数。
题目要求时间复杂度为 O(log(m+n))。
例如:
nums1 = [1, 3]
nums2 = [2]
合并后:[1, 2, 3]
中位数:2
初始思路为什么不满足要求
把两个数组拼接起来,再排序,时间复杂度至少是 O((m+n)log(m+n))。
即使使用双指针在线性时间内完成合并,也需要 O(m+n),仍然没有达到题目要求的对数复杂度。
因此,关键是不要真正构造合并后的数组,而是直接确定中位数左右两侧的边界。
核心观察:寻找两个分割点
把两个数组分别切成左右两部分:
nums1: [左半边] | [右半边]
nums2: [左半边] | [右半边]
设 i 表示从 nums1 中取多少个元素放入左半边,j 表示从 nums2 中取多少个元素放入左半边。
如果总长度为 m+n,令:
half = (m + n + 1) / 2
为了保证左半边元素数量正确,需要满足:
i + j = half
j = half - i
这里的 i、j 是元素数量,不是最后一个元素的下标。
例如:
nums1 = [1, 4, 7, 10]
nums2 = [2, 3, 5, 6, 8, 9]
half = 5
如果 i=2,那么 j=3:
nums1: [1, 4] | [7, 10]
nums2: [2, 3, 5] | [6, 8, 9]
左半边一共有 2+3=5 个元素。
有效分割点的条件
两个数组各自有序,所以只需要检查分割点附近的四个边界值:
left1 = nums1[i - 1] // nums1 左半边最大值
right1 = nums1[i] // nums1 右半边最小值
left2 = nums2[j - 1] // nums2 左半边最大值
right2 = nums2[j] // nums2 右半边最小值
有效分割必须满足:
left1 <= right2
left2 <= right1
这表示左半边的所有元素都不大于右半边的所有元素。
当分割点位于数组两端时,用无穷边界处理:
i == 0:left1 = 负无穷
i == m:right1 = 正无穷
j == 0:left2 = 负无穷
j == n:right2 = 正无穷
为什么只二分较短的数组
先保证 nums1 是较短数组:
if (nums1.length > nums2.length) {
交换 nums1 和 nums2;
}
这样二分范围只有 [0,m],并且根据 j=half-i 推出的 j 一定落在 nums2 的合理范围内,同时可以避免较短数组的分割位置越界。
开区间二分
我们寻找第一个满足下面条件的 i:
nums1[i] >= nums2[j - 1]
使用开区间哨兵:
int left = -1;
int right = m;
其中 left 表示条件不满足的位置,right 表示条件满足的位置或右侧哨兵。每轮计算:
int i = left + (right - left) / 2;
int j = half - i;
然后根据 nums1[i] 和 nums2[j-1] 的关系更新边界:
- 如果
nums1[i] < nums2[j-1],说明nums1左半边取少了,i应该增大:left = i。 - 否则当前
i可能已经足够,继续向左寻找第一个满足条件的位置:right = i。
循环结束时,right 就是最终的 i。
手动模拟
仍以:
nums1 = [1, 4, 7, 10]
nums2 = [2, 3, 5, 6, 8, 9]
half = 5
为例:
left = -1, right = 4
i = 1, j = 4
nums1[i] = 4 < nums2[j-1] = 6
说明 nums1 左边取少了:left = 1
下一轮:
left = 1, right = 4
i = 2, j = 3
nums1[i] = 7 >= nums2[j-1] = 5
说明 i 可能已经足够:right = 2
此时 left + 1 == right,二分结束,最终分割为:
nums1: [1, 4] | [7, 10]
nums2: [2, 3, 5] | [6, 8, 9]
左半边最大值为 max(4,5)=5,右半边最小值为 min(7,6)=6,所以中位数为:
(5 + 6) / 2 = 5.5
代码实现
class Solution {
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 half = (m + n + 1) / 2;
int left = -1;
int right = m;
while (left + 1 < right) {
int i = left + (right - left) / 2;
int j = half - i;
if (nums1[i] < nums2[j - 1]) {
left = i;
} else {
right = i;
}
}
int i = right;
int j = half - i;
int left1 = i == 0 ? Integer.MIN_VALUE : nums1[i - 1];
int right1 = i == m ? Integer.MAX_VALUE : nums1[i];
int left2 = j == 0 ? Integer.MIN_VALUE : nums2[j - 1];
int right2 = j == n ? Integer.MAX_VALUE : nums2[j];
int maxLeft = Math.max(left1, left2);
if ((m + n) % 2 == 1) {
return maxLeft;
}
int minRight = Math.min(right1, right2);
return ((double) maxLeft + minRight) / 2.0;
}
}
复杂度分析
只在较短数组上进行二分,搜索范围为 m,所以时间复杂度为:
O(log(min(m,n)))
它满足题目要求的 O(log(m+n))。算法只使用常量级变量,空间复杂度为:
O(1)
常见错误
- 奇数长度时,中位数应为左半边最大值
max(left1,left2),不能误用右半边的right1。 right1必须来自nums1[i],left2必须来自nums2[j-1]。- 计算偶数长度中位数时,先转为
double,避免两个int相加溢出。
总结
这道题的关键不是合并数组,而是寻找一个分割,使得:
i + j = half
left1 <= right2
left2 <= right1
只要左半边和右半边的数量、顺序都正确,就可以直接通过四个边界值得到中位数。