题目
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。
示例 1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
示例 2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
算法核心思想
两个有序数组 nums1(长度 m)和 nums2(长度 n),合并后总长度 total = m + n。中位数将合并数组分成两半:
- 左半部分 包含
half = (total + 1) / 2个元素(奇数时多一个,中位数就是左半部分的最大值) - 右半部分包含剩余元素
我们不去真正合并数组,而是对较短的数组做二分,找到一个分割位置 i(nums1 的分割点),从而推出 j = half - i(nums2 的分割点),使得:
左半部分所有元素 ≤ 右半部分所有元素
nums1: [ ... nums1Left ] | [ nums1Right ... ]
nums2: [ ... nums2Left ] | [ nums2Right ... ]
左半部分 右半部分
满足条件:max(nums1Left, nums2Left) ≤ min(nums1Right, nums2Right)
Java语言
java
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,n=nums2.length;
int total=m+n;
int half=(total+1)/2;
int left=0,right=m;
while(left<=right){
int i=(left+right)/2;
int j=half-i;
int nums1Left=i>0?nums1[i-1]:Integer.MIN_VALUE;
int nums1Right=i<m?nums1[i]:Integer.MAX_VALUE;
int nums2Left=j>0?nums2[j-1]:Integer.MIN_VALUE;
int nums2Right=j<n?nums2[j]:Integer.MAX_VALUE;
if(nums1Left <= nums2Right && nums2Left <= nums1Right){
if(total%2==1){
return(double)Math.max(nums1Left,nums2Left);
}
else {
return (Math.max(nums1Left, nums2Left) + Math.min(nums1Right, nums2Right)) / 2.0;
}
} else if (nums1Left > nums2Right) {
right = i - 1;
} else {
left = i + 1;
}
}
return 0.0;
}
}
逐行解释
1. 保证 nums1 是较短的数组
java
if(nums1.length > nums2.length){
int[] temp = nums1;
nums1 = nums2;
nums2 = temp;
}
为什么要交换? 我们只对 nums1 做二分搜索,搜索范围是 [0, m]。如果 nums1 更短,二分次数就是 O(log(min(m,n)))。同时,j = half - i 必须满足 0 ≤ j ≤ n,如果 nums1 太长,i 可能很大导致 j 变成负数,就越界了。
2. 初始化变量
java
int m = nums1.length, n = nums2.length;
int total = m + n;
int half = (total + 1) / 2;
total:合并后的总元素个数half:左半部分应包含的元素个数。用(total + 1) / 2而非total / 2,是为了奇数时左半部分多一个元素,中位数直接取左半最大值即可,逻辑更统一
| total | half | 说明 |
|---|---|---|
| 7(奇) | 4 | 左半 4 个,右半 3 个,中位数 = 左半最大值 |
| 8(偶) | 4 | 左半 4 个,右半 4 个,中位数 = (左半最大 + 右半最小) / 2 |
3. 二分搜索
java
int left = 0, right = m;
在 nums1 的分割位置上做二分。i 的范围是 [0, m]:
i = 0:nums1 的元素全在右半部分i = m:nums1 的元素全在左半部分
java
while(left <= right){
标准二分循环,left 超过 right 时说明搜索空间耗尽(正常情况下一定会提前 return,不会走到这里)。
4. 计算分割点
java
int i = (left + right) / 2;
int j = half - i;
-
i:nums1 中有i个元素划入左半部分 -
j:nums2 中有j个元素划入左半部分 -
两者之和必须等于
half,所以j = half - inums1: [0, 1, ..., i-1] | [i, i+1, ..., m-1]
nums2: [0, 1, ..., j-1] | [j, j+1, ..., n-1]
←--- left ---→ ←--- right ---→
5. 取出分割线两侧的四个关键元素
java
int nums1Left = i > 0 ? nums1[i-1] : Integer.MIN_VALUE;
int nums1Right = i < m ? nums1[i] : Integer.MAX_VALUE;
int nums2Left = j > 0 ? nums2[j-1] : Integer.MIN_VALUE;
int nums2Right = j < n ? nums2[j] : Integer.MAX_VALUE;
| 变量 | 含义 | 边界处理 |
|---|---|---|
nums1Left |
nums1 左半部分的最后一个元素 | i=0 时左半没有元素,设为 MIN_VALUE(保证不干扰比较) |
nums1Right |
nums1 右半部分的第一个元素 | i=m 时右半没有元素,设为 MAX_VALUE |
nums2Left |
nums2 左半部分的最后一个元素 | j=0 时设为 MIN_VALUE |
nums2Right |
nums2 右半部分的第一个元素 | j=n 时设为 MAX_VALUE |
用 MIN_VALUE / MAX_VALUE 的原因:当某个数组全部被分到一侧时,对应的"边界元素"不存在,用极值确保后续比较逻辑不出错。
6. 判断是否找到正确的分割位置
java
if(nums1Left <= nums2Right && nums2Left <= nums1Right){
这是核心判断条件。正确的分割要求:
nums1 的左半最大 ≤ nums2 的右半最小
nums2 的左半最大 ≤ nums1 的右半最小
等价于:左半部分所有元素 ≤ 右半部分所有元素。
找到了,计算中位数:
java
if(total % 2 == 1){
return (double) Math.max(nums1Left, nums2Left);
}
总长度为奇数:中位数 = 左半部分的最大值(左半比右半多一个元素)。
java
else {
return (Math.max(nums1Left, nums2Left) + Math.min(nums1Right, nums2Right)) / 2.0;
}
总长度为偶数 :中位数 = (左半最大值 + 右半最小值) / 2。/ 2.0 确保浮点除法。
没找到,调整二分边界:
java
} else if (nums1Left > nums2Right) {
right = i - 1;
nums1Left > nums2Right:nums1 划进左半的元素太大了,说明 i 偏大,需要左移 分割点,缩小 right。
java
} else {
left = i + 1;
}
否则是 nums2Left > nums1Right:nums2 划进左半的元素太大了,说明 j 偏大即 i 偏小,需要右移 分割点,增大 left。
7. 兜底返回
java
return 0.0;
理论上不会执行到这一行。只要输入合法,循环内一定会 return。这是 Java 编译要求的语法兜底。
图解示例
nums1 = [1, 3] m = 2
nums2 = [2, 4, 5] n = 3
total = 5(奇数), half = 3
二分过程:
| 轮次 | left | right | i | j | nums1Left | nums1Right | nums2Left | nums2Right | 判断 |
|---|---|---|---|---|---|---|---|---|---|
| 1 | 0 | 2 | 1 | 2 | 1 | 3 | 4 | 5 | nums2Left(4) > nums1Right(3) → left=2 |
| 2 | 2 | 2 | 2 | 1 | 3 | MAX | 2 | 4 | 3≤4 且 2≤MAX → 找到 |
奇数,返回 max(3, 2) = 3.0,即中位数 3。合并后 [1,2,3,4,5],中位数确实是 3。