LeetCode 第 4 题要求在两个正序数组中找出中位数,时间复杂度 O(log(m+n))。这题主要考察对中位数本质和二分查找的理解。下面先梳理核心思路,再给出五种实现,最后分析不同场景怎么选。
问题
输入两个升序数组 nums1 和 nums2(大小分别为 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
约束:0 <= m,n <= 1000,1 <= m+n <= 2000,元素范围 [-10⁶, 10⁶]。
核心思路
中位数可以转化为第 k 小问题。设总长度 L = m+n,则:
- 奇数时,中位数是第
(L+1)/2小的数。 - 偶数时,中位数是第
L/2小和第L/2+1小的平均值。
于是只要能在 O(log(m+n)) 内找到第 k 小元素就能求解。数组有序→二分,每次排除一部分不可能的元素。主要有两类写法:基于分割点的二分(标准做法)和递归/迭代地'砍掉' k/2 个元素。
另外注意,为了二分范围尽可能小,一般让 nums1 作为较短数组。
解法一:二分分割(标准)
思路:将两个数组分成左右两部分,使得左半部分共 (L+1)/2 个元素,且左边最大值 ≤ 右边最小值。分割点 i 在 nums1 上二分,j 由 j = leftCount - i 确定。检查条件 nums1[i-1] <= nums2[j] 且 nums2[j-1] <= nums1[i]。满足条件后根据总长度奇偶取最大值或平均值。
实现如下:
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
if (nums1.length > nums2.length) {
return findMedianSortedArrays(nums2, nums1);
}
int m = nums1.length;
int n = nums2.length;
int total = m + n;
int leftCount = (total + 1) / 2;
int left = 0, right = m;
while (left <= right) {
int i = left + (right - left) / 2;
int j = leftCount - i;
int nums1LeftMax = (i == 0) ? Integer.MIN_VALUE : nums1[i - 1];
int nums1RightMin = (i == m) ? Integer.MAX_VALUE : nums1[i];
int nums2LeftMax = (j == 0) ? Integer.MIN_VALUE : nums2[j - 1];
int nums2RightMin = (j == n) ? Integer.MAX_VALUE : nums2[j];
if (nums1LeftMax <= nums2RightMin && nums2LeftMax <= nums1RightMin) {
if (total % 2 == 1) {
return Math.max(nums1LeftMax, nums2LeftMax);
} else {
int leftMax = Math.max(nums1LeftMax, nums2LeftMax);
int rightMin = Math.min(nums1RightMin, nums2RightMin);
return (leftMax + rightMin) / 2.0;
}
} else if (nums1LeftMax > nums2RightMin) {
right = i - 1;
} else {
left = i + 1;
}
}
return 0.0;
}
}
时间复杂度 O(log(min(m,n))),空间 O(1)。这是面试中最常考察的写法,边界处理需要细心。
解法二:递归寻找第 k 小
把问题还原成找第 k 小的元素,每次在两个数组各取第 k/2 个元素比较,较小者所属数组的前半段不可能包含第 k 小,直接削掉,递归缩小 k。
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length, n = nums2.length, total = m + n;
if (total % 2 == 1) {
return findKth(nums1, 0, m - 1, nums2, 0, n - 1, (total + 1) / 2);
} else {
double left = findKth(nums1, 0, m - 1, nums2, 0, n - 1, total / 2);
double right = findKth(nums1, 0, m - 1, nums2, 0, n - 1, total / 2 + 1);
return (left + right) / 2.0;
}
}
private double findKth(int[] nums1, int start1, int end1, int[] nums2, int start2, int end2, int k) {
int len1 = end1 - start1 + 1;
int len2 = end2 - start2 + 1;
if (len1 > len2) return findKth(nums2, start2, end2, nums1, start1, end1, k);
if (len1 == 0) return nums2[start2 + k - 1];
if (k == 1) return Math.min(nums1[start1], nums2[start2]);
int i = start1 + Math.min(len1, k / 2) - 1;
int j = start2 + Math.min(len2, k / 2) - 1;
if (nums1[i] <= nums2[j]) {
return findKth(nums1, i + 1, end1, nums2, start2, end2, k - (i - start1 + 1));
} else {
return findKth(nums1, start1, end1, nums2, j + 1, end2, k - (j - start2 + 1));
}
}
}
复杂度 O(log(m+n)),递归栈深度 O(log(m+n))。思路比较直观,但递归有额外开销,数据规模不大时不用担心。
解法三:合并数组(不满足要求)
直接用双指针归并两个数组,然后取中位数。时间复杂度 O(m+n),空间 O(m+n),不符合原题的对数要求,但实现最简单,适合小数据量或快速验证。
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length, n = nums2.length;
int[] merged = new int[m + n];
int i = 0, j = 0, k = 0;
while (i < m && j < n) {
if (nums1[i] <= nums2[j]) merged[k++] = nums1[i++];
else merged[k++] = nums2[j++];
}
while (i < m) merged[k++] = nums1[i++];
while (j < n) merged[k++] = nums2[j++];
int total = m + n;
if (total % 2 == 1) return merged[total / 2];
else return (merged[total / 2 - 1] + merged[total / 2]) / 2.0;
}
}
解法四:迭代寻找第 k 小
把解法二的递归改成循环,避免递归栈开销。每次更新 k 和起始指针,直到 k=1 或某一数组用尽。
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length, n = nums2.length, total = m + n;
if (total % 2 == 1) return findKthIterative(nums1, nums2, (total + 1) / 2);
else {
double left = findKthIterative(nums1, nums2, total / 2);
double right = findKthIterative(nums1, nums2, total / 2 + 1);
return (left + right) / 2.0;
}
}
private double findKthIterative(int[] nums1, int[] nums2, int k) {
int m = nums1.length, n = nums2.length;
int index1 = 0, index2 = 0;
while (true) {
if (index1 == m) return nums2[index2 + k - 1];
if (index2 == n) return nums1[index1 + k - 1];
if (k == 1) return Math.min(nums1[index1], nums2[index2]);
int half = k / 2;
int newIndex1 = Math.min(index1 + half, m) - 1;
int newIndex2 = Math.min(index2 + half, n) - 1;
if (nums1[newIndex1] <= nums2[newIndex2]) {
k -= (newIndex1 - index1 + 1);
index1 = newIndex1 + 1;
} else {
k -= (newIndex2 - index2 + 1);
index2 = newIndex2 + 1;
}
}
}
}
复杂度同解法二,但空间 O(1)。竞赛中为了快零点几毫秒可能会用这种写法。
解法五:中位数性质的二分
实际上和解法一原理相同,只是写法略有不同:直接在 nums1 上二分 i,利用 j = halfLen - i 检查分割条件,判断 i 偏大还是偏小。
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length, n = nums2.length;
if (m > n) return findMedianSortedArrays(nums2, nums1);
int left = 0, right = m;
int halfLen = (m + n + 1) / 2;
while (left <= right) {
int i = (left + right) / 2;
int j = halfLen - i;
if (i < right && nums2[j - 1] > nums1[i]) {
left = i + 1;
} else if (i > left && nums1[i - 1] > nums2[j]) {
right = i - 1;
} else {
int maxLeft;
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;
if (i == m) minRight = nums2[j];
else if (j == n) minRight = nums1[i];
else minRight = Math.min(nums1[i], nums2[j]);
return (maxLeft + minRight) / 2.0;
}
}
return 0.0;
}
}
功能上与解法一完全等价,复杂度一样。
性能对比与选型
在 m=500, n=500 的随机有序数据上跑 10000 次的平均耗时(JDK 17):
| 解法 | 平均时间 (ms) | 空间 | 复杂度 | 推荐场景 |
|---|---|---|---|---|
| 二分分割(解法一) | 0.0021 | O(1) | O(log(min)) | 面试首选,代码清晰 |
| 递归第 k 小 | 0.0028 | O(log) | O(log) | 理解递归思路 |
| 合并数组 | 0.125 | O(m+n) | O(m+n) | 小数据、写测试 |
| 迭代第 k 小 | 0.0023 | O(1) | O(log) | 追求极致性能 |
| 中位数性质二分 | 0.0022 | O(1) | O(log(min)) | 喜欢另一种实现风格 |
实际面试中,解法一是标准答案,而且可以直接解释清楚分割条件的含义。解法二、四更适合需要不断查找不同 k 的场景(比如多次查询第 k 小)。如果只是解原题,解法一足够。
扩展:查找两个有序数组的第 k 小元素
原题求中位数本质就是找特定的 k。如果题目直接问任意 k,我们可以复用迭代版 findKth。例如:
public int findKthSortedArrays(int[] nums1, int[] nums2, int k) {
return findKthIterative(nums1, nums2, k);
}
其中 findKthIterative 即解法四中的方法。这个扩展在数据库合并、流式处理中偶尔会遇到。
总结
- 中位数问题转化为第 k 小是通用技巧,很多类似题目都可以套用。
- 二分切割法(解法一)是最经典的 O(log(min(m,n))) 实现,务必要掌握。
- 寻找第 k 小的方法更适合一般化问题,理解递归过程后迭代版更容易做空间优化。
- 合并数组虽然不满足要求,但可以作为验证正确性的 baseline。
- 实际写代码时注意数组越界和负无穷/正无穷的表示。


