4. 寻找两个正序数组的中位数:用分割点代替合并

题目

给定两个大小分别为 mn 的正序数组 nums1nums2,返回两个数组合并后的中位数。

题目要求时间复杂度为 O(log(m+n))

例如:

复制代码
nums1 = [1, 3]
nums2 = [2]

合并后:[1, 2, 3]
中位数:2

初始思路为什么不满足要求

把两个数组拼接起来,再排序,时间复杂度至少是 O((m+n)log(m+n))

即使使用双指针在线性时间内完成合并,也需要 O(m+n),仍然没有达到题目要求的对数复杂度。

因此,关键是不要真正构造合并后的数组,而是直接确定中位数左右两侧的边界。

核心观察:寻找两个分割点

把两个数组分别切成左右两部分:

复制代码
nums1: [左半边] | [右半边]
nums2: [左半边] | [右半边]

i 表示从 nums1 中取多少个元素放入左半边,j 表示从 nums2 中取多少个元素放入左半边。

如果总长度为 m+n,令:

复制代码
half = (m + n + 1) / 2

为了保证左半边元素数量正确,需要满足:

复制代码
i + j = half
j = half - i

这里的 ij 是元素数量,不是最后一个元素的下标。

例如:

复制代码
nums1 = [1, 4, 7, 10]
nums2 = [2, 3, 5, 6, 8, 9]
half = 5

如果 i=2,那么 j=3

复制代码
nums1: [1, 4]    | [7, 10]
nums2: [2, 3, 5] | [6, 8, 9]

左半边一共有 2+3=5 个元素。

有效分割点的条件

两个数组各自有序,所以只需要检查分割点附近的四个边界值:

复制代码
left1  = nums1[i - 1]   // nums1 左半边最大值
right1 = nums1[i]       // nums1 右半边最小值
left2  = nums2[j - 1]   // nums2 左半边最大值
right2 = nums2[j]       // nums2 右半边最小值

有效分割必须满足:

复制代码
left1 <= right2
left2 <= right1

这表示左半边的所有元素都不大于右半边的所有元素。

当分割点位于数组两端时,用无穷边界处理:

复制代码
i == 0:left1  = 负无穷
i == m:right1 = 正无穷
j == 0:left2  = 负无穷
j == n:right2 = 正无穷

为什么只二分较短的数组

先保证 nums1 是较短数组:

复制代码
if (nums1.length > nums2.length) {
    交换 nums1 和 nums2;
}

这样二分范围只有 [0,m],并且根据 j=half-i 推出的 j 一定落在 nums2 的合理范围内,同时可以避免较短数组的分割位置越界。

开区间二分

我们寻找第一个满足下面条件的 i

复制代码
nums1[i] >= nums2[j - 1]

使用开区间哨兵:

复制代码
int left = -1;
int right = m;

其中 left 表示条件不满足的位置,right 表示条件满足的位置或右侧哨兵。每轮计算:

复制代码
int i = left + (right - left) / 2;
int j = half - i;

然后根据 nums1[i]nums2[j-1] 的关系更新边界:

  • 如果 nums1[i] < nums2[j-1],说明 nums1 左半边取少了,i 应该增大:left = i
  • 否则当前 i 可能已经足够,继续向左寻找第一个满足条件的位置:right = i

循环结束时,right 就是最终的 i

手动模拟

仍以:

复制代码
nums1 = [1, 4, 7, 10]
nums2 = [2, 3, 5, 6, 8, 9]
half = 5

为例:

复制代码
left = -1, right = 4
i = 1, j = 4
nums1[i] = 4 < nums2[j-1] = 6
说明 nums1 左边取少了:left = 1

下一轮:

复制代码
left = 1, right = 4
i = 2, j = 3
nums1[i] = 7 >= nums2[j-1] = 5
说明 i 可能已经足够:right = 2

此时 left + 1 == right,二分结束,最终分割为:

复制代码
nums1: [1, 4]    | [7, 10]
nums2: [2, 3, 5] | [6, 8, 9]

左半边最大值为 max(4,5)=5,右半边最小值为 min(7,6)=6,所以中位数为:

复制代码
(5 + 6) / 2 = 5.5

代码实现

复制代码
class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        if (nums1.length > nums2.length) {
            int[] temp = nums1;
            nums1 = nums2;
            nums2 = temp;
        }

        int m = nums1.length;
        int n = nums2.length;
        int half = (m + n + 1) / 2;

        int left = -1;
        int right = m;

        while (left + 1 < right) {
            int i = left + (right - left) / 2;
            int j = half - i;

            if (nums1[i] < nums2[j - 1]) {
                left = i;
            } else {
                right = i;
            }
        }

        int i = right;
        int j = half - i;

        int left1 = i == 0 ? Integer.MIN_VALUE : nums1[i - 1];
        int right1 = i == m ? Integer.MAX_VALUE : nums1[i];
        int left2 = j == 0 ? Integer.MIN_VALUE : nums2[j - 1];
        int right2 = j == n ? Integer.MAX_VALUE : nums2[j];

        int maxLeft = Math.max(left1, left2);

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

        int minRight = Math.min(right1, right2);
        return ((double) maxLeft + minRight) / 2.0;
    }
}

复杂度分析

只在较短数组上进行二分,搜索范围为 m,所以时间复杂度为:

复制代码
O(log(min(m,n)))

它满足题目要求的 O(log(m+n))。算法只使用常量级变量,空间复杂度为:

复制代码
O(1)

常见错误

  1. 奇数长度时,中位数应为左半边最大值 max(left1,left2),不能误用右半边的 right1
  2. right1 必须来自 nums1[i]left2 必须来自 nums2[j-1]
  3. 计算偶数长度中位数时,先转为 double,避免两个 int 相加溢出。

总结

这道题的关键不是合并数组,而是寻找一个分割,使得:

复制代码
i + j = half
left1 <= right2
left2 <= right1

只要左半边和右半边的数量、顺序都正确,就可以直接通过四个边界值得到中位数。

相关推荐
CarIise4 小时前
C语言结构体与链表
数据结构·算法
晚风醉蝶4 小时前
1-18-基数排序-RadixSort
python·算法·排序算法·基数排序
lch2011_yb5 小时前
CSP-J 2022 上升点列 题解
算法·深度优先
lvts_cs5 小时前
化工产业规划对城市发展的具体指导作用
算法·动态规划
AI服务老曹5 小时前
水位识别算法接入AI视频分析平台全流程与误报优化指南(附接口字段与排查表)
人工智能·算法·音视频
江畔柳前堤5 小时前
消息队列:从直觉到精通的完整认知地图
开发语言·人工智能·算法·机器学习·ruby on rails·scala
ai产品老杨6 小时前
明火识别算法接入AI视频分析平台全流程与误报优化指南(附接口字段与排查表)
人工智能·算法·音视频
讨厌吃蛋黄酥6 小时前
📚408数据结构|最短路径算法一篇彻底搞懂!BFS、Dijkstra、Floyd别再傻傻分不清了
算法
charlie1145141916 小时前
deque、list 与 forward_list:vector 之外的三个选择
开发语言·数据结构·c++·list·开源项目
动词ing6 小时前
【学习笔记】数据结构(数组滑动窗口+无重复字符的最长字串题目)
数据结构·笔记·学习