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

题目描述

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

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

题解

如果不考虑时间复杂度,可以将两个数组遍历一遍,升序合并成一个新的数组,返回下标为n/2的数;这个过程中,会将两个数组中的数逐个比较,其实只需要比较到 (m+n)/2 时就可以了,但时间复杂度还是O(m+n)

换一种想法(此处[]内的数不是下标,是第几个数)

求中位数,也就是求第K大的数,先比较两个数组第K/2个数,如果A[K/2]大于B[K/2],那么B[K/2]之前的数字(包括B[K/2])都不可能是第K大的数,可以直接不看,从B[K/2+1:m]中找到 K = K-K/2(这个结果不一定是K/2,因为还会向下取整)大的数。递归进行,直到K=1,比较两个数组有效长度的首个数字,最小值为结果。

需要特殊处理的情况

  • 如果 A[k/2-1] 或者 B[k/2-1] 越界,那么我们可以选取对应元素数组中的最后一个元素。在这种情况下,排除数字的个数就不是 k/2 了,而是 A.length - index_a 
  • 如果一个数组为空,说明该数组中的所有元素都被排除,我们可以直接返回另一个数组中第 K 小的元素

我的思维误区

我一直想着,把A[K/2]和B[K/2]之前的数字都删掉,这虽然不违背找中位数,但是,没法下一步递归。

代码

#include <iostream>
using namespace std;

int find_mid(int nums1[],int nums2[],int k,int index1,int index2,int m,int n)
{
    //如果一个数组为空,也就是说,说明该数组的所有元素都被排除,我们可以直接返回另一个数组中第K小的元素
    if(index1 >= m )
    {
        return nums2[k];
    }
    if(index2 >= n)
    {
        return nums1[k];
    }
    if(k == 1)
    {
        return nums1[index1] < nums2[index2] ? nums1[index1]:nums2[index2];
    }
    //如果加上 k/2 后数组越界
    if(k/2+index1-1 >= m)
    {
        if(nums1[m-1] > nums2[k/2+index2-1])
        {
            return find_mid(nums1,nums2,k - (m-index1),index1,index2+k/2,m,n);
        }
        else
        {
            k = k -(m-index1);
            return nums2[k];
        }
    }
    if(k/2+index2-1 >= n)
    {
        if(nums2[n-1] > nums1[k/2+index1-1])
        {
            return find_mid(nums1,nums2,k - (n-index2),index1+k/2,index2,m,n);
        }
        else
        {
            k = k -(n-index2);
            return nums1[k];
        }
    }

    //先比较数组中第 k/2 的数
    if(nums1[k/2+index1-1] > nums2[k/2+index2-1])
    {
        //nums2可以去掉前几个数,并更新index
        return find_mid(nums1,nums2,k - k/2,index1,index2+k/2,m,n);
    }
    if(nums1[k/2+index1-1] <= nums2[k/2+index2-1])
    {
        //nums1可以去掉前几个数,并更新index
        return find_mid(nums1,nums2,k - k/2,index1+k/2,index2,m,n);
    }

}

int main()
{
    int m;
    int n;
    cin >> m;
    cin >> n;
    int nums1[m];
    int nums2[n];
    for(int i = 0 ; i < m ; i++)
    {
        cin >> nums1[i];
    }
    for(int j = 0 ; j < n ; j++)
    {
        cin >> nums2[j];
    }
    double result ;
    //当数组个数为奇数时,                                                                                                                                                                                                                                                                                                                                                                                                                                  
    if((m+n) % 2 == 1)
    {
        result = find_mid(nums1,nums2,(m + n) / 2 + 1,0,0,m,n);
    }
    else
    {
        result = find_mid(nums1,nums2,(m + n) / 2 + 1,0,0,m,n) + find_mid(nums1,nums2,(m + n) / 2,0,0,m,n);
        result = result / 2.0;
    }
    cout << result << endl;
    return 0;
}

方法二

分割线

首先:思考中位数的作用,分割线的作用,中位数左边的数全部小于中位数右边的数,并将集合划分成长度相等的两个部分。

分割效果如图所示(left_part中有若干个数,right_part中有若干数)

max(left_part) <= min(right_part)

如果两个数组长度之和为奇数,那么max{A[i-1],B[j-1]}即为所求(因为在奇数的情况下,我们让左边多了一个)。如果数组长度之和为偶数,那么求max{A[i-1],B[j-1]}和min{A[i],B[j]}的平均值。

i 与 j 的关系

  • 当A和B的总长度是偶数时,len(left_part) = len(right_part)
  • 当A和B的总长度是奇数时,len(left_part) = len(right_part)

故:i + j = m - i + n - j (当 m+n 为偶数时) 或者 i + j = m - i + n - j +1 (当 m+n 为奇数时),我们可以化简并统一成 : i + j = (m + n + 1) / 2 {因为,对于偶数来讲,a/2和(a+1)/2最后的结果是一样的},j = (m + n + 1)/2 - i {注意,如果 i 的范围是 m 和 n 中更大的那个数,最后可能减出一个负数,所以,要减去的是数组元素个数少的那个}

所以,我们枚举 i 在 [0,m] 范围内,由等式得到 j 

需要找的关系

我们需要找的是

它等价于{为什么等价? ->把i+1代入A[i-1]<=B[j],其实就是  B[j-1] >= A[i],那么就不满足条件1 }

所以,我们使用折半查找,如果遇到 A[i - 1] > B[j] 的,说明 i 选的太大了,right = i-1 继续选;如果遇到 A[i - 1] <= B[j] 记录此时的meda左 和 meda右 (最接近分割线的两个值),left = i + 1,继续选,直到 left > right ,就找了最大的 i 。

边界情况

为了简化分析,当 i = 0 时,A[i - 1] 怎么办 : 我们的表达式,必会有 max{A[i - 1], B[i-1]}这一步,当前一部分没有那个数组时,分割线左边只剩下另一个数组的元素,将此时的A[i - 1]设为负无穷,不影响找出左边最大值。

来个三目判断,当 i=0时,A[i - 1] = INT_MIN

时间复杂度

O(log(m))

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值