(#数组/链表操作)寻找两个正序数组的中位数

一、问题理解

1. 题目描述

给定两个大小分别为 mn 的正序(从小到大)数组 nums1nums2,请找出这两个有序数组的中位数 ,并要求算法的时间复杂度为 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. 算法思路

  1. 合并两个有序数组(使用双指针法)
  2. 找到合并后数组的中位数

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小的元素:

  1. 比较 nums1[k/2-1]nums2[k/2-1]
  2. 如果 nums1[k/2-1] 更小,说明 nums1[0]nums1[k/2-1] 都不可能是第k小的元素
  3. 排除这部分元素,在剩余部分继续查找第 k - k/2 小的元素
  4. 递归进行,直到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. 左半部分的元素个数 = 右半部分的元素个数(或左半部分多1个)
  2. 左半部分的最大值 <= 右半部分的最小值

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. 边界划分法详解

核心思想

我们要找到两个分割点 ij,使得:

复制代码
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. 寻找两个正序数组的中位数 ⭐⭐⭐⭐⭐

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/
  • 题目 :设计一个支持以下两种操作的数据结构:
    1. void addNum(int num) - 添加一个整数到数据结构中
    2. 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. 滑动窗口中位数 ⭐⭐⭐⭐

3. 力扣 462. 最少移动次数使数组元素相等 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. 解题思路

  1. 先提出简单解法(合并数组法),分析其复杂度
  2. 提出需要优化到 O(log(m+n))
  3. 解释转换为"找第k小元素"的思路
  4. 实现二分查找解法
  5. 可以进一步优化到 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

总结

寻找两个有序数组中位数的核心:

  1. 简单方法:合并数组,时间复杂度 O(m+n)
  2. 优化方法:二分查找,时间复杂度 O(log(m+n))
  3. 最优方法:边界划分,时间复杂度 O(log(min(m,n)))

关键转换

  • 中位数问题 → 第k小元素问题
  • 二分查找时,每次排除 k/2 个元素
  • 边界划分时,找到合适的分割点

面试重点

  1. 理解不同方法的复杂度
  2. 能够处理各种边界情况
  3. 清晰解释算法思路
  4. 写出正确且高效的代码
相关推荐
数据知道1 小时前
PostgreSQL 实战:详解 UPSERT(INSERT ON CONFLICT)
数据库·python·postgresql
通信小呆呆2 小时前
DDMA MIMO OFDM ISAC:从回波模型到距离-速度图与非相参积累的原理梳理
算法·信息与通信·ofdm·mimo·通信感知一体化
李昊哲小课2 小时前
奶茶店销售额预测模型
python·机器学习·线性回归·scikit-learn
leo__5202 小时前
电动助力转向(EPS)系统Simulink模型构建与应用
算法
电商API&Tina2 小时前
电商API接口的应用与简要分析||taobao|jd|微店
大数据·python·数据分析·json
TracyCoder1232 小时前
LeetCode Hot100(8/100)—— 438. 找到字符串中所有字母异位词
算法·leetcode
郝学胜-神的一滴2 小时前
深入理解Linux套接字(Socket)编程:从原理到实践
linux·服务器·开发语言·网络·c++·程序人生·算法
向前V2 小时前
Flutter for OpenHarmony轻量级开源记事本App实战:笔记编辑器
开发语言·笔记·python·flutter·游戏·开源·编辑器