这道力扣经典难题要求我们在两个已排序的数组中找到合并后的中位数。看似简单的需求背后隐藏着几个关键难点:首先,题目要求时间复杂度必须控制在O(log(m+n)),这直接排除了暴力合并的解法;其次,两个数组长度可能差异很大,需要考虑各种边界情况;最重要的是,如何在有序数组中实现对数级复杂度,这自然指向了二分查找的变种应用。
我在第一次遇到这个问题时,尝试了直接合并后取中位数的暴力解法,结果在长数组测试用例上超时。后来经过反复推敲,才理解到这道题的精髓在于将中位数查找转化为寻找第k小元素的通用解法,而二分查找正是实现这一目标的最佳工具。
中位数在统计学上表示将数据集分成两个相等部分的数值。对于合并后的数组,当总长度为奇数时,中位数就是中间那个数;当长度为偶数时,则是中间两个数的平均值。这个定义提示我们可以将问题转化为"寻找第k小的数"的通用问题。
具体来说:
这种转化使得我们可以用统一的二分查找框架处理两种情况,大大简化了问题复杂度。
传统二分查找是在单个有序数组中进行的,而本题需要在两个数组中协同查找。核心思路是:
这种协同二分的关键在于理解分割点的四个关键值:
当中位数条件满足时,这四个值之间必须满足特定的排序关系。
首先处理几个特殊情况可以简化后续逻辑:
python复制def findMedianSortedArrays(nums1, nums2):
# 确保nums1是较短的数组
if len(nums1) > len(nums2):
nums1, nums2 = nums2, nums1
m, n = len(nums1), len(nums2)
total_left = (m + n + 1) // 2
# 处理nums1为空的情况
if not nums1:
if n % 2 == 1:
return nums2[n//2]
else:
return (nums2[n//2-1] + nums2[n//2]) / 2
在较短的数组nums1上进行二分查找,初始区间为[0, m]。每次迭代:
python复制 left, right = 0, m
while left <= right:
i = (left + right) // 2
j = total_left - i
# 处理边界情况
nums1_left_max = float('-inf') if i == 0 else nums1[i-1]
nums1_right_min = float('inf') if i == m else nums1[i]
nums2_left_max = float('-inf') if j == 0 else nums2[j-1]
nums2_right_min = float('inf') if j == n else nums2[j]
# 检查分割条件
if nums1_left_max <= nums2_right_min and nums2_left_max <= nums1_right_min:
# 找到正确分割
if (m + n) % 2 == 1:
return max(nums1_left_max, nums2_left_max)
else:
return (max(nums1_left_max, nums2_left_max) + min(nums1_right_min, nums2_right_min)) / 2
elif nums1_left_max > nums2_right_min:
right = i - 1
else:
left = i + 1
由于每次迭代都将搜索区间减半,且总是在较短的数组上进行二分,因此时间复杂度为O(log(min(m,n))),这比题目要求的O(log(m+n))更优。
算法只使用了常数级别的额外空间,因此空间复杂度为O(1)。
好的测试用例应该包括:
本算法可以很容易扩展为寻找两个有序数组中第k小元素的通用解法。只需将total_left替换为k,并调整相应的边界条件即可。
对于三个或更多有序数组的情况,可以采用类似的二分思路,但需要更复杂的分割点计算和条件检查。
如果数据是以流式方式到达的,可以考虑使用堆结构来动态维护中位数,虽然时间复杂度会有所增加。
这种高效的二分查找变种在以下场景中非常有用:
我在实际工作中曾用类似算法优化过一个实时监控系统,该系统需要从多个数据源合并指标并计算中位数。原始实现使用合并后排序的方法,在数据量大时经常超时。改用这种二分查找方法后,性能提升了近百倍。
这个算法最精妙的地方在于将两个数组的分割点关联起来,通过二分查找较短数组来协同确定两个数组的分割位置。理解这一点后,类似的协同二分问题都可以迎刃而解。