寻找两个正序数组的中位数 ——二分查找解法


已解答

困难

给定两个大小分别为 mn 的正序(从小到大)数组 nums1nums2。请你找出并返回这两个正序数组的 中位数

算法的时间复杂度应该为 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

🎯 核心思想:问题转化

这道题的核心思想不是真的去合并数组,而是把问题转化为“寻找一个完美的分割点”

我们试图在两个数组 ab 中各找到一个分割点(ij),将所有元素分为“左半部分”和“右半部分”,这个分割点需要满足两个条件:

  1. 长度条件: 左半部分所有元素的总数 = (m+n+1)/2 (这个 +1 是个技巧,能同时兼容总数为奇数和偶数的情况)。
  2. 大小条件: 左半部分所有元素 <= 右半部分所有元素。

只要我们找到了这个分割点,中位数就自然产生了:

  • 总数为奇数: 中位数就是左半部分的最大值
  • 总数为偶数: 中位数就是 (左半部分的最大值 + 右半部分的最小值) / 2

🔍 算法精髓:二分查找分割点

我们不必同时寻找 ij。因为“长度条件”把它们绑定了。

在代码中,ij 指的是左半部分最后一个元素的索引

  • 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
  • bjbj1 同理。

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)。
感谢@灵神


💡 复习要点

  • 目标: 找分割点 ij
  • 约束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
  • 边界:MinIntMaxInt 处理 ij-1m-1 / n-1 时的空集情况。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jessie_waverider

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值