Article / 文章
LeetCode 第4题:寻找两个正序数组的中位数
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的中位数。 算法的时间复杂度应该为 O(log (m+n))。
题目描述
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的中位数。
算法的时间复杂度应该为 O(log (m+n))。
难度
困难
题目链接
https://leetcode.cn/problems/median-of-two-sorted-arrays/
示例
示例 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.length == m
- nums2.length == n
- 0 <= m <= 1000
- 0 <= n <= 1000
- 1 <= m + n <= 2000
- -106 <= nums1[i], nums2[i] <= 106
解题思路
方法一:二分查找
要达到 O(log(m+n)) 的时间复杂度,必须使用二分查找。关键是将问题转化为寻找第k小的数的问题。
关键点:
- 中位数的定义:
- 当总长度为奇数时,中位数是第 (m+n)/2 + 1 个数
- 当总长度为偶数时,中位数是第 (m+n)/2 和第 (m+n)/2 + 1 个数的平均值
- 使用二分查找来寻找第k小的数
- 每次比较两个数组的第 k/2 个数,排除掉不可能的部分
具体步骤:
- 确定中位数的位置k
- 在两个数组中进行二分查找:
- 比较两个数组中第 k/2 个数的大小
- 较小的那部分不可能包含第k小的数
- 排除较小的部分,继续查找剩余部分
- 递归进行上述过程,直到找到目标数
时间复杂度:O(log(m+n)) 空间复杂度:O(1)
方法二:划分数组(更优解)
这个方法的思路是将两个数组分别划分,使得:
- 左半部分的长度等于右半部分
- 左半部分的最大值小于等于右半部分的最小值
代码实现
C# 实现(二分查找)
public class Solution {
public double FindMedianSortedArrays(int[] nums1, int[] nums2) {
int totalLength = nums1.Length + nums2.Length;
if (totalLength % 2 == 1) {
return FindKth(nums1, 0, nums2, 0, totalLength / 2 + 1);
} else {
return (FindKth(nums1, 0, nums2, 0, totalLength / 2) +
FindKth(nums1, 0, nums2, 0, totalLength / 2 + 1)) / 2.0;
}
}
private double FindKth(int[] nums1, int start1, int[] nums2, int start2, int k) {
// 如果数组1已经用完,直接从数组2中返回第k个数
if (start1 >= nums1.Length) {
return nums2[start2 + k - 1];
}
// 如果数组2已经用完,直接从数组1中返回第k个数
if (start2 >= nums2.Length) {
return nums1[start1 + k - 1];
}
// 如果k=1,返回两个数组开头较小的数
if (k == 1) {
return Math.Min(nums1[start1], nums2[start2]);
}
// 比较两个数组中第k/2个数的大小
int mid1 = start1 + k/2 - 1 < nums1.Length ?
nums1[start1 + k/2 - 1] : int.MaxValue;
int mid2 = start2 + k/2 - 1 < nums2.Length ?
nums2[start2 + k/2 - 1] : int.MaxValue;
if (mid1 < mid2) {
// 排除nums1的前k/2个数
return FindKth(nums1, start1 + k/2, nums2, start2, k - k/2);
} else {
// 排除nums2的前k/2个数
return FindKth(nums1, start1, nums2, start2 + k/2, k - k/2);
}
}
}
C# 实现(划分数组)
public class Solution {
public double FindMedianSortedArrays(int[] nums1, int[] nums2) {
// 确保 nums1 是较短的数组
if (nums1.Length > nums2.Length) {
return FindMedianSortedArrays(nums2, nums1);
}
int m = nums1.Length;
int n = nums2.Length;
int left = 0;
int right = m;
while (left <= right) {
// nums1 的分割线
int i = (left + right) / 2;
// nums2 的分割线
int j = (m + n + 1) / 2 - i;
// nums1 分割线左边的值
int nums1LeftMax = (i == 0) ? int.MinValue : nums1[i - 1];
// nums1 分割线右边的值
int nums1RightMin = (i == m) ? int.MaxValue : nums1[i];
// nums2 分割线左边的值
int nums2LeftMax = (j == 0) ? int.MinValue : nums2[j - 1];
// nums2 分割线右边的值
int nums2RightMin = (j == n) ? int.MaxValue : nums2[j];
// 找到合适的分割线
if (nums1LeftMax <= nums2RightMin && nums2LeftMax <= nums1RightMin) {
// 如果是奇数个元素
if ((m + n) % 2 == 1) {
return Math.Max(nums1LeftMax, nums2LeftMax);
}
// 如果是偶数个元素
else {
return (Math.Max(nums1LeftMax, nums2LeftMax) +
Math.Min(nums1RightMin, nums2RightMin)) / 2.0;
}
}
// 需要调整分割线
else if (nums1LeftMax > nums2RightMin) {
right = i - 1;
}
else {
left = i + 1;
}
}
throw new ArgumentException("Input arrays are not sorted");
}
}
划分数组方法详解
-
基本思想:
- 在较短的数组上二分查找分割线位置
- 保证左半部分和右半部分的元素个数相等(或左边比右边多一个)
- 确保左半部分的最大值小于等于右半部分的最小值
-
关键步骤:
- 确保nums1是较短的数组,减少查找范围
- 通过二分查找确定nums1的分割线位置i
- 根据i计算nums2的分割线位置j
- 比较分割线两侧的值,判断是否找到正确位置
-
边界处理:
- 使用int.MinValue和int.MaxValue处理分割线在数组边界的情况
- 考虑总长度为奇数和偶数的不同情况
- 处理数组为空的特殊情况
-
时间复杂度分析:
- 总体时间复杂度:O(log(min(m,n)))
- 空间复杂度:O(1)
执行结果(划分数组方法):
- 执行用时:84 ms
- 内存消耗:50.8 MB
两种方法比较:
-
时间复杂度:
- 二分查找:O(log(m+n))
- 划分数组:O(log(min(m,n))),更优
-
空间复杂度:
- 两种方法都是O(1)
-
代码复杂度:
- 二分查找:实现较简单,思路直观
- 划分数组:实现较复杂,但效率更高
-
适用场景:
- 二分查找:更通用,容易理解
- 划分数组:特别适合处理有序数组的中位数问题
代码详解
totalLength % 2 == 1:判断总长度是奇数还是偶数FindKth方法:查找第k小的数- 处理边界情况:某个数组用完或k=1
- 比较两个数组中第k/2个数的大小
- 排除较小的部分,继续递归查找
int.MaxValue:处理数组长度不足k/2的情况- 递归调用时更新起始位置和k值
执行结果
- 执行用时:92 ms
- 内存消耗:51.2 MB
总结与反思
- 这是一道经典的困难题目,考察了:
- 二分查找的应用
- 分治思想
- 边界情况的处理
- 关键点:
- 理解中位数的定义
- 正确处理奇偶长度的情况
- 高效排除不可能的部分
- 优化思路:
- 可以通过划分数组的方法进一步优化
- 注意特殊情况的处理