4. 寻找两个正序数组的中位数
大约 4 分钟
4. 寻找两个正序数组的中位数
📝 题目描述
给定两个大小分别为 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是较短数组(减少二分次数),若nums1更长则交换两个数组; - 定义二分查找的范围
[l, r](l=0,r=len(nums1)),表示nums1中划分到左半部分的元素个数; - 每次二分取
i = (l + r) // 2(nums1左半部分元素数),则j = (m + n + 1) // 2 - i(nums2左半部分元素数,保证左半部分总长度 ≥ 右半部分); - 比较边界值,调整二分范围:
- 若
nums1[i-1] > nums2[j]:nums1左半部分太大,r = i - 1; - 若
nums2[j-1] > nums1[i]:nums1左半部分太小,l = i + 1;
- 若
- 找到合法划分后,计算左半部分最大值
max_left和右半部分最小值min_right,根据总长度奇偶性返回中位数。
✅ 完整代码(Java)
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 读取第一个数组
int m = scanner.nextInt();
int[] nums1 = new int[m];
for (int i = 0; i < m; i++) {
nums1[i] = scanner.nextInt();
}
// 读取第二个数组
int n = scanner.nextInt();
int[] nums2 = new int[n];
for (int i = 0; i < n; i++) {
nums2[i] = scanner.nextInt();
}
// 创建解决方案对象并计算结果
Solution solution = new Solution();
double result = solution.findMedianSortedArrays(nums1, nums2);
// 输出结果,保留6位小数
System.out.printf("%.6f", result);
scanner.close();
}
}
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
// 保证nums1是较短数组,减少二分次数
if (m > n) {
return findMedianSortedArrays(nums2, nums1);
}
int l = 0, r = m;
// 左半部分总长度(≥右半部分)
int leftTotal = (m + n + 1) / 2;
while (l <= r) {
// nums1左半部分元素数
int i = (l + r) / 2;
// nums2左半部分元素数
int j = leftTotal - i;
// 调整二分范围:nums1左半部分太大
if (i > 0 && j < n && nums1[i-1] > nums2[j]) {
r = i - 1;
}
// 调整二分范围:nums1左半部分太小
else if (j > 0 && i < m && nums2[j-1] > nums1[i]) {
l = i + 1;
}
// 找到合法划分
else {
// 计算左半部分最大值
int maxLeft;
if (i == 0) {
maxLeft = j > 0 ? nums2[j-1] : Integer.MIN_VALUE;
} 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;
if (i == m) {
minRight = j < n ? nums2[j] : Integer.MAX_VALUE;
} else if (j == n) {
minRight = nums1[i];
} else {
minRight = Math.min(nums1[i], nums2[j]);
}
// 总长度为偶数,返回平均值
return (maxLeft + minRight) / 2.0;
}
}
// 题目保证数组非空,不会走到这一步
return 0.0;
}
}🧠 通俗易懂解释
可以把问题理解为“切蛋糕分两半,左边不超过右边”:
- 把两个数组想象成两块蛋糕,我们要切两刀(分别切在
nums1和nums2上),把所有蛋糕分成左右两堆; - 要求:左堆总长度 ≥ 右堆,且左堆里最大的那块 ≤ 右堆里最小的那块;
- 找到这样的切法后,若总长度是奇数,左堆最大的就是中位数;若偶数,左堆最大和右堆最小的平均值就是中位数。
以示例1 nums1=[1,3], nums2=[2] 为例:
- 保证
nums1较短(m=2 ≤ n=1?不,交换后nums1=[2], nums2=[1,3]); - 二分查找
nums1的划分:i=0时,j=(1+2+1)/2 - 0=2; - 检查边界:
nums1左半部分为空,nums2左半部分是[1,3],左堆最大值是3,但右堆只有nums1[0]=2,不合法; - 调整为
i=1,j=2-1=1; - 左堆是
nums1[0]=2和nums2[0]=1,最大值是2;右堆是nums2[1]=3,最小值是3;满足“左≤右”; - 总长度3是奇数,中位数就是左堆最大值2。
⏱ 时间复杂度
O(log(min(m,n)))
仅对较短数组进行二分查找,每次缩小一半范围,效率最优。
💾 空间复杂度
O(1)
仅使用几个变量存储边界值和二分范围,无额外空间消耗。
想象抽绳子,找到num_i1 <= num_j2 && num_j1 <= num_i2
