Leetcode-Median of Two Sorted Arrays

前言

Leetcode刷到150道了,各种题型都已经练习了一遍,没有必要再去刷数量了!分类总结解题方法,完善知识体系已经是刻不容缓了。尤其是在看了《暗时间》之后,深有感触。总结、反思自己的思维过程也许是最重要的。对每道题进行深加工,抽象出一般的概念,得到一般的解题策略。这个过程才是最重要的,是沉淀思想的绝好途径。

先简单摘一些常用的解题方法,以后每碰到难题的时候,都要想一下用这些方法是否可以解决:

  • 时刻不忘未知量
    时刻要想到自己的问题是什么,要求什么。

  • 用特例启发思考
    构造一个合适的实例,可能会发现一般的规律。

  • 反过来推导
    设立未知数,从结论出发,向已知条件靠扰。

  • 试错

  • 调整题目的条件
    去掉一个条件,观察区别,再放上那个条件,感觉到题目的内在结构上的某种约束,进而得到答案。
  • 求解一个类似的题目
    为了优化脑中的知识结构,我们在记忆掌握和分析问题的时候都应该尽量抽象地去看待,这样才能建立知识的本质联系。
  • 列出所有可能与题目有关的定理或性质
    比如这道题目,可以列出这样的性质:中位数是数组中最中间的数。如果元素总数为奇数,它左边所有元素的个数和右边所有元素的个数相等;如果为偶数,则将所有元素平分成两左右两部分,两部分元素个数相等, 中位数为最中间两者的均值。
  • 考察反面,考察其他所有情况
  • 将问题泛化
    这道题应该要进行泛化,比如如果要求两个排序元素里的第K大元素怎么求?如果是n个排序数组呢?

题目分析1

Leetcode-CPP_p14可以从结论来推导方法:题目要求用 log(m+n) 的复杂度,而中位数的序号为 i=m+n2 ,要想达到要求的复杂度,则每次查找都应该使 i 减半,即要用到二分搜索。那怎样才能用到二分搜索呢?这一步还不是那么明显,答案是将原问题泛化,寻找两个排序数组的第k小的数,然后每步排除 k/2 个数,则最后可以达到复杂度要求。

假设 A,B 两个数组的元素个数都大于 k/2 ,那么将 A,B 的第 k 个元素,也就是A[k21] B[k21] 作比较的话,

A[0]B[0]A[1]B[1]A[k21]B[k21]A[k2]B[k2]

可以得到:

A[k21]<B[k21],A[k21]>B[k21],A[k21]==B[k21],(1)(2)(3)

对于情形 (1) A[0]A[k21] 一定是排在 B[k21] 之前,因此 A[0]A[k21] 绝对不会是第 k 小的数,可以在下一轮寻找中去掉,因此下一轮比较将变成:

A[k2]B[0]B[1]A[k2+k41]B[k41]B[k4]

这里我们不能排除 B 中的元素,是因为我们仅仅知道A[k21] B[k21] 的大小关系,而不知道 A[k21] B[0] 的关系, B[0] 可以很大,以至于大于 A[k21] ,而这个时候,其实我们是得不出进一步的结论的,因为我们还是不知道 B[0] A[k21] 后面元素的关系。当然,另一方面, B[0] 可以比较小,以至于当它增加到 B[k21] 时只比 A[k21] 大了一点点,且小于 A[k2] ,那么 B[k21] 就是我们要找的第 k 小的数,只是找到它还需要再递归几次。而这并不难分析。

因为我们是要寻找第k小的数,永远不要忘了我们的目的是什么——走得太远,不要忘了当初是为什么出发!而我们已经排除了 k2 个数,因此下一步是寻找第 k2 (这里的第 k2 是指包括当前元素的元素个数)小的数,因而又可以排除一半的数,即 k4

情形 (2) 的分析类似;

而情形 (3) 就更简单了,直接可以得到要找的数就是 A[k21] 。因为一定可以得到下面的排列:

A[0]A[k22]B[0]B[k22] A[k21] B[k21]

虽然我们不知道 AB 中的前 k2 个数的具体顺序,但是最后两个数一定是 A[k21],B[k21] ,而最后一个数正是我们要找的第 k 小的数。

算法的正确性

如何证明算法的正确性呢?
每次递归都会排除一半的元素或者排除掉整个数组,即当k2>m,最后一定会得到正确的结果。

代码

int getKth(int a[], int m, int b[], int n, int k)
{
    if (m > n)
        return getKth(b, n, a, m, k);
    if (0 == m)
        return b[k-1];
    if (1 == k)
        return min(a[0], b[0]);
    int i = min((k+1)/2, m);
    /*if (a[i-1] < b[i-1])
        return getKth(a+i, m-i, b, n, k-i);
    else if (a[i-1] > b[i-1])
        return getKth(a, m, b+i, n-i, k-i);*/
    int j = k-i;
    if (a[i-1] < b[j-1])
        return getKth(a+i, m-i, b, n, k-i);
    else if (a[i-1] > b[j-1])
        return getKth(a, m, b+i, n-i, k-j);
    else
        return a[i-1];
}

double findMedianSortedArrays(int* nums1, int nums1Size, int* nums2, int nums2Size)
{
    int total = nums1Size+nums2Size;
    if (total & 1) //odd
        return getKth(nums1, nums1Size, nums2, nums2Size, total/2+1);
    else //even
        return (getKth(nums1, nums1Size, nums2, nums2Size, total/2+1) + getKth(nums1, nums1Size, nums2, nums2Size, total/2))/2.0;       
}

代码中注释的地方是有问题的,如果只是比较 A[i1] B[i1] ,那么无论 i 是等于k2还是等于 k+12 ,最后都是不能直接用后面三种情况来处理的。所以我们还需要一个变量 j=ki 来保证目前我们比较的元素个数为 k 。还是那句话,不要忘了最初的目的是什么。

所以这里的关键在于选出k个数,比较每个一维数组的最后一个元素的大小。对于 kn 大于一维数组的长度 m 的情形,就会越界,这时只能取m个元素了,那另外一个数组就必须取 km 个元素了,对于 n==2 时,显然 km 对于第2个数组是不越界的。但对 n>2 的情形,则情况会复杂很多。

下面是用vector加上迭代器的代码:

int getKthOfVectors(vector<int>& nums1, vector<int>::iterator it1, vector<int>& nums2, vector<int>::iterator it2, int k)
{
    int sz1 = nums1.end()-it1;
    int sz2 = nums2.end()-it2;
    if (sz1 > sz2)
        return getKthOfVectors(nums2, it2, nums1, it1, k);
    if (0 == sz1)
        return *(it2+k-1);
    if (1 == k)
        return min(*it1, *it2);
    int i = min((k+1)/2, sz1);
    int j = k-i;
    if (*(it1+i-1) < *(it2+j-1))
    {
        it1 += i;
        return getKthOfVectors(nums1, it1, nums2, it2, k-i);
    }   
    else if (*(it1+i-1) > *(it2+j-1))
    {
        it2 += j;
        return getKthOfVectors(nums1, it1, nums2, it2, k-j);
    }   
    else
        return *(it1+i-1);
}

double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2)
{
    int total = nums1.size()+nums2.size();  
    if (total & 1) //odd
        return getKthOfVectors(nums1, nums1.begin(), nums2, nums2.begin(), (total+1)/2);
    else //even
        return (getKthOfVectors(nums1, nums1.begin(), nums2, nums2.begin(), total/2+1)+getKthOfVectors(nums1, nums1.begin(), nums2, nums2.begin(), total/2))/2.0;
}

效率

假定 n 是要找的第n小的数。则:
T(n)=T(n/2)+O(1) ,由主定理 T(n)=logn

题目分析2

根据discuss里分享的解答,还可以利用中位数的这一性质:中位数两边的元素个数相等(或相差1)。列出这一性质并不难,难就难在怎么根据这一性质继续往下走。

A[0]B[0]left partA[1]B[1]A[i1]B[j1]right partA[i]B[j]A[m1]B[n1]

当左右两部分的元素个数相等或者相差1时,而且 A[i]>B[j1],B[j]>A[j1] ,那么中位数就不难找出来了。因此我们只要找出 i

由此,可列方程i+j=mi+nj或者 i+j=mi+nj+1

{i+j=mi+nj,i+j=mi+nj+1,(m+n)(m+n)

{j=m+n2i,j=m+n+12i,(m+n)(m+n)

j=m+n+12i (将m+n为奇数和偶数统一起来)

因此我们只要在 0 ~m中寻找 i ,就可以得到解答,而且由于是寻找中位数,它一定是存大的,就是说我们用binary search来寻找i,是一定能找到的。值得注意的是,这里的 i[0,m] ,当 i==0 时, A 全部在left part,当i==m时, A 全部在rigth part部分。

算法的正确性

每次查找,要么找到i,要么会缩小查找范围,而 i 一定是存在的,所以最后一定能找到i

代码

double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2)
{
    int sz1 = nums1.size();
    int sz2 = nums2.size();
    if (sz1 > sz2)
        return findMedianSortedArrays(nums2, nums1);
    int imin = 0;
    int imax = sz1;
    int i, j;
    while (imin <= imax)
    {
        i = (imin+imax)/2;
        j = (sz1+sz2+1)/2 - i;
        if (i > 0 && j < sz2 && nums2[j] < nums1[i-1])
            imax = i-1;
        else if (j > 0 && i < sz1 && nums1[i] < nums2[j-1])
            imin = i+1;
        else 
            break;
    }
    int num1;
    if (0 == i)
        num1 = nums2[j-1];
    else if (0 == j)
        num1 = nums1[i-1];
    else
        num1 = max(nums1[i-1], nums2[j-1]);

    if ((sz1+sz2) & 1) //odd
        return num1;

    int num2 = min(nums1[i], nums2[j]);
    return (num1+num2)/2.0;
}

注意:代码最后返回时用到除法,除数要用2.0,否则返回的是int类型转换到double,结果错误。

效率

二分查找的效率当然是 log2min(m,n)

推广

如果是在 n 个已排序的数组,寻找第k小的数,该怎么求呢?
根据思路1,我们可以比较每个数组的第 kn 个数,如果全部相等,则找到第 k 小的数;否则,可以排除(n1)kn个数,只有最大的那个元素所在的数组不能排除掉。

上面所说的是理想情况下,实际写代码的时候要考虑的东西稍复杂一些,当数组的元素个数小于 kn 时,明显就会越界。再有首先得保证,所有数组的元素总数一定大于 k 的。

因此,可以推广为找出二维vector中的第k小的数。

接口为:

double findMedianSortedArrays(vector<vector<int>> &nums, int k)

效率又该怎么计算呢?
这里 n k均有可能是变量,为了更好的与主定理对应,我们用
n 表示输入的规模,即

(1)如果是在 n 个已排序的数组,寻找第b小的数

复杂度与 n 其实没有关系,只与b有关,因此 T(n)=O(1)

(2) 如果是在 b 个已排序的数组,寻找第n小的数

每次递归后 n 都会变成原来的1b T(n)=T(n/b)+O(1) ,由主定理 T(n)=logn
最坏情况下,每次只能排除一个数,那么时间复杂度就会降为 O(n)

结语

如果 n 维数组的每维的长度相等,还比较好办。如果每维的长度不等,对于最好情况下的每次递归后规模变成原来的1n就会退化成每次递归后规模只减了1。所以,以上的思路只对2个已经排序的一维数组有比较好的效果。

不管怎么说,第一篇博客,加油!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可以使用二分查找算法来解决这个问题。 首先,我们可以将两个数组合并成一个有序数组,然后求出中位数。但是,这个方法的时间复杂度为 $O(m + n)$,不符合题目要求。因此,我们需要寻找一种更快的方法。 我们可以使用二分查找算法在两个数组中分别找到一个位置,使得这个位置将两个数组分成的左右两部分的元素个数之和相等,或者两部分的元素个数之差不超过 1。这个位置就是中位数所在的位置。 具体来说,我们分别在两个数组中二分查找,假设现在在第一个数组中找到了一个位置 $i$,那么在第二个数组中对应的位置就是 $(m + n + 1) / 2 - i$。如果 $i$ 左边的元素个数加上 $(m + n + 1) / 2 - i$ 左边的元素个数等于 $m$ 个,或者 $i$ 左边的元素个数加上 $(m + n + 1) / 2 - i$ 左边的元素个数等于 $m + 1$ 个,则这个位置就是中位数所在的位置。 具体的实现可以参考以下 Java 代码: ```java public double findMedianSortedArrays(int[] nums1, int[] nums2) { int m = nums1.length, n = nums2.length; if (m > n) { // 保证第一个数组不大于第二个数组 int[] tmp = nums1; nums1 = nums2; nums2 = tmp; int t = m; m = n; n = t; } int imin = 0, imax = m, halfLen = (m + n + 1) / 2; while (imin <= imax) { int i = (imin + imax) / 2; int j = halfLen - i; if (i < imax && nums2[j - 1] > nums1[i]) { imin = i + 1; // i 太小了,增大 i } else if (i > imin && nums1[i - 1] > nums2[j]) { imax = i - 1; // i 太大了,减小 i } else { // i 是合适的位置 int maxLeft = 0; if (i == 0) { // nums1 的左边没有元素 maxLeft = nums2[j - 1]; } else if (j == 0) { // nums2 的左边没有元素 maxLeft = nums1[i - 1]; } else { maxLeft = Math.max(nums1[i - 1], nums2[j - 1]); } if ((m + n) % 2 == 1) { // 总元素个数是奇数 return maxLeft; } int minRight = 0; if (i == m) { // nums1 的右边没有元素 minRight = nums2[j]; } else if (j == n) { // nums2 的右边没有元素 minRight = nums1[i]; } else { minRight = Math.min(nums1[i], nums2[j]); } return (maxLeft + minRight) / 2.0; } } return 0.0; } ``` 时间复杂度为 $O(\log\min(m, n))$。
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值