算法中阶练习---两个有序数组的中位数(leetcode肝了一天,上面的题解有瑕疵)

1. 题源:leetcode困难题,2011年计算机统考408数据结构算法大题

2. 题目描述

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

算法的时间复杂度应该为 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

3.分析

就题目本意来说,这题难道应该归类为简单题。但当要求时间复杂度为 O(log (m+n)) 时,level大大提高,解法有很多,但符合时间复杂度却很少。例如
方法一:合并数组,然后找。
方法二:直接在2个数组遍历,找到第k个最小的数

以上的时间复杂度都为O(m+n),不符合要求。
方法三:
这个问题等价于在2个数组中找出第k个小的数,推导过程如下:
当在2个数组A和B当中找第K小的数时,不妨先看
a:A[k/2-1] 和b:B[k/2-1]的大小。
如果a<=b,则在b数组中最多只有B[0]~B[k/2-2]共 k/2-1个小于a,又因为a数组是递增,所以至多有A[0] ~A[k/2-2]和B[0] ~ B[k/2-2]共计k-2个小于a,所以a必然不是第k小的数,则可以排除A[0] ~A[k/2-1]这些范围,k=k-k/2;

要注意以下几种特殊情况:

  1. 如果 A[k/2−1] 或者 B[k/2−1] 越界,那么我们可以选取对应数组中的最后一个元素。在这种情况下,我们必须根据排除数的个数减少 k 的值,而不能直接将 k减去 k/2。
  2. 如果一个数组为空,说明该数组中的所有元素都被排除,我们可以直接返回另一个数组中第 k 小的元素。
  3. 如果 k=1,我们只要返回两个数组首元素的最小值即可。
初始:
k=(4+9+1)/2=7
A:1 3 4 9
B:1 2 3 4 5 6 7 8 9

第一次:
K/2-1=2比较:A[2]>B[2]
数组筛选后为:
A:1 3 
B:4 5 6 7 8 9
k=k-k/2=4;

第二次:
K/2-1=1 比较:35
数组筛选后为:
A:4 9
B:4 5 6 7 8 9
k=K-k/2=2

第三次:
k/2-1=0 比较 4 4
数组筛选后为:
A:9
B:4 5 6 7 8 9
k=K-k/2=1

第四次
k为1,选取2个数组最小的数字,为4

解法四:
题解写的严谨易懂,这里就概括一下,省去一些证明。
将A B分别进行某种划分和合并

 left_part          |         right_part
    A[0], A[1], ..., A[i-1]  |  A[i], A[i+1], ..., A[m-1]
    B[0], B[1], ..., B[j-1]  |  B[j], B[j+1], ..., B[n-1]
  1. 若A B长度为偶数,则当
    len(left_part)=len(right_part) 且 max(left_part)≤min(right_part)
    此时,i + j = m - i + n - j 即 i+j=(m+n)/2
中位数就是前一部分的最大值和后一部分的最小值的平均值:
median=(max(left_part)+min(right_part))/2
  • 若A B长度为奇数,则当
    len(left_part)=len(right_part) +1 且max(left_part)≤min(right_part),
    此时i + j = m - i + n - j+1 i+j=(m+n+1)/2
中位数就是前一部分的最大值:
median=(max(left_part))

我们注意到当m+n为偶数时,i+j=(m+n)/2 和i+j=(m+n+1)/2是相等的,于是i=(m+n+1)/2-j这定了划分时左右数目关系,B[j−1]≤A[i] 且 A[i−1]≤B[j] 定义了左边最大数字<=右边最小数
于是数学语言描述如下

[0, m]中找到 i,使得:
B[j−1]≤A[i] 且 A[i−1]≤B[j],其中i=(m+n+1)/2-j;

等价于:

[0, m]中找到最大的 i,使得:
A[i−1]≤B[j],其中i=(m+n+1)/2-j;

于是解法为:i 在 [0, m]的区间上进行二分搜索,找到最大的i满足,A[i−1]≤B[j],其中i=(m+n+1)/2-j
这里要注意一下几点:

  • 对于i=0、i=m、j=0、j=n 这样的临界条件,我们只需要规定A[−1]=B[−1]=−∞,A[m]=B[n]=∞ 。
  • 对于0≤i≤m,0≤j≤n。如果我们规定 A 的长度小于等于B 的长度,m≤n。这样对于任意的i∈[0,m],都有 j ∈[0, n],j=(m+n+1)/2-i。

4.关于leetcode上面的瑕疵

根据中位数的定义,当 m+n 是奇数时,中位数是两个有序数组中的第 (m+n)/2 个元素,当 m+n 是偶数时,中位数是两个有序数组中的第 (m+n)/2个元素和第 (m+n)/2+1 个元素的平均值。因此,这道题可以转化成寻找两个有序数组中的第 k小的数,其中 k 为 (m+n)/2 或 (m+n)/2+1

这里如果是奇数时,如果这里的中位数是第 (m+n)/2 个元素,那只能理解为从0开始计数,这和下面的偶数矛盾了。这里应该改为 (m+n+1)/2个元素,后面题解对应的代码也是(m+n+1)/2

5.代码部分

思路三解法:个人看到二分习惯性用递归了,就一直想怎么用递归写。最后完成了,但感觉太累赘了,耗时也很多(用了vector容器的增删改,这里应该是超复杂度了)后来想想应该直接用数组下标表示的,虽然麻烦但耗时少,唉,小菜鸡一个。

class Solution {
public:
   double fun(vector<int> a, vector<int> b,int k){
   		//递归边界2
        if(a.size()==0){
            return b[k-1];
        }
        else if(b.size()==0){
            return  a[k-1];
        }
        //递归边界1
        else if(k==1){
            return a[0]>b[0]? b[0]:a[0];
        }
        else{
        //数组越界情况
            if ((k/2-1)>a.size()-1){
                if(a.back()<=b[k/2-1])
                {k=k-a.size();
                a.clear();
                return fun(a,b,k);}
                else{
                b.erase(b.begin(),b.begin()+k/2);
                k=k-k/2;
                return fun(a,b,k);
                }
            }
           // 数组越界情况
            else if ((k/2-1)>b.size()-1){
              if(b.back()<=a[k/2-1])
                {k=k-b.size();
                b.clear();
                return fun(a,b,k);}
                else{
                a.erase(a.begin(),a.begin()+k/2);
                k=k-k/2;
                return fun(a,b,k);
                }
            }
            //正常比较
            else if(a[k/2-1]<=b[k/2-1]){
                a.erase(a.begin(),a.begin()+k/2);
                k=k-k/2;
                return fun(a,b,k);               
            }
            else{
                b.erase(b.begin(),b.begin()+k/2);
                k=k-k/2;
                return fun(a,b,k);
            }
        }
        
    }
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
       int k=nums1.size()+nums2.size();
    
        if(k%2==0){
           return (fun(nums1, nums2, k/2)+fun(nums1, nums2, k/2+1))/2.0; 
        }
        
        else   return  fun(nums1,nums2,(k+1)/2);
        
    }

};

下面附上官方标解

class Solution {
public:
    int getKthElement(const vector<int>& nums1, const vector<int>& nums2, int k) {
 

        int m = nums1.size();
        int n = nums2.size();
        int index1 = 0, index2 = 0;

        while (true) {
            // 边界情况
            if (index1 == m) {
                return nums2[index2 + k - 1];
            }
            if (index2 == n) {
                return nums1[index1 + k - 1];
            }
            if (k == 1) {
                return min(nums1[index1], nums2[index2]);
            }

            // 正常情况
            int newIndex1 = min(index1 + k / 2 - 1, m - 1);
            int newIndex2 = min(index2 + k / 2 - 1, n - 1);
            int pivot1 = nums1[newIndex1];
            int pivot2 = nums2[newIndex2];
            if (pivot1 <= pivot2) {
                k -= newIndex1 - index1 + 1;
                index1 = newIndex1 + 1;
            }
            else {
                k -= newIndex2 - index2 + 1;
                index2 = newIndex2 + 1;
            }
        }
    }

    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        int totalLength = nums1.size() + nums2.size();
        if (totalLength % 2 == 1) {
            return getKthElement(nums1, nums2, (totalLength + 1) / 2);
        }
        else {
            return (getKthElement(nums1, nums2, totalLength / 2) + getKthElement(nums1, nums2, totalLength / 2 + 1)) / 2.0;
        }
    }
};

感受:没有对比就没有伤害

解法四代码:

class Solution {
public:
    double findMedianSortedArrays(vector<int>& a, vector<int>& b) {
        if(a.size()>b.size())
            return findMedianSortedArrays(b, a);
        int m=a.size();
        int n=b.size();
        int left=0,right=m;
        //考虑一下四个特殊情况
        int meidian1=0,meidian2=0;
        while(left<=right){
            int i=(left+right)/2;
            int j=(m+n+1)/2-i;
            //要求b[j−1]≤a[i] 且 a[i−1]≤b[j]
            //下面对 i=0时的a[i-1]  i=m时的a[m]  j=0时的b[j-1]   j=n时的b[n]进行特殊处理
            int left_max_i=(i==0?INT_MIN :a[i-1]);
            int right_min_i=(i==m?INT_MAX :a[i]);
            int left_max_j=(j==0?INT_MIN :b[j-1]);
            int right_min_j=(j==n?INT_MAX :b[j]);
            //下面就是二分查找到那个最大的i,且满足left_max_i <= ritht_min_j
            if(left_max_i <= right_min_j){
                meidian1=max(left_max_i,left_max_j);
                meidian2=min(right_min_i,right_min_j);
                left=i+1;
            }
            else{
                right=i-1;
            }
        }
        if((m+n)%2==0){
            return (meidian1+meidian2)/2.0;
        }
        else{
            return meidian1;
        }
    }
   
    
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值