引言
在算法面试和刷题中,"寻找两个有序数组的中位数"是 LeetCode 上的经典难题。本文将以新手友好的方式,带你用双指针归并法高效解决这一问题,提供清晰的思路、完整的 Java 代码和复杂度分析,助你轻松应对面试和实战场景。
问题描述
给定两个有序数组 nums1 和 nums2,求它们合并后的中位数。示例源自 LeetCode:LeetCode 4. Median of Two Sorted Arrays。
解题思路:双指针归并法
核心思路:
- 使用两个指针分别遍历两个数组,按从小到大的顺序合并到新数组。
- 合并完成后,根据合并后数组长度的奇偶性直接计算中位数。
- 算法时间复杂度为 O(m+n),实现简单易懂。
实现简单,时间复杂度 O(m+n)。如需 O(log(m+n)),可采用二分查找优化。
Java 完整代码(含详细注释)
java
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
// 1. 创建一个辅助数组,长度为两个原数组长度之和,用于存放合并后的有序序列
int [] nums = new int[n+m];
double res = 0;
// i 指向 nums1 的当前处理位置,j 指向 nums2,k 指向新数组 nums
int i = 0, j = 0, k = 0;
// 2. 归并过程:比较两个数组当前的元素,谁小就先放谁进新数组
while (i < m && j < n){
if (nums1[i] < nums2[j]){
nums[k] = nums1[i]; // nums1 当前值更小,放入新数组
i++; // nums1 指针后移
} else {
nums[k] = nums2[j]; // nums2 当前值更小(或相等),放入新数组
j++; // nums2 指针后移
}
k++; // 新数组指针永远后移
}
// 3. 扫尾工作:如果 nums1 还有剩余元素,直接按顺序全部拼接到新数组后面
while (i < m){
nums[k] = nums1[i];
i++;
k++;
}
// 4. 扫尾工作:如果 nums2 还有剩余元素,直接按顺序全部拼接到新数组后面
while (j < n) {
nums[k] = nums2[j];
j++;
k++;
}
// 5. 计算中位数:根据合并后大数组总长度的奇偶性来判断
if((n + m) % 2 != 0 ){
// 如果是奇数,中位数就是中间那个数
// 例如长度为 5,下标 0,1,2,3,4,中位数下标为 5/2 = 2
res = (double) nums[(n+m)/2];
} else {
// 如果是偶数,中位数是中间两个数的平均值
// 例如长度为 4,下标 0,1,2,3,中位数为 (nums[1] + nums[2]) / 2
res = (double) (nums[(n+m)/2] + nums[(n+m)/2-1]) / 2;
}
return res;
}
}
复杂度分析
- 时间复杂度:O(m+n)(m、n 为两个数组长度)。
- 空间复杂度:O(m+n)(需新数组存储归并结果)。
进阶优化
本方法实现简单、易于理解,适合初学者和面试快速上手。如果追求更高效率,可研究二分查找优化至 O(log(m+n)),欢迎在评论区交流你的优化思路!
拓展二分查找
思路:
-
转化问题:在两个有序数组中寻找中位数,本质上是寻找一个划分点,将 nums1 和 nums2分别划分为左右两部分。
-
核心条件:
-
左半部分的长度等于右半部分的长度(或比右半部分多 1)。
-
左半部分的最大值≤ 右半部分的最小值,即:
max(left_A, left_B) <= min(right_A, right_B)。
-
-
优化搜索:由于数组是有序的,我们只需要在较短的数组上进行二分查找。如果短数组的划分点确定了,长数组的划分点为了满足"长度相等"也就自动确定了。
解题过程
-
确保 m ≤ n:如果 nums1 较长,交换两者。这能保证二分查找的时间复杂度为 O(log(min(m, n)))。
-
设定二分范围 :在
[0, m]之间搜索nums1的划分位置 i。 -
计算对应位置:
- i 是
nums1的划分点,j = (m + n + 1) / 2 - i 是 nums2的划分点。
- i 是
-
二分判断:
-
如果 nums1
[i-1]nums2[j],说明 i太大了,需要向左收缩。 -
如果nums2
[j-1] >nums1[i],说明 i太小了,需要向右扩张。
-
-
处理边界 :考虑到划分点可能在数组的两端(0或 m),使用
Integer.MIN_VALUE和Integer.MAX_VALUE来简化边界判断。 -
计算结果:
-
总长度为奇数:返回左半部分的最大值。
-
总长度为偶数:返回(左半部分最大值 + 右半部分最小值)/ 2。
-
复杂度
-
时间复杂度: O(log(min(m, n)))。我们在较短的数组上进行二分查找。
-
空间复杂度: O(1)。只使用了常数级别的额外变量。
代码
java
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, right = m;
// 整个左半部分需要的元素个数
int totalLeft = (m + n + 1) / 2;
while (left < right) {
// 二分查找 nums1 的划分点 i
int i = left + (right - left + 1) / 2;
int j = totalLeft - i;
// 如果 nums1[i-1] > nums2[j],说明 i 划分得太靠右了
if (nums1[i - 1] > nums2[j]) {
right = i - 1;
} else {
left = i;
}
}
int i = left;
int j = totalLeft - i;
// 边界处理:左侧最大值
int nums1LeftMax = (i == 0) ? Integer.MIN_VALUE : nums1[i - 1];
int nums2LeftMax = (j == 0) ? Integer.MIN_VALUE : nums2[j - 1];
// 边界处理:右侧最小值
int nums1RightMin = (i == m) ? Integer.MAX_VALUE : nums1[i];
int nums2RightMin = (j == n) ? Integer.MAX_VALUE : nums2[j];
if ((m + n) % 2 != 0) {
// 奇数长度,直接返回左半部分最大值
return Math.max(nums1LeftMax, nums2LeftMax);
} else {
// 偶数长度,取左侧最大和右侧最小的平均值
return (Math.max(nums1LeftMax, nums2LeftMax) + Math.min(nums1RightMin, nums2RightMin)) / 2.0;
}
}
}