题目描述
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应为 $O(\log (m+n))$。
示例 1:
1 | 输入:nums1 = [1,3], nums2 = [2] |
示例 2:
1 | 输入:nums1 = [1,2], nums2 = [3,4] |
核心思路:切分与二分查找
要在 $O(\log(m+n))$ 时间内解决问题,我们不能通过合并后再排序($O(m+n)$),而必须利用数组的有序性使用二分查找。
1. 中位数的本质:划分
中位数的本质是将一个集合划分为两个长度相等(或差1)的子集,使得左半部分的最大值 $\le$ 右半部分的最小值。
对于两个数组 nums1 和 nums2,我们要找到两个切分点 i 和 j:
nums1被切分为:nums1[0...i-1](左) |nums1[i...m-1](右)nums2被切分为:nums2[0...j-1](左) |nums2[j...n-1](右)
2. 满足的条件
为了让这两部分合并后依然满足中位数的定义,我们需要满足:
- 数量平衡:
i + j = (m + n + 1) / 2。这样左半部分的总长度就固定了。如果我们确定了i,那么j也就随之确定。 - 数值交叉小于等于:
nums1[i-1] <= nums2[j](nums1 的左侧最大 $\le$ nums2 的右侧最小)nums2[j-1] <= nums1[i](nums2 的左侧最大 $\le$ nums1 的右侧最小)
3. 为什么用二分?
因为 i 的增加会导致 j 的减少(为了维持数量平衡),这种单调性让我们可以在较短的数组(假设为 nums1)中通过二分查找来寻找完美的切分点 i。
算法详解
- 选择短数组:对较短的数组进行二分查找,可以将复杂度控制在 $O(\log(\min(m, n)))$。
- 范围确定:
left = 0,right = m。 - 二分收缩:
- 如果
nums1[i-1] > nums2[j]:说明i太大了,需要向左收缩right = i - 1。 - 如果
nums2[j-1] > nums1[i]:说明i太小了,需要向右扩张left = i + 1。
- 如果
- 边界处理:当
i或j处于数组边缘(0或length)时,使用Int.MIN_VALUE或Int.MAX_VALUE代替。
代码实现 (Kotlin)
1 | class Solution { |
复杂度分析
- 时间复杂度:$O(\log(\min(m, n)))$。我们只对较短的数组进行二分查找。
- 空间复杂度:$O(1)$。只使用了常数个变量。
思考与扩展
Q:如果有 N 个有序数组,如何求中位数?
- 堆排序法:使用最小堆同时遍历 N 个数组,弹出 $(m+n+…)/2$ 次元素。时间复杂度 $O(K \cdot \log N)$,其中 $K$ 是总元素个数的一半。
- 多路归并:类似合并 K 个有序链表。
- 二分答案法:在所有数字的数值范围
[min, max]内进行二分查找,通过count函数统计小于等于mid的元素总数,直到找到第K小的数。这种方法对处理超大规模数据非常有效。