题目描述:
给定两个大小分别为 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.length == mnums2.length == n0 <= m <= 10000 <= n <= 10001 <= m + n <= 2000-10^6 <= nums1[i], nums2[i] <= 10^6
思路:
困难题确实不一样。能O(1)获得的是两个数组分别的长度。一个数组的中位数怎么算?
应该是 mid = (0 + n)/2。对吗?不完全对。奇数的情况是数组中的一个值。偶数的情况是两个值的平均。
倘若合并了,能快速找到吗?可以。但是合并的时间复杂度最快也是O(nlogn) - 快速排序
很明显这个是O(log(m+n)),二分查找
假设我要找的是合并后排序数组的第k个值(奇数情况)。k = (0 + 3)/2 = 1。位置正确,这意味着我要找的是两个数组“合并”之后的第k个值。
没有思路了。看看题解吧。
DS提供的思路:
首先确定较小的数组。找到一个分割点i,对较长的数组,找到分割点j。对于i和j,需要满足:
-
i + j = (n + m)/2 - 这意味着找到的分割点刚好是一半
-
这两个分割点的关系:需要满足
- i的左边最大值要小于j的右边最小值
- j的左边最大值要小于i的右边最大值
- 且i + j = (n + m)/2
-
如果分割成功:
- 如果m+n是奇数,则取min(num1[i], num2[j])
- 如果m+n是偶数,则取(max(left) + min(right))/2
-
边界条件处理:
- 可能一个数组完全比另一个数组大。这种情况就是需要判断越界。然后判断哪个更大。然后根据奇偶性找到位置。
太难了,放弃了。
DS的解答
以下是满足要求的完整代码实现,已处理所有边界条件并添加详细注释:
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
// 确保nums1是较短的数组以优化时间复杂度
if (nums1.length > nums2.length) {
int[] temp = nums1;
nums1 = nums2;
nums2 = temp;
}
int m = nums1.length, n = nums2.length;
// 计算中位数的分割点
int totalLeft = (m + n + 1) / 2;
int left = 0, right = m;
while (left < right) {
// 二分法确定nums1的分割点i
int i = left + (right - left + 1) / 2;
int j = totalLeft - i;
// 关键比较逻辑(处理边界)
if (nums1[i - 1] > nums2[j]) {
right = i - 1; // i需要减小
} else {
left = i; // 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 actualLeftMax = Math.max(nums1LeftMax, nums2LeftMax);
// 计算右半部分最小值(处理边界)
int nums1RightMin = (i == m) ? Integer.MAX_VALUE : nums1[i];
int nums2RightMin = (j == n) ? Integer.MAX_VALUE : nums2[j];
int actualRightMin = Math.min(nums1RightMin, nums2RightMin);
// 根据总长度奇偶性返回结果
if ((m + n) % 2 == 1) {
return actualLeftMax;
} else {
return (actualLeftMax + actualRightMin) / 2.0;
}
}
}
关键点说明:
-
数组交换优化:通过确保nums1始终是较短的数组,将时间复杂度优化到O(log(min(m,n)))
-
分割点计算: •
totalLeft = (m + n + 1) / 2统一处理奇偶情况 • 使用left + (right - left + 1) / 2避免死循环 -
边界处理技巧: • 使用
Integer.MIN_VALUE/MAX_VALUE处理分割点在数组端点的情况 • 通过i == 0/i == m等判断处理空数组的情况 -
中位数计算逻辑: • 奇数长度时直接取左半最大值 • 偶数长度时取左右部分平均值
复杂度分析:
• 时间复杂度:O(log(min(m,n))) • 空间复杂度:O(1)
该实现通过了LeetCode所有测试用例,可直接提交使用。建议结合二分查找示意图理解分割点的移动逻辑。
再战一次
先上代码:
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
if (nums1.length > nums2.length) {
int[] temp = nums1;
nums1 = nums2;
nums2 = temp;
}
int m = nums1.length, n = nums2.length;
int le = 0, ri = m;
while (le < ri) {
int i = (le + ri + 1) >> 1; // dont use min to compare
int j = ((n + m + 1) >> 1) - i;
int numsi_1 = (i == 0) ? Integer.MIN_VALUE : nums1[i-1];
int numsj = (j == n) ? Integer.MAX_VALUE : nums2[j];
if (numsi_1 > numsj) {
ri = i - 1; // try i first.
} else {
le = i;
}
}
int i = le, j = ((n + m + 1) >> 1) - i;
int numsi_1 = (i == 0) ? Integer.MIN_VALUE : nums1[i-1];
int numsi = (i == m) ? Integer.MAX_VALUE : nums1[i];
int numsj_1 = (j == 0) ? Integer.MIN_VALUE : nums2[j-1];
int numsj = (j == n) ? Integer.MAX_VALUE : nums2[j];
if (((n + m) % 2) == 1) {
return Math.max(numsi_1, numsj_1);
}
return (Math.max(numsi_1, numsj_1) + Math.min(numsi, numsj)) / 2.0;
}
}
什么?这次竟然成功了?原因呢?是直接写出来的吗?非也。
这道题很复杂,大体可以分为以下几部分:
- 确保nums1是较短的数组,如果较长,则交换。
- 对较短的nums1进行二分查找
- 计算i的值,这里会纠结是(left + right + 1)/2 还是 (left + right)/2,对吗。类似的,计算j也在考虑要不要+1的问题。
- 判断条件, 这里要清楚,找的是分割点,所以判断条件肯定是i-1和j比,或者i和j-1比。
- 边界的更新,对right,是更新成i-1,还是更新成i?对left,更新成i+1还是i?
- 二分查找结束了,i是left?还是+1或者-1?
- 奇数的情况,这个要固定成i-1和j-1的较大值
- 偶数的情况,类似的,较大值+较小值的和,再除以二。
可见即便是了解了这道题的基本思路,但是细节上依旧很容易犯错,导致无法AC。
我的建议:
举例子。一定要在纸上画,靠小例子来验证你的思路。为了节约思维负担,这里有个小技巧,只需要无脑记忆left+right+1和n+m+1的部分就好了。剩下需要额外推导的就是right = i - 1还是i,left = i还是i+1了。无非是四种情况。
而我推荐你使用下面的几个例子来画图验证(左侧是nums1,右侧是nums2):
[1], [2, 3]
[2], [1, 3]
[1, 2], [3, 4]
[3, 4], [1, 2]
是用以上的例子去研究结束之后的i和j,看看是怎么划分的。正确的划分应该如下:
[1 /], [2,/ 3] 奇数,取i-1和j-1较大值。/左边是x-1,右边是x。
[2 /], [1,/ 3] 奇数,取i-1和j-1较大值。/左边是x-1,右边是x。
[1, 2 /], [/3, 4] 偶数,取平均。左边没数字,就是Integer.MIN_VALUE。右边同理。
[/3, 4], [1, 2/] 偶数,取平均。i是3, i-1是最小值,j-1是2,j是最大值。左边取大=2, 右边取小=3.取平均。
知道了什么是“正确”之后就好办了,就去验证right和left分别的设定就好了。
希望对你有帮助。