4. 寻找两个正序数组的中位数 ——二分查找解法
已解答
困难
给定两个大小分别为 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 == m -
nums2.length == n -
0 <= m <= 1000 -
0 <= n <= 1000 -
1 <= m + n <= 2000 -
-106 <= nums1[i], nums2[i] <= 106
🎯 核心思想:问题转化
这道题的核心思想不是真的去合并数组,而是把问题转化为“寻找一个完美的分割点”。
我们试图在两个数组 a 和 b 中各找到一个分割点(i 和 j),将所有元素分为“左半部分”和“右半部分”,这个分割点需要满足两个条件:
- 长度条件: 左半部分所有元素的总数 =
(m+n+1)/2(这个+1是个技巧,能同时兼容总数为奇数和偶数的情况)。 - 大小条件: 左半部分所有元素 <= 右半部分所有元素。
只要我们找到了这个分割点,中位数就自然产生了:
- 总数为奇数: 中位数就是左半部分的最大值。
- 总数为偶数: 中位数就是 (左半部分的最大值 + 右半部分的最小值) / 2。
🔍 算法精髓:二分查找分割点
我们不必同时寻找 i 和 j。因为“长度条件”把它们绑定了。
在代码中,i 和 j 指的是左半部分最后一个元素的索引。
a的左半部分有i+1个元素。(0~i)b的左半部分有j+1个元素。(0~j)- 根据长度条件:
(i+1) + (j+1) = (m+n+1)/2 - 推导出
j的公式:j = (m+n+1)/2 - i - 2
这样,我们只需要在较短的数组 a (为了效率,代码开头保证了 a 是短数组) 中通过二分查找找到那个完美的 i 即可。
🔑 关键代码解析
1. 二分查找的逻辑(开区间写法)
left, right := -1, m // 搜索 i 的范围是 (-1, m)
for left+1 < right { //保证开区间不为空
i := left + (right-left)/2 //i 就是二分查找里的mid
j := (m+n+1)/2 - i - 2
if a[i] <= b[j+1] { //说明i还可以增大
left = i // 缩小二分区间为 (i, right)
} else {
right = i // 缩小二分区间为 (left, i)
}
}
i := left //left 就是我们最终找到的完美分割点 i
j := (m+n+1)/2 - i - 2
- 搜索范围:
i的取值范围是[-1, m-1]。i = -1表示a的左半部分为空。i = m-1表示a的右半部分为空。
- 判断条件:
if a[i] < b[j+1]- “大小条件”要求
a[i] <= b[j+1]并且b[j] <= a[i+1]。 - 我们主要用
a[i] < b[j+1]来驱动二分查找。 a[i] < b[j+1]成立:- 说明
a[i]这个值是安全的,它确实小于b的右半部分。 i可能是我们要找的答案,或者真正的答案在i的右边(i还可以再大一点)。- 所以我们收缩左边界:
left = i。
- 说明
a[i] >= b[j+1]不成立:- 说明
a[i]太大了,它不满足“a左 <=b右”的条件。 i肯定不是答案,真正的答案必定在i的左边。- 所以我们收缩右边界:
right = i。
- 说明
- “大小条件”要求
- 循环结束:
- 当
left+1 == right时,循环停止。 - 此时
i = left就是我们找到的满足a[i] < b[j+1]的最大索引i。 - 这个二分查找的精妙之处在于,当你找到这个
i时,另一个条件b[j] <= a[i+1]会被自动满足。
- 当
2. 边界处理 (防越界)
ai := math.MinInt; if i >= 0 { ai = a[i] }
bj := math.MinInt; if j >= 0 { bj = b[j] }
ai1 := math.MaxInt; if i+1 < m { ai1 = a[i+1] }
bj1 := math.MaxInt; if j+1 < n { bj1 = b[j+1] }
这是为了处理分割点在数组两端时的“越界”情况。
ai = a[i](a的左部最大)- 如果
i = -1(a的左部为空),ai必须是最小值,不影响max运算,故取MinInt。
- 如果
ai1 = a[i+1](a的右部最小)- 如果
i = m-1(a的右部为空),ai1必须是最大值,不影响min运算,故取MaxInt。
- 如果
bj和bj1同理。
3. 计算结果
maxLeft := max(ai, bj)
minRight := min(ai1, bj1)
if (m+n)%2 == 0 { // 偶数
return float64(maxLeft+minRight) / 2.0
} else { // 奇数
return float64(maxLeft)
}
maxLeft就是整个“左半部分”的最大值。minRight就是整个“右半部分”的最小值。- 根据总长度的奇偶性,返回最终答案。
时间复杂度:O(logmin(m,n)),其中 m 是 a 的长度,n 是 b 的长度。
空间复杂度:O(1)。
感谢@灵神
💡 复习要点
- 目标: 找分割点
i和j。 - 约束1 (长度):
(i+1) + (j+1) = (m+n+1)/2->j = ... - 约束2 (大小):
a[i] <= b[j+1]且b[j] <= a[i+1]。 - 手段: 在短数组
a上二分查找i。 - 二分逻辑:
if a[i] < b[j+1]成立,说明i偏小,left = i;反之i偏大,right = i。 - 边界: 用
MinInt和MaxInt处理i或j在-1或m-1 / n-1时的空集情况。
1611

被折叠的 条评论
为什么被折叠?



