Leetcode二分查找(1)

发布于:2025-09-04 ⋅ 阅读:(21) ⋅ 点赞:(0)

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

给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。

算法的时间复杂度应该为 O(log (m+n)) 。

解题思路总览

  1. 完整归并法(暴力合并后取中位)
  2. 双指针部分归并(只走到中位位置)
  3. 二分划分法(对较短数组做二分,O(log(min(m,n))))
  4. 第 k 小元素法(递归缩减 k,O(log(m+n)))

思路一:完整归并法(暴力)

原理与适用场景:
将两个有序数组整体按归并排序方式合并到一个新数组,再取中位数。时间 O(m+n),空间 O(m+n)。实现最直观,适合数据规模小或用于验证其它算法正确性。
实现步骤:

  1. 申请长度 m+n 的临时数组。
  2. 双指针 i,j 同步遍历,依次写入较小值。
  3. 追加尚未耗尽数组的剩余部分。
  4. 根据总长度奇偶返回中位数或两中点平均。
    JAVA 代码实现(逐行中文注释):
public double findMedianSortedArraysMerge(int[] nums1, int[] nums2) {        // 入口:暴力完整归并
    int m = nums1.length;                                                   // 数组1长度
    int n = nums2.length;                                                   // 数组2长度
    int[] merged = new int[m + n];                                          // 新数组存放合并结果
    int i = 0, j = 0, k = 0;                                                // i/j 指向原数组;k 指向合并数组写入位置
    while (i < m && j < n) {                                                // 两数组都还有元素
        if (nums1[i] <= nums2[j]) {                                         // 选择较小(稳定)
            merged[k++] = nums1[i++];                                       // 复制 nums1[i] 并移动指针
        } else {                                                            // 否则选择 nums2[j]
            merged[k++] = nums2[j++];                                       // 复制 nums2[j] 并移动指针
        }
    }
    while (i < m) merged[k++] = nums1[i++];                                 // 复制 nums1 剩余元素
    while (j < n) merged[k++] = nums2[j++];                                 // 复制 nums2 剩余元素
    int total = m + n;                                                      // 合并后总长度
    if ((total & 1) == 1) {                                                 // 若总长度为奇数
        return merged[total / 2];                                           // 直接返回中间位置元素
    } else {                                                                // 若为偶数
        return (merged[total / 2 - 1] + merged[total / 2]) / 2.0;           // 返回两个中点的平均
    }
}

思路二:双指针部分归并

原理与适用场景:
不必完整合并,只需走到中位索引(或两个索引)。遍历过程中记录当前与前一个元素。节省空间为 O(1),时间仍最坏 O(m+n)。适合中等规模、追求更低常数的场景。
实现步骤:

  1. 计算总长度 total,确定需要访问的一个或两个目标位置 target1 / target2。
  2. i,j 指向两数组,循环选择较小元素,模拟归并序列下标 idx++。
  3. 每次迭代更新 prev=cur, cur=新元素。
  4. 当 idx == target2 时,根据奇偶返回结果。
    JAVA 代码实现:
public double findMedianSortedArraysPartial(int[] nums1, int[] nums2) {      // 入口:部分归并到中位
    int m = nums1.length, n = nums2.length;                                  // 记录长度
    int total = m + n;                                                       // 总长度
    int target2 = total / 2;                                                 // 第二目标位置(奇偶通用)
    int target1 = (total % 2 == 0) ? target2 - 1 : target2;                  // 若偶数需要两个位置
    int i = 0, j = 0;                                                        // 两个数组的扫描指针
    int idx = -1;                                                            // 已“取出”的元素在归并序列中的位置
    int prev = 0, cur = 0;                                                   // prev 保存前一个值,cur 保存当前值
    while (i < m || j < n) {                                                 // 只要至少一个数组未耗尽
        int val;                                                             // 当前挑选出的值
        if (i < m && (j >= n || nums1[i] <= nums2[j])) {                     // 选择 nums1 当前元素
            val = nums1[i++];                                                // 取出并前进 i
        } else {                                                             // 否则选择 nums2 当前元素
            val = nums2[j++];                                                // 取出并前进 j
        }
        idx++;                                                               // 归并位置前进
        prev = cur;                                                          // 更新前一个值
        cur = val;                                                           // 设置当前值
        if (idx == target2) {                                                // 已达到较大目标位置
            if ((total & 1) == 1) {                                          // 若总长度奇数
                return cur;                                                  // 直接返回当前值
            } else {                                                         // 若总长度偶数
                return (prev + cur) / 2.0;                                   // 两个中位位置求平均
            }
        }
    }
    return 0.0;                                                              // 理论上不可达
}

思路三:二分划分法(推荐最优)

原理与适用场景:
设在两个数组上各取一条“分割线”,使得左半部分共 (m+n+1)/2 个元素,且 leftMax <= rightMin。只需在较短数组上二分尝试其分割位置 i,同时 j = leftTotal - i 自动确定另一个数组的分割。满足边界条件后即可在 O(1) 时间计算中位数。整体时间 O(log(min(m,n))),空间 O(1)。适合需要满足题目对数级复杂度要求的主流场景。
实现步骤:

  1. 若 nums1 长度大于 nums2,交换以保证 nums1 更短。
  2. 设 leftTotal = (m+n+1)/2,二分 i ∈ [0,m],对应 j = leftTotal - i。
  3. 取 nums1LeftMax, nums1RightMin, nums2LeftMax, nums2RightMin(越界用 ±∞)。
  4. 若 nums1LeftMax > nums2RightMin,说明 i 太大(左侧来自 nums1 的元素多了),hi = i - 1。
  5. 否则若 nums2LeftMax > nums1RightMin,说明 i 太小,lo = i + 1。
  6. 否则找到合法划分:奇数总长返回 maxLeft;偶数返回 (maxLeft + minRight)/2。
    JAVA 代码实现:
public double findMedianSortedArraysBinary(int[] nums1, int[] nums2) {       // 入口:二分划分法
    if (nums1.length > nums2.length) {                                       // 保证 nums1 更短,提高二分效率
        return findMedianSortedArraysBinary(nums2, nums1);                   // 若不满足则交换参数
    }
    int m = nums1.length, n = nums2.length;                                  // 两数组长度
    int leftTotal = (m + n + 1) / 2;                                         // 左侧应包含的总元素数(偏向左)
    int lo = 0, hi = m;                                                      // 在短数组范围内二分
    while (lo <= hi) {                                                       // 标准二分循环
        int i = lo + (hi - lo) / 2;                                          // nums1 左部分取 i 个
        int j = leftTotal - i;                                               // nums2 左部分取 j 个
        int nums1LeftMax = (i == 0) ? Integer.MIN_VALUE : nums1[i - 1];      // i==0 则左侧无元素 => -∞
        int nums1RightMin = (i == m) ? Integer.MAX_VALUE : nums1[i];         // i==m 则右侧无元素 => +∞
        int nums2LeftMax = (j == 0) ? Integer.MIN_VALUE : nums2[j - 1];      // 同理处理边界
        int nums2RightMin = (j == n) ? Integer.MAX_VALUE : nums2[j];         // 同理处理边界
        if (nums1LeftMax > nums2RightMin) {                                  // i 过大,需要左移
            hi = i - 1;                                                     // 调整上界
        } else if (nums2LeftMax > nums1RightMin) {                           // i 过小,需要右移
            lo = i + 1;                                                     // 调整下界
        } else {                                                             // 找到合法划分
            int maxLeft = Math.max(nums1LeftMax, nums2LeftMax);              // 左侧最大值
            if (((m + n) & 1) == 1) {                                        // 若总长度为奇数
                return maxLeft;                                             // 直接返回左侧最大
            } else {                                                         // 若总长度为偶数
                int minRight = Math.min(nums1RightMin, nums2RightMin);       // 右侧最小值
                return (maxLeft + minRight) / 2.0;                           // 平均得到中位数
            }
        }
    }
    return 0.0;                                                              // 理论上不可能执行到此
}

思路四:第 k 小元素法(通用递归)

原理与适用场景:
中位数可视为第 k 小(或两次查询)。递归比较两个数组当前第 k/2 小的候选并丢弃较小侧的前 half 个元素,k 随之减少。每轮至少丢弃 k/2 个元素,总复杂度 O(log(m+n))。可扩展到任意 k 查询。
实现步骤:

  1. 定义 getK(a,i,b,j,k) 返回从 a[i:], b[j:] 合并后的第 k 小。
  2. 若某数组已耗尽,直接在另一数组取第 k 个。
  3. 若 k==1,返回两数组首元素较小者。
  4. half = k/2,计算 a 与 b 对应的比较位置 i+half-1 与 j+half-1,越界视为 +∞。
  5. 丢弃较小值所在数组的前 half 个元素,递归更新 k。
  6. 根据奇偶一次或两次调用得到中位数。
    JAVA 代码实现:
public double findMedianSortedArraysKth(int[] nums1, int[] nums2) {          // 入口:第 k 小模型获得中位数
    int m = nums1.length, n = nums2.length;                                  // 记录两数组长度
    int total = m + n;                                                       // 总元素数
    if ((total & 1) == 1) {                                                  // 若总长度为奇数
        return getKth(nums1, 0, nums2, 0, total / 2 + 1);                    // 取第 (total/2+1) 小
    } else {                                                                 // 若总长度为偶数
        double left = getKth(nums1, 0, nums2, 0, total / 2);                 // 左侧中位位置
        double right = getKth(nums1, 0, nums2, 0, total / 2 + 1);            // 右侧中位位置
        return (left + right) / 2.0;                                         // 平均得到中位数
    }
}

private double getKth(int[] a, int i, int[] b, int j, int k) {               // 递归:返回第 k 小
    if (i >= a.length) return b[j + k - 1];                                  // a 用尽 -> 直接从 b 取
    if (j >= b.length) return a[i + k - 1];                                  // b 用尽 -> 直接从 a 取
    if (k == 1) return Math.min(a[i], b[j]);                                 // k==1 返回两者较小
    int half = k / 2;                                                        // 每轮丢弃至少 half 个
    int iNew = i + half - 1;                                                 // a 的比较索引
    int jNew = j + half - 1;                                                 // b 的比较索引
    int aVal = (iNew >= a.length) ? Integer.MAX_VALUE : a[iNew];             // 越界视作 +∞
    int bVal = (jNew >= b.length) ? Integer.MAX_VALUE : b[jNew];             // 越界视作 +∞
    if (aVal < bVal) {                                                       // a 的前 half 个全都较小
        return getKth(a, i + half, b, j, k - half);                          // 丢弃 a 的 half 个
    } else {                                                                 // 否则丢弃 b 的 half 个
        return getKth(a, i, b, j + half, k - half);                          // 继续递归求解
    }
}

补充说明(方法对比)

时间复杂度:

  • 完整归并:O(m+n)
  • 部分归并:O(m+n)(最坏)
  • 二分划分:O(log(min(m,n)))(题目期望首选)
  • 第 k 小:O(log(m+n))
    空间复杂度:
  • 完整归并:O(m+n)
  • 部分归并:O(1)
  • 二分划分:O(1)
  • 第 k 小:O(log(m+n)) 递归栈(可改迭代降为 O(1))
    优缺点概述:
  • 完整归并:实现最简单;空间 & 线性时间较大,不满足对数复杂度目标。
  • 部分归并:省空间;仍是线性时间,适合快速验证。
  • 二分划分:代码精炼,常量空间,对数时间;需仔细处理边界。
  • 第 k 小:通用可复用到任意排名查询;实现略长,有递归栈。
    边界情况:
  • 一数组为空:全部方法(除完整归并外的优化法)均已显式处理或天然兼容。
  • 元素全部相同:不影响划分条件判定。
  • 长度极端悬殊:二分划分仍只在短数组上二分,性能稳定。
  • 溢出风险:仅在偶数平均时涉及两 int 相加(安全)。若值范围接近上限可先转 long。
    选择建议:
    生产/性能敏感:二分划分;
    需要扩展任意 k:第 k 小;
    教学 / 调试:部分归并;
    快速草稿:完整归并。

至此,四种思路的原理、步骤、完整 Java 逐行注释实现及复杂度比较已全部给出。


网站公告

今日签到

点亮在社区的每一天
去签到