zoukankan      html  css  js  c++  java
  • LeetCode——4. Median of Two Sorted Arrays

    一.题目链接:https://leetcode.com/problems/median-of-two-sorted-arrays

    二.题目大意:

      给定两个排序过的数组,求出两个数组的中位数,要求时间复杂度为O(log(m+n)).

    .题解:

      这是我在LeetCode上做的第一道hard级别的题目,没想到难度系数达到了5!被虐哭了。。。言归正传,这道题目的解题思路看起来并不难,难点在于它限定了时间复杂度,而且是对数级别的复杂度(说明优化程度已经相当高了)。这里,我给出三种解决方案,当然前两种时间复杂度并不满足要求(仅供参考思路);只有第三种方案符合题目要求:

    方法1:

      看到这种题目,首先想到的就是对两个数组进行合并,合并成一个有序的数组,然后根据数组元素个数的奇偶性求中位数即可。此处的排序方法有多种,最适合该题目的排序算法应该是归并排序。对于归并排序(默认升序),只需定义2个指针pa和pb分别指向数组a(长度为m)和数组b(长度为n),如果数组a的当前元素小,则*pa赋值给新数组,并且pa++;否则,*pb赋值给新数组,并且pb++;这么遍历指导遍历完某个数组,剩下的部分直接拼接到新数组末尾。最后找中位数。这么做的话时间复杂度为O(m+n),空间复杂度为O(1)。

    方法2:

      我们想要的仅仅是数组的中位数,而排序操作实际上消耗的代价太大,而我们没必要消耗如此大的代价。所以,此处不必对数组进行合并与排序,但还是借鉴方法1的思想,定义一个计数器cnt,每当*pa<*pb时,cnt++,pa++;每当*pa>=*pb,cnt++,pb++;当cnt==k时们就能找到相当于合并后数组的第k大的数了(此处包括本文所指的第k大的数指的是在数组中位置排序为k的数,而不是按大小来算的)。而中位数就是去中间大的那个数。这么做的话,时间复杂度也是O(m+n),空间复杂度O(1)。但是平均时间复杂度肯定是比方法1要小的。所以,方法2效率更好一些;遇到类似题目时,应该优先考虑方法2。

    方法3:

      前两种方法,都不能满足题目的要求。到底怎样才能找到指数级别的方法呢?首先我们来求一个更一般化的问题:求有序数组中第k个元素。而求有序数组的中位数实际就是这个问题的一个特例。具体一点说,假设数组的长度为n,当n为奇数时,即求第(n/2+1)个元素,当n为偶数时,即求第(n/2+1)和第(n/2)个元素,然后取他们的均值。下面我们从两个角度来分析:

      1.如何搜索两个有序数组中第k个元素呢,这里又有个技巧。假设数组都是从小到大排列,对于第一个数组中前p个元素和第二个数组中前q个元素,我们想要的最终结果是:(1)p+q等于k,(2)第一个数组中第p个元素(即A[p-1])和第二个数组中第q-1个元素(即B[q-2])都小于等于总数组第k个元素(或者第一个数组中第p-1个元素(即A[p-2])和第二个数组中列q个元素(即B[q-q]))。因为总数组中,必然有k-1个元素小于等于第k个元素。这样第p个元素或者第q个元素就是我们要找的第k个元素

      2.如果我们每次都能够删除一个一定在k位置之前的元素,那么我们需要进行k次。但是如果我们每次都删除一半呢?(即p和q都是k/2级别的)要达到log级别,我们应该充分利用数组的有序性,也就是类似于二分查找。

    结合角度1和角度2,我们可以通过二分法将问题规模缩小,假设p=k/2,则q=k-p=k/2,且p+q=k。这样A[0]~A[p-1]共有k/2个元素,如果第一个数组第p个元素小于第二个数组第q个元素(即A[k/2-1] < B[k/2-1]),我们不确定第二个数组中第q个元素是大了还是小了(即B[k/2-1]的大小不确定),但第一个数组的前p个元素肯定都小于目标(反证法可证),所以我们将第一个序列前p个元素全部抛弃(A[0]~A[k/2 -1]全部抛弃),形成一个较短的新数组(从A[k/2]开始)。然后,用新数组替代原先的第一个数组,再找其中的第k-p个元素(因为我们已经排除了p个元素,k需要更新为k-p,相当于总数组的第k个位置变成了总数组第k-p个位置(总数组变小了)),依次递归。同理,如果第一个数组中的第p个元素大于第二个数组中的第q个元素,我们则抛弃第二个数组的前q个元素(B[0]~B[k/2 -1]全部抛弃)。递归的终止条件有如下几种:

                                                  (1)较短数组中的所有元素都被抛弃,则返回较长数组中的第k个元素(在数组中下标是k-1)。

                                                  (2)第一个数组中的第p个元素等于二数组中第q个元素(即A[k/2-1] == B[k/2 - 1]),此时第一个数组中的第p个元素和二数组中第q个元素就是总数组中的第k个元素。(此时返回A[k/2-1]或者B[k/2-1]都可以)

                                                  (3)k == 1时,此时k不能再继续更新了,返回min(A[0],B[0])。(即本次传参时的数组A、B的起始位置)、

    此外,还要注意:每次递归不仅要更新数组起始位置(起始位置之前的元素被抛弃),也要更新k的大小(扣除被抛弃的元素)!代码如下:

    class Solution {
    public:
    double findMedianSortedArrays(const vector<int>& A, const vector<int>& B) {
    const int m = A.size();
    const int n = B.size();
    int total = m + n;
    if (total & 1)//判断数组长度是否为奇数
    return find_kth(A.begin(), m, B.begin(), n, total / 2 + 1);
    else
    return (find_kth(A.begin(), m, B.begin(), n, total / 2)+ find_kth(A.begin(), m, B.begin(), n, total / 2 + 1)) / 2.0;
    }
    private:
    static int find_kth(std::vector<int>::const_iterator A, int m,std::vector<int>::const_iterator B, int n, int k) {
    //始终保证m小于等于n
    if (m > n) 
    return find_kth(B, n, A, m, k);//此处一定不要忘记加return
    if (m == 0) 
    return *(B + k - 1);
    if (k == 1) 
    return min(*A, *B);
    //相当于把k分成p和q两部分
    int ia = min(k / 2, m), ib = k - ia;
    if (*(A + ia - 1) < *(B + ib - 1))
    return find_kth(A + ia, m - ia, B, n, k - ia);
    else if (*(A + ia - 1) > *(B + ib - 1))
    return find_kth(A, m, B + ib, n - ib, k - ib);
    else
    return A[ia - 1];
    }
    };
    

    该方法的时间复杂度为O(log(m+n)),空间复杂度为O(log(m+n))(递归过程中每次调用都为O(1),总共为O(log(m+n))),符合题目要求。

    注意:

    1.当数组A的长度小于k/2时,此时选择p=m;那么此时B的长度一定是大于k/2的,并且舍去的一定是B的k/2个元素。此处利用反证法证明:

      假设第K位置的元素在B的前k/2中,例如位置在索引i(i <= k/2-1)那么A必然拥有前k中的k -(i+1)个元素,而i <= k/2-1,则 i+1 <= k/2  , k - (i+1) >= k/2与条件:A的元素不够k/2矛盾,所以假设不成立,得证。

    所以此时舍去的必然是B的k/2个元素,下一步迭代的过程中k也会变小所以对于更新后的k,A的长度还是小于k/2的话,则会继续删除B的k/2个元素。

    2.总的来说,删除元素的过程主要包括三种类型:

                        (1).A[k/2-1] < B[k/2-1],此时删除B的前k/2个元素。

                        (2)B[k/2-1] < B[k/2-1],此时删除A的前k/2个元素。

                        (3)A的元素个数小于k/2的话,此时删除B的前k/2个元素。 (不过这一过程并没有在程序中显试的表现出来,而是隐含在程序处理的过程中,此处说明这一步只是为了更好的理解本算法;实际上

    int ia = min(k / 2, m)
    

      这行代码就是这一点的体现。)

    3.函数find_kth中的参数m、n和k,都表示的是数字的位置,而不是数组的下标;实际数组的下标要再减去1。

    =====================================================附录====================================================================

    1.A[k/2-1]<B[k/2-1]时,总数组第k位置的数,会有三种情况:

    (1)在A[k/2]~A[m-1]这一部分中。例如:

    A={2,3,5}

    B={1,7,8} 

    找到第4位置上的数,此时为5。

    (2)在B[0]~B[k/2-1]这一部分。例如:

    A={1,2,3,8}

    B={4,5,7}

    找到第4位置上的数,此时为4。

    (3)就是B[k/2-1]。例如:

    A={1,2,5}

    B={3,4,7}

    找到第4位置上的数,此时为4。

    2.当A[k/2-1]>B[k/2`-1]时,也一样。

    3.为什么当A[k/2-1] == B[k/2-1]时,A[k/2-1]或B[k/2-1]就是第k位置上的数?

    证明:

    对于A[0]~A[k/2-2],它们全部都小于等于A[k/2-1],所以它们一定是在总数组中第k位置之前。同理,对于B[0]~B[k/2-2],它们全部都小于等于B[k/2-1],所以它们也一定是在总数组中第k位置之前。这样就有(k/2-1)+(k/2-1) = k-2个已确定的数在k位置之前。又由于A[k/2-1] == B[k/2-1],而A[k/2-1]或者B[k/2-1]是大于这k-2个数中的最小的数,故第k位置的数一定是A[k/2-1]或者B[k/2-1]。

  • 相关阅读:
    几何变换
    图片移动、旋转、resize、
    load、display、save
    java基础—java三大集合ArrayList、HashSet、HashMap
    java基础—修饰符、抽象类、接口
    java基础——转义字符
    java基础—String类型常用api
    Python中 sys.argv[]的用法
    Java—this关键字
    在CMD中运行Python文件
  • 原文地址:https://www.cnblogs.com/wangkundentisy/p/7912833.html
Copyright © 2011-2022 走看看