(LeetCode-Hot100)4. 寻找两个正序数组的中位数

问题简介

LeetCode 4. 寻找两个正序数组的中位数

题目描述

给定两个大小分别为 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

示例 3:

复制代码
输入:nums1 = [], nums2 = [1]
输出:1.00000

解题思路

💡 方法一:暴力合并(不符合题目要求,但作为理解基础)

  1. 将两个数组合并成一个有序数组
  2. 直接计算中位数
  3. 时间复杂度:O(m + n),空间复杂度:O(m + n)

缺点:不满足 O(log(m+n)) 的时间复杂度要求


💡 方法二:二分查找(推荐解法)

核心思想:将问题转化为寻找第 k 小的元素

步骤详解:
  1. 统一处理奇偶情况

    • 如果总长度为奇数,中位数 = 第 (m+n+1)/2 小的数
    • 如果总长度为偶数,中位数 = (第 (m+n)/2 小的数 + 第 (m+n)/2+1 小的数) / 2
  2. 设计 findKth 函数

    • 在两个有序数组中找到第 k 小的元素
    • 每次比较两个数组的第 k/2 个元素
    • 排除较小的那一部分,因为它们不可能包含第 k 小的元素
  3. 边界处理

    • 当一个数组为空时,直接返回另一个数组的第 k 个元素
    • 当 k = 1 时,返回两个数组首元素的较小值
  4. 递归/迭代实现

    • 通过不断缩小搜索范围,最终找到目标元素

💡 方法三:二分分割点(更优雅的二分法)

核心思想:在较短的数组上进行二分,找到合适的分割点

  1. 确保 nums1 是较短的数组(如果不是则交换)
  2. 在 nums1 上进行二分搜索,寻找分割点 i
  3. 对应的 nums2 分割点为 j = (m + n + 1) / 2 - i
  4. 检查分割是否有效:nums1[i-1] <= nums2[j] && nums2[j-1] <= nums1[i]
  5. 根据奇偶性计算中位数

代码实现

java 复制代码
// Java 实现 - 方法二:二分查找第k小元素
class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int m = nums1.length, n = nums2.length;
        int total = m + n;
        
        if (total % 2 == 1) {
            return findKth(nums1, 0, nums2, 0, total / 2 + 1);
        } else {
            return (findKth(nums1, 0, nums2, 0, total / 2) + 
                    findKth(nums1, 0, nums2, 0, total / 2 + 1)) / 2.0;
        }
    }
    
    private int findKth(int[] nums1, int start1, int[] nums2, int start2, int k) {
        // 确保 nums1 剩余长度 <= nums2 剩余长度
        if (nums1.length - start1 > nums2.length - start2) {
            return findKth(nums2, start2, nums1, start1, k);
        }
        
        // 边界情况:nums1 已经遍历完
        if (start1 >= nums1.length) {
            return nums2[start2 + k - 1];
        }
        
        // 边界情况:k = 1
        if (k == 1) {
            return Math.min(nums1[start1], nums2[start2]);
        }
        
        // 计算比较位置
        int mid1 = Math.min(start1 + k / 2 - 1, nums1.length - 1);
        int mid2 = start2 + k / 2 - 1;
        
        if (nums1[mid1] <= nums2[mid2]) {
            // 排除 nums1 的前半部分
            return findKth(nums1, mid1 + 1, nums2, start2, k - (mid1 - start1 + 1));
        } else {
            // 排除 nums2 的前半部分
            return findKth(nums1, start1, nums2, mid2 + 1, k - k / 2);
        }
    }
}

// Java 实现 - 方法三:二分分割点
class Solution2 {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        if (nums1.length > nums2.length) {
            return findMedianSortedArrays(nums2, nums1);
        }
        
        int m = nums1.length, n = nums2.length;
        int left = 0, right = m;
        int median1 = 0, median2 = 0;
        
        while (left <= right) {
            int i = (left + right) / 2;
            int j = (m + n + 1) / 2 - i;
            
            int nums1LeftMax = (i == 0) ? Integer.MIN_VALUE : nums1[i - 1];
            int nums1RightMin = (i == m) ? Integer.MAX_VALUE : nums1[i];
            int nums2LeftMax = (j == 0) ? Integer.MIN_VALUE : nums2[j - 1];
            int nums2RightMin = (j == n) ? Integer.MAX_VALUE : nums2[j];
            
            if (nums1LeftMax <= nums2RightMin) {
                median1 = Math.max(nums1LeftMax, nums2LeftMax);
                median2 = Math.min(nums1RightMin, nums2RightMin);
                left = i + 1;
            } else {
                right = i - 1;
            }
        }
        
        return (m + n) % 2 == 0 ? (median1 + median2) / 2.0 : median1;
    }
}
go 复制代码
// Go 实现 - 方法二:二分查找第k小元素
func findMedianSortedArrays(nums1 []int, nums2 []int) float64 {
    m, n := len(nums1), len(nums2)
    total := m + n
    
    if total%2 == 1 {
        return float64(findKth(nums1, 0, nums2, 0, total/2+1))
    } else {
        return float64(findKth(nums1, 0, nums2, 0, total/2)+findKth(nums1, 0, nums2, 0, total/2+1)) / 2.0
    }
}

func findKth(nums1 []int, start1 int, nums2 []int, start2 int, k int) int {
    // 确保 nums1 剩余长度 <= nums2 剩余长度
    if len(nums1)-start1 > len(nums2)-start2 {
        return findKth(nums2, start2, nums1, start1, k)
    }
    
    // 边界情况:nums1 已经遍历完
    if start1 >= len(nums1) {
        return nums2[start2+k-1]
    }
    
    // 边界情况:k = 1
    if k == 1 {
        return min(nums1[start1], nums2[start2])
    }
    
    // 计算比较位置
    mid1 := min(start1+k/2-1, len(nums1)-1)
    mid2 := start2 + k/2 - 1
    
    if nums1[mid1] <= nums2[mid2] {
        // 排除 nums1 的前半部分
        return findKth(nums1, mid1+1, nums2, start2, k-(mid1-start1+1))
    } else {
        // 排除 nums2 的前半部分
        return findKth(nums1, start1, nums2, mid2+1, k-k/2)
    }
}

func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

// Go 实现 - 方法三:二分分割点
func findMedianSortedArrays2(nums1 []int, nums2 []int) float64 {
    if len(nums1) > len(nums2) {
        return findMedianSortedArrays2(nums2, nums1)
    }
    
    m, n := len(nums1), len(nums2)
    left, right := 0, m
    var median1, median2 int
    
    for left <= right {
        i := (left + right) / 2
        j := (m + n + 1) / 2 - i
        
        nums1LeftMax := math.MinInt32
        if i > 0 {
            nums1LeftMax = nums1[i-1]
        }
        nums1RightMin := math.MaxInt32
        if i < m {
            nums1RightMin = nums1[i]
        }
        
        nums2LeftMax := math.MinInt32
        if j > 0 {
            nums2LeftMax = nums2[j-1]
        }
        nums2RightMin := math.MaxInt32
        if j < n {
            nums2RightMin = nums2[j]
        }
        
        if nums1LeftMax <= nums2RightMin {
            median1 = max(nums1LeftMax, nums2LeftMax)
            median2 = min(nums1RightMin, nums2RightMin)
            left = i + 1
        } else {
            right = i - 1
        }
    }
    
    if (m+n)%2 == 0 {
        return float64(median1+median2) / 2.0
    }
    return float64(median1)
}

func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

示例演示

📌 以示例1为例:nums1 = [1,3], nums2 = [2]

方法二执行过程:

  1. 总长度 = 3(奇数),需要找第 2 小的元素
  2. 调用 findKth([1,3], 0, [2], 0, 2)
  3. 比较 nums1[0] = 1 和 nums2[0] = 2
  4. 1 < 2,排除 nums1[0],在 [3] 和 [2] 中找第 1 小的元素
  5. 返回 min(3, 2) = 2

方法三执行过程:

  1. nums1 较短,在 nums1 上二分
  2. i = 1, j = (2+1+1)/2 - 1 = 1
  3. 检查分割:nums1[0] = 1 <= nums2[1] = ∞ ✓,nums2[0] = 2 <= nums1[1] = 3 ✓
  4. 左侧最大值 = max(1, 2) = 2,返回 2

答案有效性证明

正确性保证:

  1. 方法二

    • 每次递归都能正确排除 k/2 个不可能的元素
    • 边界条件处理完整
    • 最终必然能找到第 k 小的元素
  2. 方法三

    • 通过二分确保找到正确的分割点
    • 分割后左侧元素数量 = 右侧元素数量(或左侧多1)
    • 中位数由分割点附近的四个值决定

数学归纳法验证

  • 基础情况:当一个数组为空时,结果显然正确
  • 归纳步骤:每次递归/迭代都保持问题规模减小,且不丢失正确答案

复杂度分析

方法 时间复杂度 空间复杂度 是否满足要求
暴力合并 O(m + n) O(m + n)
二分查找第k小 O(log(m + n)) O(log(m + n))
二分分割点 O(log(min(m, n))) O(1)

💡 最优解:方法三的时间复杂度更优,为 O(log(min(m, n)))


问题总结

📌 关键洞察

  • 中位数问题可以转化为寻找第 k 小元素的问题
  • 利用数组有序的特性,可以通过二分法高效排除不可能的区域
  • 选择在较短数组上进行二分可以进一步优化时间复杂度

📌 技巧要点

  1. 边界处理:空数组、k=1 等特殊情况
  2. 索引计算:避免数组越界,使用 min/max 保护
  3. 奇偶统一:通过 (m+n+1)/2 和 (m+n+2)/2 统一处理

📌 面试建议

  • 先给出暴力解法展示思路
  • 再优化到 O(log(m+n)) 的二分解法
  • 重点解释为什么每次可以排除 k/2 个元素
  • 强调边界条件的处理逻辑
相关推荐
追随者永远是胜利者2 小时前
(LeetCode-Hot100)2. 两数相加
java·算法·leetcode·go
初夏睡觉2 小时前
每日一题( P1518 [USACO2.4] 两只塔姆沃斯牛 The Tamworth Two)(第二天)
算法
L_Aria2 小时前
3824. 【NOIP2014模拟9.9】渴
c++·算法·图论
gorgeous(๑>؂<๑)2 小时前
【ICLR26-Oral Paper】透过对比的视角:视觉语言模型中的自改进视觉推理
人工智能·算法·语言模型·自然语言处理
前路不黑暗@2 小时前
Java项目:Java脚手架项目通用基类和常量类的封装(九)
java·spring boot·笔记·学习·spring cloud·maven·intellij-idea
AC赳赳老秦2 小时前
软件组件自动化的革命:DeepSeek 引领高效开发新时代
运维·人工智能·算法·云原生·maven·devops·deepseek
小亮✿2 小时前
并查集OJ做题报告
算法·个人知识总结·做题报告
我命由我123452 小时前
Photoshop - Photoshop 工具栏(61)切片工具
学习·ui·职场和发展·求职招聘·职场发展·学习方法·photoshop
ShineWinsu2 小时前
对于模拟实现C++list类的详细解析—上
开发语言·数据结构·c++·算法·面试·stl·list