题目描述

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

image-20260928190743064

image-20260928190743065

题意分析

给定两个各自按非递减顺序排列的数组,返回全部元素按顺序排列后的中位数。总长度为奇数时,中位数是正中间的一个数;为偶数时,是中间两个数的平均值。重复元素也分别占据一个位置,不能去重。

两个数组可以有一个为空,但不会同时为空。题目要求对数时间复杂度,因此不能先完整合并或重新排序;我们只需要确定中间位置的值,不需要得到整个有序数组。

解法:二分划分较短数组

核心思路

[!blue]

中位数是有序序列的中间边界。设两个数组长度分别为 m、n,合并后的左半部分固定包含 leftSize = (m + n + 1) / 2 个元素:总长度为偶数时左右一样多,为奇数时左侧多一个。只要左侧所有数都不大于右侧所有数,奇数时取左侧最大值,偶数时取左侧最大值与右侧最小值的平均值。

不必真正合并数组。分别从两个数组取一段前缀放入左侧,其余后缀放入右侧。i 表示第一个数组分给左侧的元素个数,j 表示第二个数组分给左侧的元素个数;左侧数量固定,因此选择 i 后,j = leftSize - i 也随之确定。切点是元素个数,可以取 0 或数组长度,并不一定指向一个实际元素。完整有序序列在中间切开后,左侧必然也能表示成这两个数组的前缀,因此一定存在合法划分。

每个数组内部已经有序,所以同一数组的左侧元素一定不大于它的右侧元素。剩下只需检查两组交叉边界:nums1Left <= nums2Right 和 nums2Left <= nums1Right。这里 Left 是相应前缀的最后一个数,Right 是相应后缀的第一个数。两条不等式同时成立,就能保证整个左半部分不大于整个右半部分。

如果 nums1Left > nums2Right,说明第一个数组放到左侧的数太多了。继续增大 i 会让 nums1Left 更大或不变,同时让 j 减小、nums2Right 更小或不变,矛盾不可能消失,因此只能减小 i。反过来,若 nums2Left > nums1Right,说明第一个数组放到左侧的数太少,只能增大 i。这种单调性允许在 [0, m] 中二分切点。

先保证第一个数组较短,即 m <= n。此时 m <= leftSize <= n,所以任意 0 <= i <= m 都有 0 <= j <= n,另一数组的切点不会越界。某侧没有元素时,左边界用负无穷、右边界用正无穷代替;代码用整数最小值和最大值充当哨兵,统一处理空数组和端点切割。

解题步骤

  1. 若第一个数组更长,交换两个数组的角色;记较短数组长度为 m,另一个为 n。
  2. 固定 leftSize = (m + n + 1) / 2,在闭区间 [0, m] 中二分 i,用 j = leftSize - i 确定另一个切点。
  3. 读取两个切点左右的四个边界值;空前缀使用负无穷,空后缀使用正无穷。
  4. 两条交叉不等式均成立时,得到合法划分。总长度为奇数就返回两侧左边界的最大值;为偶数就再取右边界的最小值,求两者平均。
  5. 若 nums1Left > nums2Right,令 right = i - 1;否则是第二条不等式不成立,令 left = i + 1,继续二分。

代码实现

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        // 只在较短数组中选择切点,才能让另一侧划分始终落在合法范围。
        if (nums1.length > nums2.length) {
            return findMedianSortedArrays(nums2, nums1);
        }

        int m = nums1.length;
        int n = nums2.length;
        int leftSize = (m + n + 1) / 2;
        int left = 0;
        int right = m;

        while (left <= right) {
            int i = left + (right - left) / 2;
            // 左侧总数量固定,较短数组确定划分后另一侧随之确定。
            int j = leftSize - i;
            int nums1Left = i == 0 ? Integer.MIN_VALUE : nums1[i - 1];
            int nums1Right = i == m ? Integer.MAX_VALUE : nums1[i];
            int nums2Left = j == 0 ? Integer.MIN_VALUE : nums2[j - 1];
            int nums2Right = j == n ? Integer.MAX_VALUE : nums2[j];

            // 数组内部已有序,只需检查两组交叉边界。
            if (nums1Left <= nums2Right && nums2Left <= nums1Right) {
                int leftMax = Math.max(nums1Left, nums2Left);

                if ((m + n) % 2 == 1) {
                    return leftMax;
                }

                int rightMin = Math.min(nums1Right, nums2Right);

                // 求和之前转换,避免窄整数相加先溢出。
                return ((double) leftMax + rightMin) / 2.0;
            }

            if (nums1Left > nums2Right) {
                right = i - 1;
            } else {
                left = i + 1;
            }
        }

        return 0.0;
    }
}
func findMedianSortedArrays(nums1 []int, nums2 []int) float64 {
    // 只在较短数组中选择切点,才能让另一侧划分始终落在合法范围。
    if len(nums1) > len(nums2) {
        return findMedianSortedArrays(nums2, nums1)
    }

    m := len(nums1)
    n := len(nums2)
    leftSize := (m + n + 1) / 2
    left := 0
    right := m
    maxSentinel := int(^uint(0) >> 1)
    minSentinel := -maxSentinel - 1

    for left <= right {
        i := left + (right-left)/2
        // 左侧总数量固定,较短数组确定划分后另一侧随之确定。
        j := leftSize - i
        // 缺失的左邻居用最小值、右邻居用最大值,统一处理边界划分。
        nums1Left := minSentinel
        nums1Right := maxSentinel
        nums2Left := minSentinel
        nums2Right := maxSentinel

        if i > 0 {
            nums1Left = nums1[i-1]
        }
        if i < m {
            nums1Right = nums1[i]
        }
        if j > 0 {
            nums2Left = nums2[j-1]
        }
        if j < n {
            nums2Right = nums2[j]
        }

        // 数组内部已有序,只需检查两组交叉边界。
        if nums1Left <= nums2Right && nums2Left <= nums1Right {
            leftMax := maxInt(nums1Left, nums2Left)
            if (m+n)%2 == 1 {
                return float64(leftMax)
            }

            rightMin := minInt(nums1Right, nums2Right)
            // 求和之前转换,避免整数相加先溢出。
            return (float64(leftMax) + float64(rightMin)) / 2.0
        }

        if nums1Left > nums2Right {
            right = i - 1
        } else {
            left = i + 1
        }
    }

    return 0.0
}

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

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

复杂度分析

  • 时间复杂度:$O(\log(m + 1))$,m 为较短数组长度,二分候选切点共 m + 1 个,每次只比较四个边界值。较短数组为空时为 $O(1)$。
  • 空间复杂度:$O(1)$,只保存切点和边界值;交换数组角色最多发生一次,不会产生随输入增长的递归栈。

关键点总结

[!green]

  • 划分同时满足“左侧数量正确”和“左侧所有值不大于右侧”,才足以确定中位数。
  • 数组内部的顺序已由输入保证,只需要核对两组交叉边界。
  • 二分调整的是第一个数组贡献给左侧的数量,第二个数组的贡献随之反向变化。
  • 较短数组保证另一个切点合法,端点哨兵让空分区沿用同一组比较。

易错点总结

[!yellow]

  • 把切点范围写成 [0, m - 1] 会漏掉整个数组都放入左侧的情况;切点共有 m + 1 个。
  • 未先选择短数组,按 j = leftSize - i 得到的下标可能超出第二个数组范围。
  • 使用左侧最大值作为奇数答案时,左侧必须多一个元素,不能把 leftSize 写成 (m + n) / 2。
  • 只检查一组交叉边界,不能保证两个数组合起来仍然左小右大。
  • 偶数答案必须用浮点除法,且应在相加前转为浮点数,避免整数求和溢出。

相似题目

题目 难度 关联与区别
补充题 17. 两个有序数组第 k 小的数 困难 中位数可转化为一个或两个第k小值查询,复用二分淘汰或分割思路。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/26276715
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!