头图

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

关键词:二分查找、数组划分

题目来源:4. 寻找两个正序数组的中位数 - 力扣(Leetcode)

题目描述

 T二分查找
 T数组划分

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

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

输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 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

二分查找

题目要求时间复杂度为O(log(m+n)),且数组有序,可采用二分求解,具体做法如下:

当m+n为奇数时,题目等价于找到第k小的元素,当m+n为偶数时,题目等价于找到第k和k+1小的数。故,任务就是找到第k小的数,若m+n为偶数,则在第k小的数后面再考虑一个数即可。

  1. 设i=0,j=0,ti,tj,i表示数组A的起始考虑位置,也即A当前考虑A[i..m-1],类似的,B当前考虑B[j..n-1],ti和tj分别表示A和B的下一个起始考虑位置
  2. 令ti = i+k/2,tj = j+k/2,ti、tj分别不能超过m、n
  3. 对于数组A[i..m-1]、B[j..n-1],考虑A[ti-1]和B[tj-1]

    • 若A[ti-1]<B[tj-1],说明A[i..ti-1]必定不是第k小的数,也即可以排除A[i..ti-1],共k/2个数。于是,令i = ti,k -= k/2。
    • 若A[ti-1]>B[tj-1],说明B[j..tj-1]必定不是第k小的数,也即可以排除B[j..tj-1],共k/2个数。于是,令j = tj,k -= k/2。
    • 若A[i+k/2-1]=B[j+k/2-1],则可归为A[ti-1]<B[tj-1]的情况

循环结束条件

  • 当i=m时,m+n为偶数 ? return (B[j+k-1]+B[j+k])/2 : return min( B[j+k-1]+B[j+k] )
  • 当j=n时,m+n为偶数 ? return (A[i+k-1]+A[i+k])/2 : return min( A[i+k-1]+A[i+k] )
  • 当k=1时,m+n为偶数 ? return (A[i]+B[i])/2 : return min( A[i], B[i] )
double findMedianSortedArrays(vector<int> &nums1, vector<int> &nums2) {
    auto &A = nums1, &B = nums2;
    int m = A.size(), n = B.size(), k = (n + m + 1) >> 1;
    int i = 0, j = 0, fg = (m + n) & 1, ti, tj;
    while (true) {
        // 循环出口
        if (i == m)return fg ? B[j + k - 1] : (B[j + k - 1] + B[j + k]) / 2.0;
        if (j == n)return fg ? A[i + k - 1] : (A[i + k - 1] + A[i + k]) / 2.0;
        if (k == 1) {
            if (fg)return min(A[i], B[j]);
            if (A[i] < B[j])
                return (A[i] + (i == m - 1 ? B[j] : min(A[i + 1], B[j])))/2.0;
            else
                return (B[j] + (j == n - 1 ? A[i] : min(B[j + 1], A[i])))/2.0;
        }
        // 二分
        ti = min(i + (k >> 1), m), tj = min(j + (k >> 1), n);
        if (A[ti-1] <= B[tj-1]) {
            k -= ti - i, i = ti;
        } else
            k -= tj - j, j = tj;
    }
}

时间复杂度:O( log(m+n) )。每次循环都会排除k/2个数

空间复杂度:O(1)

数组划分

中位数性质:中位数将数组划分成两半,左半部分的数均小于等于右半部分的数。

利用中位数的上述性质,可得到如下做法:

设k=( m+n+1 )/2。

i将数组A划分为A[0..i-1]和A[i..m-1]两部分,其中,i=0时A[0..i-1]为空,i=m时A[i..m-1]为空。

j=k-i,j将数组B划分成B[0..j-1]和B[j..n-1]两部分,其中,j=0时B[0..j-1]为空,j=n时A[j..n-1]为空。

令L=A[0..i-1] + B[0..j-1],R=A[i..m-1] + B[j..n-1]

考虑(m+n)为偶数的情况,L和R恰好各含有一半的元素。当L中的元素全小于等于R中的元素时,中位数=(max(L)+max(R))/2,而max(L)=max(A[i-1],B[j-1]),min(R)=min(A[i],B[j])。

L中的元素全小于R中的元素等价于A[i-1]≤B[j]且B[j-1]≤A[i]。

于是,可通过二分来确定i的值,也即划分点,当有A[i-1]≤B[j]且B[j-1]≤A[i],说明已经找到中位数。

为确保j不会越界,也即使得j保持在区间[0..n]内,应时数组A的长度大于数组B。

考虑(m+n)为奇数的情况,因为L中含有k个元素,故中位数为max(L)。

double findMedianSortedArrays_4(vector<int> &nums1, vector<int> &nums2) {
    auto res=[&](vector<int> &A,vector<int> &B){
        int m = A.size(), n = B.size(), k = (n + m + 1) >> 1, fg = (m + n) & 1;
        // 分界点可选范围[l..r]
        int l = 0, r = m, i, j;
        // 答案一定存在
        while (true) {
            // 左:A[0..i-1] B[0..j-1]
            i = (l + r + 1) >> 1, j = k - i;
            // 交界处的四个数
            int la = i ? A[i - 1] : INT_MIN, ra = i != m ? A[i] : INT_MAX;
            int lb = j ? B[j - 1] : INT_MIN, rb = j != n ? B[j] : INT_MAX;
            // 符合条件
            if (la <= rb && lb <= ra) {
                if (fg) return (double)max(la, lb);
                return (max(la, lb) + min(ra, rb)) / 2.0;
            }
                // 缩小范围
            else if (la <= rb)l = i;
            else r = i - 1;
        }
    };
    return nums1.size() > nums2.size() ? res(nums2, nums1) : res(nums1, nums2);
}

时间复杂度:O(log(min(m,n)))

空间复杂度:O(1)


字节幺零二四
9 声望5 粉丝

talk is cheap, show me you code!