LeetCode 4 两个排序数组的中位数(数组、二分查找、分治法)

该博客介绍了如何在O(log (m+n))的时间复杂度内找到两个有序数组的中位数。通过二分查找和分治策略,确保数组总长度始终为奇数,然后寻找第k个元素,最终找到中位数。文章包含两种C++代码实现和详细解析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目要求:

给定两个大小为 m 和 n 的有序数组 nums1 和 nums2 

请找出这两个有序数组的中位数。要求算法的时间复杂度为 O(log (m+n)) 。

你可以假设 nums1 和 nums2 不同时为空。

示例 1:

nums1 = [1, 3]
nums2 = [2]

中位数是 2.0

示例 2:

nums1 = [1, 2]
nums2 = [3, 4]

中位数是 (2 + 3)/2 = 2.5

 

 

C++代码1:

(1)暴力解法:

class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
    vector<int>nums;
    int i=0,j=0;
    while(i<nums1.size() && j<nums2.size())
    {
        if(nums1[i]<nums2[j])
        {
            nums.push_back(nums1[i++]);
        }
        else
        {
            nums.push_back(nums2[j++]);
        }
    }
    if(i==nums1.size())
    {
        while(j<nums2.size())
            nums.push_back(nums2[j++]);
    }
    else if(j==nums2.size())
    {
        while(i<nums1.size())
            nums.push_back(nums1[i++]);
    }
    if(nums.size()%2)
        return nums[nums.size()/2];
    else
    {
        return ((nums[nums.size()/2]+nums[nums.size()/2-1])/2.0);
    }
    }
};

 

 

 

结果:

 

 

解析:

既然两个数组已经是有序的了,那么可以将两个数组合并成一个数组,依然有序,然后根据奇偶来判断出中位数。

虽然测试通过了,不过估计是LeetCode自身没办法检查时间复杂度,这个归并的方法时间复杂度是O(m+n),很明显不符合题目中的O(log (m+n)) 的算法复杂度要求。

 

 

 

 

C++代码2:

class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        if(nums1.size() == 0)
            return MedofArray(nums2);
        if(nums2.size() == 0)
            return MedofArray(nums1);
        int n = nums1.size();
        int m = nums2.size();
        if(n > m)   //保证数组1一定最短
            return findMedianSortedArrays(nums2,nums1);
        int L1,L2,R1,R2,c1,c2,lo = 0, hi = 2*n;  //我们目前是虚拟加了'#'所以数组1是2*n+1长度
        while(lo <= hi)   //二分
        {
            c1 = (lo+hi)/2;  //c1是二分的结果
            c2 = m+n- c1;
            L1 = (c1 == 0)?INT_MIN:nums1[(c1-1)/2];   //map to original element
            R1 = (c1 == 2*n)?INT_MAX:nums1[c1/2];
            L2 = (c2 == 0)?INT_MIN:nums2[(c2-1)/2];
            R2 = (c2 == 2*m)?INT_MAX:nums2[c2/2];

            if(L1 > R2)
                hi = c1-1;
            else if(L2 > R1)
                lo = c1+1;
            else
                break;
        }
        return (max(L1,L2)+ min(R1,R2))/2.0;
    }
    double MedofArray(vector<int>& nums)
    {
        if(nums.size() == 0)    return -1;
        return (nums[nums.size()/2]+nums[(nums.size()-1)/2])/2.0;
    }
};

 

 

 

 

结果:

 

 

 

解析:

通过切一刀,能够把有序数组分成左右两个部分,切的那一刀就被称为割(Cut),割的左右会有两个元素,分别是左边最大值和右边最小值。

定义L = Max(LeftPart),R = Min(RightPart)

割可以割在两个数中间,也可以割在一个数上,如果割在一个数上,那么这个数即属于左边,也属于右边。

下面举2个例子:

1.比如说[2 3 5 7]这个序列,割就在3和5之间

[2 3 / 5 7]

中值就是(3+5)/2 = 4

2.如果是[2 3 4 5 6]这个序列,割在4上,我们可以把4分成2个

[2 3 (4/4) 5 7]

中值就是(4+4)/2 = 4

这样可以保证不管中值是一个数还是两个数都能统一运算。

 

因此对于单数组,找其中的第k个元素特别容易,这里就不解释了,我们主要来看双数组怎么解决。

 

 

双数组

我们设:C​i​​为第i个数组的割。 L​i​​为第i个数组割后的左元素. R​i​​为第i个数组割后的右元素。

 

Alt text

 

 

 

我们看如何从双数组里取出第k个元素

  1. 首先L​i​​<= R​i​​是肯定的(因为数组有序,左边肯定小于右边)
  2. 如果我们让L​1​​<=R​2​​ && L​2​​<=R​1​​ 
  3. 那么左半边 全小于右半边,如果左边的元素个数相加刚好等于k,那么第k个元素就是Max(L1,L2)
  4. 如果 L1>R2,说明数组1的左边元素太大(多),我们把C1减小,把C2增大。L2>R1同理,把C1增大,C2减小。

 

Alt text

假设k=3

对于

[1 4 7 9]

[2 3 5]

设C1 = 2,那么C2 = k-C1 = 3-2= 1

[1 4/7 9]

[2/3 5]

这时候,L1(4)>R2(3),说明C1要减小,C2要增大,C1 = 1,C2=k-C1 = 2

[1/4 7 9]

[2 3/5]

这时候,满足了L​1​​<=R​2​​ && L​2​​<=R​1​​,第3个元素就是Max(1,3) = 3。

如果对于上面的例子,把k改成4就恰好是中值。

下面具体来看特殊情况的中值问题。

 

双数组的奇偶

中值的关键在于,如何处理奇偶性,单数组的情况,我们已经讨论过了,那双数组的奇偶问题怎么办,m+n为奇偶处理方案都不同

 

让数组恒为奇数

有没有办法让两个数组长度相加一定为奇数或偶数呢?

其实有的,虚拟加入‘#'(这个trick在manacher算法中也有应用),让数组长度恒为奇数(2n+1恒为奇数)。

Ps.注意是虚拟加,其实根本没这一步,因为通过下面的转换,我们可以保证虚拟加后每个元素跟原来的元素一一对应

Alt text

 

映射关系

这有什么好处呢,为什么这么加?因为这么加完之后,每个位置可以通过/2得到原来元素的位置。

Alt text

 

在虚拟数组里表示“割”

不仅如此,割更容易,如果割在‘#'上等于割在2个元素之间,割在数字上等于把数字划到2个部分。

奇妙的是不管哪种情况:

Li = (Ci-1)/2
Ri = Ci/2

例:

  1. 割在4/7之间‘#',C = 4,L=(4-1)/2=1 ,R=4/2=2 刚好是4和7的原来位置!
  2. 割在3上,C = 3,L=(3-1)/2=1,R=3/2 =1,刚好都是3的位置!

剩下的事情就好办了,把2个数组看做一个虚拟的数组A,目前有2m+2n+2个元素,割在m+n+1处,所以我们只需找到m+n+1位置的元素和m+n+2位置的元素就行了。(在数组中是[m+n]和[m+n+1])

左边:A[m+n] = Max(L1+L2)
右边:A[m+n+1] = Min(R1+R2)

Mid = (A[m+n]+A[m+n+1])/2 = (Max(L1+L2) + Min(R1+R2) )/2

至于在两个数组里找割的方案,就是上面的方案。

 

分治的思路

有了上面的知识后,现在的问题就是如何利用分治的思想。

怎么分?

最快的分的方案是二分,有2个数组,我们对哪个做二分呢? 根据之前的分析,我们知道了,只要C1或C2确定,另外一个也就确定了。这里,为了效率,我们肯定是选长度较短的做二分,假设为C1。

怎么治?

也比较简单,我们之前分析了:就是比较L1,L2和R1,R2。

  • L1>R2,把C1减小,C2增大。—> C1向左二分
  • L2>R1,把C1增大,C2减小。—> C1向右二分

越界问题

如果C1或C2已经到头了怎么办? 这种情况出现在:如果有个数组完全小于或大于中值。可能有4种情况:

  • C1 = 0 —— 数组1整体都比中值大,L1 >R2,中值在2中
  • C2 = 0 —— 数组1整体都比中值小,L1 <R2,中值在1中
  • C1 = n*2 —— 数组1整体都比中值小,L1 <R2,中位数在2中
  • C2 = m*2 —— 数组1整体都比中值大,L1 >R2,中位数在1中

其实,如果我已经确定了数组1是最短的数组,那只有两种情况了,比较好处理:

  • 如果C1 = 0 —> 那么我们缩小L1,L1 = INT_MIN,保证判断正确。
  • 如果C1 = n*2 —> 那么我们增大R1,R1 = INT_MAX,保证判断正确。

 

 

 

 

更详细的解析可以看以下链接:

https://hk029.gitbooks.io/leetbook/%E5%88%86%E6%B2%BB/004.%20Median%20of%20Two%20Sorted%20Arrays[H]/004.%20Median%20of%20Two%20Sorted%20Arrays[H].html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值