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

题目来源

题目描述

在这里插入图片描述

class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {

    }
};

题目解析

中位数:

  • 如果某个有序数组长度是奇数,那么其中位数就是最中间那个
  • 如果是偶数,那么就是最中间两个数字的平均值

合并取中

  • 先将两个数组合并,两个有序数组的合并也是归并排序中的一部分。然后根据奇数,还是偶数,返回中位数
class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        int M = nums1.size(), N = nums2.size();
        
        std::vector<int> arr(M + N);
        int count = 0;
        int i = 0, j = 0;
        while (count != M + N){
            if(i == M){
                while (j < N){
                    arr[count++] = nums2[j++];
                }
                break;
            }
            
            if(j == M){
                while (i < M){
                    arr[count++] = nums1[i--];
                }
                break;
            }

            if (nums1[i] < nums2[j]){
                arr[count++] = nums1[i++];
            }else{
                arr[count++] = nums2[j++];
            }
        }



        if (count %2 != 0){
            return arr[count/2];
        }else {
            return (arr[count/2-1] + arr[count/2])/2.0;
        }
    } 
};
  • 时间复杂度:遍历全部数组 (m+n)
  • 空间复杂度:开辟了一个数组,保存合并后的两个数组 O(m+n)

双指针

  • 不需要合并两个有序数组,只要找到中位数的位置即可。

    • 由于两个数组的长度已知,因此中位数对应的两个数组的下标之和也是已知的
    • 维护两个指针,初始时分别指向两个数组的下标 0 的位置,每次将指向较小值的指针后移一位(如果一个指针已经到达数组末尾,则只需要移动另一个数组的指针),直到到达中位数的位置。
  • tick:不需要区分m+n是奇数还是偶数:

    • 分别找第 (m+n+1) / 2 个,和 (m+n+2) / 2 个,然后求其平均值即可,这对奇偶数均适用。若 m+n 为奇数的话,那么其实 (m+n+1) / 2 和 (m+n+2) / 2 的值相等,相当于两个相同的数字相加再除以2,还是其本身。
class Solution {
public:
    double findMedianSortedArrays(vector<int>& A, vector<int>& B) {
        int M = A.size(), N = B.size();
        int len = M + N;
        int left = -1, right = -1;
        int aStart = 0, bStart = 0;
        for (int i = 0; i <= len / 2; i++) {
            left = right;
            if (aStart < M && (bStart >= N || A[aStart] < B[bStart])) {
                right = A[aStart++];
            } else {
                right = B[bStart++];
            }
        }
        if ((len & 1) == 0)
            return (left + right) / 2.0;
        else
            return right;
    } 
};

  • 时间复杂度:遍历 len/2+1 次,len=m+n,所以时间复杂度依旧是 O(m+n)
  • 空间复杂度:O(1)

二分

  • 时间复杂度要求 O(log(m+n)。看到log,很明显,我们只有用到二分的方法才能达到。
  • 我们不妨用另一种思路,题目是求中位数,其实就是求第k小的数的一种特殊情况,而求第小的数只有一种算法
  • 第二种方法中,我们一次遍历就相当于去掉不可能是中位数的一个值,也就是一个个排除。由于数列是有序的,其实我们完全可以一半一半的排除。
  • 假设我们要找的是第k小的数,我们每次循环排除掉 k / 2 k/2 k/2个数。举个例子

假设我们要找的是第7小的数。
在这里插入图片描述

  • 我们比较两个数组的第k / 2个数字,如果k是奇数,向下取整。
    • 也就是比较第3个数字,上边数组中的4和下边数组中的3,哪个小,就表明该数组的前k/2个数字都不是第k小的数字,所以可以排除
    • 将1349和45678910两个数组作为新的数组进行比较
      在这里插入图片描述
  • 由于我们已经排除掉了3个数字,所以在两个新数组中,我们只需要找第7-3=4小的数字就可以了,也就是k=4
  • 此时两个数组,比较低2个数字,因为3 < 5,所以我们可以将小的那个数组中的 1 ,3 排除掉了。
    在这里插入图片描述
  • 我们又排除掉 2 个数字,所以现在找第 4 - 2 = 2 小的数字就可以了。
  • 此时比较两个数字中的第k/2=1个数,4==4,怎么办呢?由于两个数相等,所以我们无论去掉哪个数组中的都行,因为去掉1个总会保留1个的,所以没有影响。为了统一,我们就假设 4 > 4 吧,所以此时将下边的 4 去掉
    在这里插入图片描述
  • 由于又去掉 1 个数字,此时我们要找第 1 小的数字,所以只需判断两个数组中第一个数字哪个小就可以了,也就是 4。
  • 所以第 7 小的数字是 4。

我们每次都是取k/2的数进行比较,有可能会遇到数组长度小于k/2的时候
在这里插入图片描述

  • 此时k/2等于3,而上边的数组长度是2,我们此时将箭头指向它的末尾就可以了。
  • 这样的话,由于2<3,所以就会导致上边的数组1,2都被排除。造成下边的情况

在这里插入图片描述

  • 由于2个元素被排除,所以此时k=5
  • 又因为上边的数组已经空了,所以我们只需要返回下边的数组的第5个数组就可以了

从上边可以看到,无论是找第奇数个还是第偶数个数字,对我们的算法并没有影响,而且在算法进行中,k的值都有可能从奇数变为偶数,最都会变为1或者由于一个数组空了,直接返回结果

所以我们采用递归的思路,为了防止数组长度小于k/2,所以每次比较min(k / 2, len(数组))对应的数字,把小的那个对应的数组的数字排除,将两个新数组进入递归,并且k要减去排除的数组的个数。递归出口就是当k==1或者其中一个数组长度是0了

class Solution {
    int getKth(vector<int>& nums1, int start1, int end1, vector<int>& nums2, int start2, int end2, int k){
        int len1 = end1 - start1 + 1;
        int len2 = end2 - start2 + 1;
        //让 len1 的长度小于 len2,这样就能保证如果有数组空了,一定是 len1
        if(len1 > len2){
            return getKth(nums2, start2, end2, nums1, start1, end1, k);
        }
        if(len1 == 0){
            return nums2[start2 + k - 1];
        }
        if(k == 1){
            return std::min(nums1[start1], nums2[start2]);
        }
        int i = start1 + std::min(len1, k / 2) - 1;
        int j = start2 + std::min(len2, k/2) - 1;
        if (nums1[i] > nums2[j]) {
            return getKth(nums1, start1, end1, nums2, j + 1, end2, k - (j - start2 + 1));
        }
        else {
            return getKth(nums1, i + 1, end1, nums2, start2, end2, k - (i - start1 + 1));
        }
    }
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size(), m = nums2.size();
        int left = (n + m + 1) / 2;
        int right = (n + m + 2) / 2;
        //将偶数和奇数的情况合并,如果是奇数,会求两次同样的 k 。
        return (getKth(nums1, 0, n - 1, nums2, 0, m - 1, left) + getKth(nums1, 0, n - 1, nums2, 0, m - 1, right)) * 0.5;
    }
};
class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        int m = nums1.size(), n = nums2.size(), left = (m + n + 1) / 2, right = (m + n + 2) / 2;
        return (findKth(nums1, 0, nums2, 0, left) + findKth(nums1, 0, nums2, 0, right)) / 2.0;
    }
    int findKth(vector<int>& nums1, int i, vector<int>& nums2, int j, int k) {
        if (i >= nums1.size()) return nums2[j + k - 1];
        if (j >= nums2.size()) return nums1[i + k - 1];
        if (k == 1) return min(nums1[i], nums2[j]);
        int midVal1 = (i + k / 2 - 1 < nums1.size()) ? nums1[i + k / 2 - 1] : INT_MAX;
        int midVal2 = (j + k / 2 - 1 < nums2.size()) ? nums2[j + k / 2 - 1] : INT_MAX;
        if (midVal1 < midVal2) {
            return findKth(nums1, i + k / 2, nums2, j, k - k / 2);
        } else {
            return findKth(nums1, i, nums2, j + k / 2, k - k / 2);
        }
    }
};

在这里插入图片描述

切割(不会)

为了解决这个问题,我们需要理解 “中位数的作用是什么”。在统计中,中位数被用来:将一个集合划分为两个长度相等的子集,其中一个子集中的元素总是大于另一个子集中的元素。

所以我们只需要将数组进行切割。

一个长度为m的数组,有0~m总共m+1个位置可以切
在这里插入图片描述
我们把数组A和数组B分别在i和j进行切割
在这里插入图片描述
将 i 的左边和 j 的左边组合成「左半部分」,将 i 的右边和 j 的右边组合成「右半部分」。

  • 当A数组和B数组的总长度是偶数时,如果我们能够保证:
    • 左半部分长度等于右半部分:i + j = m - i + n - j , 也就是 j = ( m + n ) / 2 - i
    • 左半部分最大的值小于等于右半部分最小的值:max ( A [ i - 1 ] , B [ j - 1 ])) <= min ( A [ i ] , B [ j ]))
    • 那么,中位数就可以表示如下:
      • (左半部分最大值 + 右半部分最小值 )/ 2。
      • (max ( A [ i - 1 ] , B [ j - 1 ])+ min ( A [ i ] , B [ j ])) / 2
  • 当 A 数组和 B 数组的总长度是奇数时,如果我们能够保证
    • 左半部分的长度比右半部分大 1: i + j = m - i + n - j + 1也就是 j = ( m + n + 1) / 2 - i
      • 左半部分最大的值小于等于右半部分最小的值 max ( A [ i - 1 ] , B [ j - 1 ])) <= min ( A [ i ] , B [ j ]))
      • 那么,中位数就是:
        • 左半部分最大值,也就是左半部比右半部分多出的那一个数。
        • m a x ( A [ i − 1 ] , B [ j − 1 ] ) max ( A [ i - 1 ] , B [ j - 1 ]) max(A[i1],B[j1])
  • 上边的第一个条件我们其实可以合并为 j = ( m + n + 1 ) / 2 − i j = ( m + n + 1) / 2 - i j=(m+n+1)/2i,因为如果 m + n m + n m+n 是偶数,由于我们取的是 int值,所以加 1 1 1 也不会影响结果。

类似题目

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值