完整教程:LeetCode 4:寻找两个正序数组的中位数

LeetCode 4:寻找两个正序数组的中位数

在这里插入图片描述

问题定义与核心挑战

给定两个有序(升序)数组nums1nums2,要求找到它们的中位数,且算法时间复杂度为 O(log(m+n))mn 分别是两个数组的长度)。

中位数定义

  • 若合并后数组长度为奇数,中位数是中间位置的元素(如 [1,2,3] 的中位数是 2)。
  • 若合并后数组长度为偶数,中位数是中间两个元素的平均值(如 [1,2,3,4] 的中位数是 (2+3)/2=2.5)。

常规方法的不足

若直接合并两个数组(时间复杂度 O(m+n)),虽能解决问题,但无法满足题目对时间复杂度的严格要求(O(log(m+n)))。因此,必须利用数组有序的特性,通过 二分查找 避免完全合并。

核心思路:二分法定位分割点

中位数的本质是将合并后的数组划分为左右两部分,使得:

  • 左边所有元素 ≤ 右边所有元素;
  • 左边元素个数 ≥ 右边(奇数长度时左边多一个)。

对于两个有序数组,我们可以通过二分查找分割点,将 nums1nums2 分别划分为左右两部分,使得:

  • nums1 的左半部分 + nums2 的左半部分 nums1 的右半部分 + nums2 的右半部分;
  • 左半部分总元素数为 (m+n+1)//2(向上取整,保证奇数长度时左边多一个)。

算法步骤详解

步骤 1:统一数组长度(优化二分范围)

为了减少二分查找的范围,确保 nums1 是较短的数组(若 nums1 更长,则交换 nums1nums2)。这样,二分查找仅需在较短数组上进行,降低时间复杂度。

int m = nums1.length, n = nums2.length;
if (m > n) {
// 交换数组,确保 nums1 是较短的
int[] temp = nums1;
nums1 = nums2;
nums2 = temp;
m = nums1.length;
n = nums2.length;
}
步骤 2:二分查找分割点

nums1 中二分查找分割点 i,并计算 nums2 的对应分割点 j

  • i 表示 nums1 左半部分的元素个数(范围:0 ≤ i ≤ m);
  • j = (m + n + 1) // 2 - i,保证左半部分总元素数为 (m+n+1)//2(向上取整)。
int left = 0, right = m;
while (left < right) {
// 向上取整,避免死循环(如 left=1, right=2 时,mid=2)
int mid = (left + right + 1) / 2;
int j = (m + n + 1) / 2 - mid;
if (nums1[mid-1] > nums2[j]) {
// i 太大,左移右边界
right = mid - 1;
} else {
// i 太小,右移左边界
left = mid;
}
}
int i = left;
int j = (m + n + 1) / 2 - i;
步骤 3:处理边界值(分割点越界)

分割点 ij 可能为 0(左半部分为空)或等于数组长度(右半部分为空),需用极值(-∞+∞)处理:

// nums1 左半部分的最大值(i=0 时,左半部分为空,设为 -∞)
double nums1_left = (i == 0) ? Integer.MIN_VALUE : nums1[i-1];
// nums1 右半部分的最小值(i=m 时,右半部分为空,设为 +∞)
double nums1_right = (i == m) ? Integer.MAX_VALUE : nums1[i];
// nums2 左半部分的最大值(j=0 时,左半部分为空,设为 -∞)
double nums2_left = (j == 0) ? Integer.MIN_VALUE : nums2[j-1];
// nums2 右半部分的最小值(j=n 时,右半部分为空,设为 +∞)
double nums2_right = (j == n) ? Integer.MAX_VALUE : nums2[j];
步骤 4:计算中位数

根据合并后数组的长度(m+n)的奇偶性,计算中位数:

  • 奇数:左半部分最大值(max(nums1_left, nums2_left));
  • 偶数:左半部分最大值与右半部分最小值的平均值((max(...) + min(...)) / 2)。
if ((m + n) % 2 == 1) {
// 奇数长度,中位数是左半部分最大值
return Math.max(nums1_left, nums2_left);
} else {
// 偶数长度,中位数是左右部分的中间值平均
return (Math.max(nums1_left, nums2_left) + Math.min(nums1_right, nums2_right)) / 2.0;
}

完整代码(Java)

class Solution
{
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length, n = nums2.length;
// 确保 nums1 是较短的数组,优化二分范围
if (m > n) {
int[] temp = nums1;
nums1 = nums2;
nums2 = temp;
m = nums1.length;
n = nums2.length;
}
int left = 0, right = m;
while (left < right) {
// 向上取整,避免死循环
int mid = (left + right + 1) / 2;
int j = (m + n + 1) / 2 - mid;
if (nums1[mid - 1] > nums2[j]) {
// i 太大,左移右边界
right = mid - 1;
} else {
// i 太小,右移左边界
left = mid;
}
}
int i = left;
int j = (m + n + 1) / 2 - i;
// 处理边界值:左半部分或右半部分为空的情况
double nums1_left = (i == 0) ? Integer.MIN_VALUE : nums1[i - 1];
double nums1_right = (i == m) ? Integer.MAX_VALUE : nums1[i];
double nums2_left = (j == 0) ? Integer.MIN_VALUE : nums2[j - 1];
double nums2_right = (j == n) ? Integer.MAX_VALUE : nums2[j];
// 计算中位数
if ((m + n) % 2 == 1) {
return Math.max(nums1_left, nums2_left);
} else {
return (Math.max(nums1_left, nums2_left) + Math.min(nums1_right, nums2_right)) / 2.0;
}
}
}

关键逻辑解析

1. 数组交换的意义

将较长数组与较短数组交换,确保二分查找在较短数组上进行,减少二分次数(如 m=3, n=5 时,二分范围是 0~3 而非 0~5)。

2. 分割点的数学意义

j = (m + n + 1) // 2 - i 保证 左半部分总元素数为 (m+n+1)//2

  • m+n 为奇数,左半部分比右半部分多 1 个元素,中位数是左半部分的最大值;
  • m+n 为偶数,左半部分与右半部分元素数相等,中位数是两部分的中间值平均。
3. 边界值处理

通过 Integer.MIN_VALUEInteger.MAX_VALUE 处理“左半部分为空”或“右半部分为空”的情况,确保比较逻辑的一致性。

4. 二分循环的细节

使用 (left + right + 1) / 2 向上取整,避免 leftright 相邻时的死循环(如 left=1, right=2 时,mid=2 而非 1,确保范围缩小)。

该方法通过 二分查找分割点 避免了数组的完全合并,将时间复杂度优化到 O(log(min(m,n)))(等价于 O(log(m+n))),是处理“两个有序数组中位数”问题的经典方案。

posted on 2025-08-01 13:28  ljbguanli  阅读(14)  评论(0)    收藏  举报