Skip to content

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

约 1133 字大约 4 分钟

2025-02-24

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

提示:

  • nums1.length == m
  • nums2.length == n
  • 0 <= m <= 1000
  • 0 <= n <= 1000
  • 1 <= m + n <= 2000
  • -106 <= nums1[i], nums2[i] <= 106

奇偶分析+二分查找

如果 pivot1 <= pivot2,说明 nums1 的 index1 到 newIndex1 范围内的元素都不可能是第 k 小的元素 (因为即使这些元素与 nums2 中的前几个元素合并,数量也还不够 k)。因此,这些元素可以被安全地排除掉, 同时更新 index1 的位置到 newIndex1 + 1(即排除这部分元素后,index1 的新起点)。

k -= (newIndex1 - index1 + 1); 这行代码更新了 k 的值,减去了被排除的元素的数量,即下一次寻找第 k 小的元素变为寻找第 k - 排除的元素数量 小的元素。

  • 如果 A[k/21]<B[k/21]A[k / 2-1]<B[k / 2-1], 则比 A[k/21]A[k / 2-1] 小的数最多只有 AA 的前 k/21k / 2-1 个数和 BB 的前 k/21k / 2-1 个数,即比 A[k/21]A[k / 2-1] 小的数最多只有 k2k-2 个,因此 A[k/21]A[k / 2-1] 不可能是第 kk 个数, A[0]A[0]A[k/21]A[k / 2-1] 也都不可能是第 kk 个数,可以全部排除。
  • 如果 A[k/21]>B[k/21]A[k / 2-1]>B[k / 2-1] ,则可以排除 B[0]B[0]B[k/21]B[k / 2-1]
  • 如果 A[k/21]=B[k/21]A[k / 2-1]=B[k / 2-1] ,则可以归入第一种情况处理。
class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int length1 = nums1.length, length2 = nums2.length;
        int totalLength = length1 + length2;
        
        if (totalLength % 2 == 1) {
            // 当总长度为奇数时,中位数是中间的一个元素
            int midIndex = totalLength / 2;
            double median = getKthElement(nums1, nums2, midIndex + 1);
            return median;
        } else {
            // 当总长度为偶数时,中位数是中间两个元素的平均值
            int midIndex1 = totalLength / 2 - 1, midIndex2 = totalLength / 2;
            double median = (getKthElement(nums1, nums2, midIndex1 + 1) + getKthElement(nums1, nums2, midIndex2 + 1)) / 2.0;
            return median;
        }
    }

    // 找到两个正序数组中第 k 小的元素
    public int getKthElement(int[] nums1, int[] nums2, int k) {
        int length1 = nums1.length, length2 = nums2.length;
        int index1 = 0, index2 = 0;

        while (true) {
            // 边界情况。注意有两个边界情况,一个是数组为空,一个是k=1。几个独立变量就有几种边界情况。
            if (index1 == length1) {
                // 如果nums1为空数组,则直接返回nums2中的第k小的元素,类似下面的newIndex2的计算
                return nums2[index2 + k - 1];
            }
            if (index2 == length2) {
                // 如果nums2为空数组,则直接返回nums1中的第k小的元素
                return nums1[index1 + k - 1];
            }
            // 注意不要漏掉1这个边界情况
            if (k == 1) {
                // 如果k为1,即找最小元素,直接返回nums1和nums2当前索引的最小值
                return Math.min(nums1[index1], nums2[index2]);
            }
            
            // 正常情况,二分查找
            int half = k / 2;
            // 计算新的检查位置,从起点向后查k/2-1个,到达本数组的第k/2个数,注意不能超出数组长度
            // k对应的是长度,index是索引,长度-1=索引
            int newIndex1 = Math.min(index1 + half - 1, length1 - 1);
            int newIndex2 = Math.min(index2 + half - 1, length2 - 1);
            int pivot1 = nums1[newIndex1], pivot2 = nums2[newIndex2];
            
            if (pivot1 <= pivot2) {
                // pivot1较小或等于pivot2,排除nums1中index1到newIndex1的部分,并更新k值和本数组的起点
                // k 向 1 靠近,起点向终点靠近,这样就能被上面的边界情况处理。即每次排除一些元素,收敛到边界情况。
                // 举例,index1=0,newIndex1=k/2-1,此时k-k/2=k/2.
                // 注意不一定是减去k/2!因为newIndex还有一个上界——length - 1
                k -= (newIndex1 - index1 + 1);
                // index1=k/2,index2=0,下一轮假设index1越界了,则nums[index2+k-1]=nums[k-1]即第k个元素
                index1 = newIndex1 + 1;
            } else {
                // pivot2较小,排除nums2中index2到newIndex2的部分,并更新k值和本数组的起点
                k -= (newIndex2 - index2 + 1);
                index2 = newIndex2 + 1;
            }
        }
    }
}
  • 时间复杂度: O(log(m+n))O(\log (m+n)) ,其中 mmnn 分别是数组 nums1nums_1nums2nums_2 的长度。初始时有 k=(m+n)/2k=(m+n) / 2k=(m+n)/2+1k=(m+n) / 2+1 ,每一轮循环可以将查找范围减少一半,因此时间复杂度是 O(log(m+n))O(\log (m+n))
  • 空间复杂度: O(1)O(1)