一、问题理解
1. 题目描述
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2,请找出这两个有序数组的中位数 ,并要求算法的时间复杂度为 O(log(m+n))。
2. 中位数定义
- 如果元素总数是奇数:中位数是最中间那个数
- 如果元素总数是偶数:中位数是中间两个数的平均值
3. 示例
python
# 示例1
nums1 = [1, 3]
nums2 = [2]
中位数 = 2.0
# 示例2
nums1 = [1, 2]
nums2 = [3, 4]
中位数 = (2 + 3) / 2 = 2.5
# 示例3
nums1 = [0, 0]
nums2 = [0, 0]
中位数 = 0.0
二、方法1:合并数组法(直观但不符合时间复杂度要求)
1. 算法思路
- 合并两个有序数组(使用双指针法)
- 找到合并后数组的中位数
2. 完整代码
python
def findMedianSortedArrays_merge(nums1, nums2):
"""
合并数组法
时间复杂度:O(m+n),空间复杂度:O(m+n)
不符合题目要求的 O(log(m+n)),但简单易懂
"""
# 合并两个有序数组
merged = []
i, j = 0, 0
while i < len(nums1) and j < len(nums2):
if nums1[i] <= nums2[j]:
merged.append(nums1[i])
i += 1
else:
merged.append(nums2[j])
j += 1
# 添加剩余元素
if i < len(nums1):
merged.extend(nums1[i:])
if j < len(nums2):
merged.extend(nums2[j:])
# 计算中位数
n = len(merged)
if n % 2 == 1: # 奇数个元素
return float(merged[n // 2])
else: # 偶数个元素
mid1 = merged[n // 2 - 1]
mid2 = merged[n // 2]
return (mid1 + mid2) / 2.0
3. 代码详解
python
# 步骤1:双指针合并
i, j = 0, 0 # 分别指向nums1和nums2的起始位置
merged = [] # 合并后的数组
while i < len(nums1) and j < len(nums2):
if nums1[i] <= nums2[j]:
merged.append(nums1[i]) # 取较小的
i += 1 # 移动nums1指针
else:
merged.append(nums2[j]) # 取较小的
j += 1 # 移动nums2指针
# 为什么用while而不是for?
# 因为两个数组长度不同,需要分别移动指针
# 步骤2:处理剩余元素
if i < len(nums1):
merged.extend(nums1[i:]) # nums1还有剩余
if j < len(nums2):
merged.extend(nums2[j:]) # nums2还有剩余
# 步骤3:计算中位数
n = len(merged)
# 情况1:奇数长度
# 例子:[1,2,3,4,5],n=5,索引:0,1,2,3,4
# 中位数索引 = n//2 = 5//2 = 2 → merged[2]=3
if n % 2 == 1:
return float(merged[n // 2])
# 情况2:偶数长度
# 例子:[1,2,3,4],n=4,索引:0,1,2,3
# 中位数索引 = (n//2-1 和 n//2) = (1,2) → merged[1]=2, merged[2]=3
# 平均值 = (2+3)/2 = 2.5
else:
mid1 = merged[n // 2 - 1]
mid2 = merged[n // 2]
return (mid1 + mid2) / 2.0
4. 可视化示例
nums1: [1, 3, 5] i=0 ↑
nums2: [2, 4, 6] j=0 ↑
merged: []
第1步: 比较1和2 → 1较小 → merged=[1], i=1
第2步: 比较3和2 → 2较小 → merged=[1,2], j=1
第3步: 比较3和4 → 3较小 → merged=[1,2,3], i=2
第4步: 比较5和4 → 4较小 → merged=[1,2,3,4], j=2
第5步: nums2遍历完,添加nums1剩余[5] → merged=[1,2,3,4,5]
长度n=5是奇数,中位数=merged[2]=3.0
三、方法2:二分查找法(满足O(log(m+n)))
1. 算法思路(核心)
我们不合并数组,而是直接寻找第k小的元素:
- 中位数就是第 (m+n)/2 小的元素(奇数情况)
- 或者第 (m+n)/2 和 (m+n)/2+1 小的元素的平均值(偶数情况)
关键转换:找两个有序数组的中位数 → 找两个有序数组的第k小元素
2. 二分查找原理
我们要在两个有序数组中找第k小的元素:
- 比较
nums1[k/2-1]和nums2[k/2-1] - 如果
nums1[k/2-1]更小,说明nums1[0]到nums1[k/2-1]都不可能是第k小的元素 - 排除这部分元素,在剩余部分继续查找第
k - k/2小的元素 - 递归进行,直到k=1
3. 完整代码
python
def findMedianSortedArrays_binary(nums1, nums2):
"""
二分查找法(递归)
时间复杂度:O(log(m+n)),空间复杂度:O(log(m+n))(递归栈)
"""
def find_kth(nums1, i, nums2, j, k):
"""
在nums1[i:]和nums2[j:]中寻找第k小的元素
"""
# 如果nums1为空,直接返回nums2的第k小
if i >= len(nums1):
return nums2[j + k - 1]
# 如果nums2为空,直接返回nums1的第k小
if j >= len(nums2):
return nums1[i + k - 1]
# 如果k=1,返回两个数组当前元素中较小的一个
if k == 1:
return min(nums1[i], nums2[j])
# 计算要比较的索引(注意防止数组越界)
mid_k = k // 2
index1 = min(i + mid_k - 1, len(nums1) - 1)
index2 = min(j + mid_k - 1, len(nums2) - 1)
# 比较两个元素
if nums1[index1] <= nums2[index2]:
# 排除nums1的前mid_k个元素
return find_kth(nums1, index1 + 1, nums2, j, k - (index1 - i + 1))
else:
# 排除nums2的前mid_k个元素
return find_kth(nums1, i, nums2, index2 + 1, k - (index2 - j + 1))
m, n = len(nums1), len(nums2)
total = m + n
# 根据总长度的奇偶性计算中位数
if total % 2 == 1: # 奇数
return float(find_kth(nums1, 0, nums2, 0, total // 2 + 1))
else: # 偶数
left = find_kth(nums1, 0, nums2, 0, total // 2)
right = find_kth(nums1, 0, nums2, 0, total // 2 + 1)
return (left + right) / 2.0
4. 二分查找代码详解
python
def find_kth(nums1, i, nums2, j, k):
"""
核心递归函数:在nums1[i:]和nums2[j:]中找第k小的元素
"""
# 边界情况1:一个数组已经遍历完
if i >= len(nums1):
return nums2[j + k - 1] # 直接在nums2中找
if j >= len(nums2):
return nums1[i + k - 1] # 直接在nums1中找
# 边界情况2:k=1,找最小的元素
if k == 1:
return min(nums1[i], nums2[j]) # 比较两个数组的第一个元素
# 正常情况:比较两个数组的第k/2个元素
mid_k = k // 2 # 每次排除mid_k个元素
# 计算索引(防止数组越界)
index1 = min(i + mid_k - 1, len(nums1) - 1)
index2 = min(j + mid_k - 1, len(nums2) - 1)
if nums1[index1] <= nums2[index2]:
# nums1的前mid_k个元素都不可能是第k小的元素
# 排除这些元素,在剩余部分找第(k - 排除数量)小的元素
excluded = index1 - i + 1 # 实际排除的元素数量
return find_kth(nums1, index1 + 1, nums2, j, k - excluded)
else:
# nums2的前mid_k个元素都不可能是第k小的元素
excluded = index2 - j + 1 # 实际排除的元素数量
return find_kth(nums1, i, nums2, index2 + 1, k - excluded)
5. 可视化示例
nums1: [1, 3, 5, 7]
nums2: [2, 4, 6, 8]
总长度 m+n=8,要找第4和第5小的元素(偶数情况)
找第4小的元素:
初始:k=4, i=0, j=0
mid_k = 4//2 = 2
比较 nums1[1]=3 和 nums2[1]=4
3 < 4,排除nums1的前2个元素 [1,3]
现在:k=4-2=2, i=2, j=0
比较 nums1[3]=7 和 nums2[1]=4
4 < 7,排除nums2的前2个元素 [2,4]
现在:k=2-2=0? 不对,实际排除2个,k=2-2=0
等等,这里有问题...
让我们重新推导:
第一次排除后,k应该变为 k - 实际排除数量 = 4 - 2 = 2
第二次排除后,k = 2 - 2 = 0
但k=0时应该返回什么?
实际上,算法不会让k变为0,因为每次排除mid_k个元素后,k会减少排除的数量
当k减少到1时,直接比较两个数组当前的最小值
正确流程:
第一次:排除nums1的2个元素,k=4-2=2
第二次:排除nums2的2个元素,k=2-2=0
但k=0不符合条件,实际应该是在k=1时结束
调整索引计算:
index1 = min(i + mid_k - 1, len(nums1)-1) = min(0+2-1, 3) = 1
实际排除数量 = index1 - i + 1 = 1 - 0 + 1 = 2
新k = 4 - 2 = 2
i变为 index1+1 = 2
继续...
四、方法3:更优的二分查找(边界划分法)
1. 算法思路
在较短数组上进行二分查找,找到合适的分割位置,使得:
- 左半部分的元素个数 = 右半部分的元素个数(或左半部分多1个)
- 左半部分的最大值 <= 右半部分的最小值
2. 完整代码
python
def findMedianSortedArrays(nums1, nums2):
"""
边界划分法(最优解)
时间复杂度:O(log(min(m,n))),空间复杂度:O(1)
"""
# 确保nums1是较短的数组
if len(nums1) > len(nums2):
nums1, nums2 = nums2, nums1
m, n = len(nums1), len(nums2)
total = m + n
# 在nums1上进行二分查找
left, right = 0, m
while left <= right:
# i: nums1的分割点(左边有i个元素)
# j: nums2的分割点(左边有j个元素)
i = (left + right) // 2
j = (total + 1) // 2 - i # 保证左半部分比右半部分多1个或相等
# nums1左边最大值(如果左边没有元素,设为负无穷)
nums1_left_max = float('-inf') if i == 0 else nums1[i - 1]
# nums1右边最小值(如果右边没有元素,设为正无穷)
nums1_right_min = float('inf') if i == m else nums1[i]
# nums2左边最大值
nums2_left_max = float('-inf') if j == 0 else nums2[j - 1]
# nums2右边最小值
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 total % 2 == 1:
# 奇数:左半部分的最大值
return float(max(nums1_left_max, nums2_left_max))
else:
# 偶数:左半部分最大值和右半部分最小值的平均值
left_max = max(nums1_left_max, nums2_left_max)
right_min = min(nums1_right_min, nums2_right_min)
return (left_max + right_min) / 2.0
elif nums1_left_max > nums2_right_min:
# nums1左边太大,需要减小i(向左移动分割点)
right = i - 1
else:
# nums1左边太小,需要增大i(向右移动分割点)
left = i + 1
# 理论上不会执行到这里
return 0.0
3. 边界划分法详解
核心思想
我们要找到两个分割点 i 和 j,使得:
nums1: [0 ... i-1] | [i ... m-1]
nums2: [0 ... j-1] | [j ... n-1]
满足条件:
1. 左半部分总元素数 = 右半部分总元素数(或左半部分多1个)
i + j = (m + n + 1) // 2
2. 所有左半部分的元素 <= 所有右半部分的元素
max(nums1[i-1], nums2[j-1]) <= min(nums1[i], nums2[j])
为什么这样可以找到中位数?
因为中位数就是左半部分的最大值(奇数情况),或者是左半部分最大值和右半部分最小值的平均值(偶数情况)。
可视化示例
nums1: [1, 3, 5]
nums2: [2, 4, 6, 8]
m=3, n=4, total=7
我们要找 i 和 j,使得:
i + j = (7+1)//2 = 4
尝试 i=1, j=3:
nums1左: [1], 右: [3,5]
nums2左: [2,4], 右: [6,8]
条件检查:
左半部分最大值 = max(1,4)=4
右半部分最小值 = min(3,6)=3
4 > 3 ❌ 不满足
尝试 i=2, j=2:
nums1左: [1,3], 右: [5]
nums2左: [2], 右: [4,6,8]
条件检查:
左半部分最大值 = max(3,2)=3
右半部分最小值 = min(5,4)=4
3 <= 4 ✅ 满足!
总长度7是奇数,中位数 = 左半部分最大值 = 3.0
五、力扣对应题目
1. 力扣 4. 寻找两个正序数组的中位数 ⭐⭐⭐⭐⭐
- 链接:https://leetcode.cn/problems/median-of-two-sorted-arrays/
- 难度:困难
- 要求:时间复杂度 O(log(m+n))
- 推荐解法:边界划分法(二分查找)
python
class Solution:
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
# 确保nums1是较短的数组
if len(nums1) > len(nums2):
nums1, nums2 = nums2, nums1
m, n = len(nums1), len(nums2)
total = m + n
half = (total + 1) // 2 # 左半部分应有的长度
# 在nums1上二分查找
left, right = 0, m
while left <= right:
i = (left + right) // 2 # nums1的分割点
j = half - i # nums2的分割点
# 处理边界情况
nums1_left = float('-inf') if i == 0 else nums1[i-1]
nums1_right = float('inf') if i == m else nums1[i]
nums2_left = float('-inf') if j == 0 else nums2[j-1]
nums2_right = float('inf') if j == n else nums2[j]
# 检查分割点是否正确
if nums1_left <= nums2_right and nums2_left <= nums1_right:
# 找到了正确的分割点
if total % 2 == 1:
return max(nums1_left, nums2_left)
else:
return (max(nums1_left, nums2_left) + min(nums1_right, nums2_right)) / 2
elif nums1_left > nums2_right:
right = i - 1 # i太大,向左移动
else:
left = i + 1 # i太小,向右移动
# 理论上不会执行到这里
return 0.0
六、相关练习题
1. 力扣 295. 数据流的中位数 ⭐⭐⭐
- 链接:https://leetcode.cn/problems/find-median-from-data-stream/
- 题目 :设计一个支持以下两种操作的数据结构:
void addNum(int num)- 添加一个整数到数据结构中double findMedian()- 返回所有元素的中位数
- 解法:使用两个堆(大顶堆+小顶堆)
python
import heapq
class MedianFinder:
def __init__(self):
# 大顶堆(存储较小的一半)
self.max_heap = [] # Python默认小顶堆,所以存负数
# 小顶堆(存储较大的一半)
self.min_heap = []
def addNum(self, num: int) -> None:
# 先加入大顶堆,然后平衡两个堆
heapq.heappush(self.max_heap, -num)
# 确保大顶堆的最大值 <= 小顶堆的最小值
if self.max_heap and self.min_heap and -self.max_heap[0] > self.min_heap[0]:
heapq.heappush(self.min_heap, -heapq.heappop(self.max_heap))
# 平衡两个堆的大小
if len(self.max_heap) > len(self.min_heap) + 1:
heapq.heappush(self.min_heap, -heapq.heappop(self.max_heap))
elif len(self.min_heap) > len(self.max_heap):
heapq.heappush(self.max_heap, -heapq.heappop(self.min_heap))
def findMedian(self) -> float:
if len(self.max_heap) > len(self.min_heap):
return -self.max_heap[0]
else:
return (-self.max_heap[0] + self.min_heap[0]) / 2
2. 力扣 480. 滑动窗口中位数 ⭐⭐⭐⭐
- 链接:https://leetcode.cn/problems/sliding-window-median/
- 题目:给你一个数组和滑动窗口的大小k,返回滑动窗口中位数的数组
- 解法:维护两个堆(大顶堆+小顶堆),但需要支持删除操作
3. 力扣 462. 最少移动次数使数组元素相等 II ⭐⭐
- 链接:https://leetcode.cn/problems/minimum-moves-to-equal-array-elements-ii/
- 题目:找到使所有数组元素相等的最小移动次数
- 关键:中位数是最优目标值
python
class Solution:
def minMoves2(self, nums: List[int]) -> int:
# 排序后取中位数
nums.sort()
median = nums[len(nums) // 2]
# 计算每个元素到中位数的距离之和
moves = 0
for num in nums:
moves += abs(num - median)
return moves
七、常见错误与调试
错误1:索引越界
python
# ❌ 错误:直接访问索引,未考虑边界
nums1_left = nums1[i-1] # 如果i=0,会报错
nums1_right = nums1[i] # 如果i=len(nums1),会报错
# ✅ 正确:处理边界情况
nums1_left = float('-inf') if i == 0 else nums1[i-1]
nums1_right = float('inf') if i == len(nums1) else nums1[i]
错误2:整数除法问题
python
# ❌ 错误:整数除法丢失精度
median = (mid1 + mid2) / 2 # Python3中是浮点数,但最好明确
# ✅ 正确:明确使用浮点数除法
median = (mid1 + mid2) / 2.0 # 或 float(mid1 + mid2) / 2
错误3:忘记处理空数组
python
# ❌ 错误:假设两个数组都不为空
if nums1[0] <= nums2[0]:
# ✅ 正确:先检查数组是否为空
if not nums1:
return findMedianInSingleArray(nums2)
if not nums2:
return findMedianInSingleArray(nums1)
八、复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|
| 合并数组法 | O(m+n) | O(m+n) | 简单易懂,不符合题目要求 |
| 二分查找(递归) | O(log(m+n)) | O(log(m+n)) | 符合要求,但递归有栈开销 |
| 边界划分法 | O(log(min(m,n))) | O(1) | 最优解,常数空间 |
九、面试技巧
1. 解题思路
- 先提出简单解法(合并数组法),分析其复杂度
- 提出需要优化到 O(log(m+n))
- 解释转换为"找第k小元素"的思路
- 实现二分查找解法
- 可以进一步优化到 O(log(min(m,n)))
2. 常见问题
Q:为什么时间复杂度要是 O(log(m+n))?
A:因为两个数组都是有序的,应该利用这个性质。线性扫描是 O(m+n),但有序性提示我们可以用二分查找。
Q:如何处理边界情况?
A:需要考虑数组为空、数组长度不同、索引越界等情况。使用哨兵值(正无穷/负无穷)可以简化边界处理。
Q:为什么选择在较短的数组上进行二分查找?
A:因为二分查找的时间复杂度是 O(log(min(m,n))),比 O(log(m+n)) 更好。在较短的数组上查找,可以减少查找范围。
十、练习题
练习1:实现合并数组法并测试
python
def test_merge_method():
test_cases = [
([1, 3], [2], 2.0),
([1, 2], [3, 4], 2.5),
([0, 0], [0, 0], 0.0),
([], [1], 1.0),
([2], [], 2.0),
([1, 3, 5], [2, 4, 6], 3.5),
]
for nums1, nums2, expected in test_cases:
result = findMedianSortedArrays_merge(nums1, nums2)
print(f"{nums1} + {nums2} → {result} {'✓' if result == expected else '✗'}")
练习2:实现二分查找法
python
def findKthElement(nums1, nums2, k):
"""在两个有序数组中找第k小的元素"""
# 你的实现
pass
练习3:处理特殊情况
python
def findMedianWithDuplicates(nums1, nums2):
"""处理有重复元素的情况"""
# 提示:大部分算法可以直接处理重复元素
pass
总结
寻找两个有序数组中位数的核心:
- 简单方法:合并数组,时间复杂度 O(m+n)
- 优化方法:二分查找,时间复杂度 O(log(m+n))
- 最优方法:边界划分,时间复杂度 O(log(min(m,n)))
关键转换:
- 中位数问题 → 第k小元素问题
- 二分查找时,每次排除 k/2 个元素
- 边界划分时,找到合适的分割点
面试重点:
- 理解不同方法的复杂度
- 能够处理各种边界情况
- 清晰解释算法思路
- 写出正确且高效的代码