算法四寻找两个有序数组的中位数

时间:2024-02-01 20:18:54

算法四寻找两个有序数组的中位数

给定两个大小为 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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/median-of-two-sorted-arrays
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

这道题被列为难,主要是因为题目要求时间复杂度是O(log(m+n))。看到这个,在算法题里面基本上意味着二分法,也就是快速排序的变种。

我的解一,第一个解,并不符合时间复杂度的要求,先写出来测试一下对题目的理解是否正确。把两个有序数组通过插入排序的思路,放到一个数组中,然后直接求解。

排序的时候,随便拿一个数组作为基准遍历,也就是肯定一个数组的数据会被完全插入进去。再嵌套一个循环遍历第二个数组,把小的放入整合的数组中。如果数组一小,就不断把数组一的数据放入,如果数组二小,就把数组二的数据放入,碰到数组二大,就可以直接跳过,因为数组都是有序的。最后需要再判断一边数组二,因为有可能数组一是空的或是数组二有比数组一最大的数据还大的值,导致数组一遍历结束,数组二有数据没有插入。这种方法需要把数组排序一遍,所以是O(m+n),就是O(N)。

class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
            vector<int> sortv;
            sortv.reserve(nums1.size() + nums2.size());
            vector<int>::iterator iter2 = nums2.begin();
            for (auto iter1 = nums1.begin(); iter1 != nums1.end(); iter1++)
            {
                for (; iter2 != nums2.end(); iter2++)
                {
                    if (*iter2 < *iter1)
                    {
                        sortv.emplace_back(*iter2);
                    }
                    else
                    {
                        break;
                    }
                }
                sortv.emplace_back(*iter1);
            }
            for (; iter2 != nums2.end(); iter2++)
            {
                sortv.emplace_back(*iter2);
            }
            if (sortv.size() == 1)
            {
                return sortv[0];
            }
            if (sortv.size() % 2 == 0)
            {
                return (double)(sortv[sortv.size() / 2] + sortv[sortv.size() / 2 - 1]) / 2;
            }
            else
            {
                return (sortv[sortv.size() / 2]);
            }
    }
};

坑一,数组二需要再判断遍历一边

坑二,所有数组只有一个元素,导致判断奇偶求值的时候越界。

我的解二,既然数组是有序的,我们可以充分利用这个信息。只需遍历两个数组前一半的数据,不需要放到一个数组中,我们就可以得到结果。这种方法只遍历了一半,所以是O((m+n)/2),但也是O(N)。

思路,找出总数据一半的值,遍历两个数组,当前数据更小的数组索引加一,直到循环结束。用两个值记录前后两个数据,像窗口一样向前移动。判断奇偶,然后求所得值。

class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        int allsize = nums1.size() + nums2.size();
        int midsize = allsize / 2;
        double midnum1 = 0;
        double midnum2 = 0;
        int nums1index = 0;
        int nums2index = 0;
        for(int i = 0; i <= midsize; i++)
        {
            int nownum = 0;
            if(nums1index < nums1.size() && nums2index < nums2.size())
            {
                if(nums1[nums1index] < nums2[nums2index])
                {
                    nownum = nums1[nums1index];
                    nums1index++;
                }
                else
                {
                    nownum = nums2[nums2index];
                    nums2index++;
                }
            }
            else if(nums1index < nums1.size())
            {
                nownum = nums1[nums1index];
                nums1index++;
            }
            else if(nums2index < nums2.size())
            {
                nownum = nums2[nums2index];
                nums2index++;
            }
            if(i == 0)
            {
                midnum1 = nownum;
                midnum2 = nownum;
            }
            else if(i == 1)
            {
                midnum2 = nownum;
            }
            else
            {
                midnum1 = midnum2;
                midnum2 = nownum;
            }
        }
        if(allsize % 2 == 0)
        {
            return (midnum1 + midnum2) / 2;
        }
        else
        {
            return midnum2;
        }
    }
};

坑一,只有一个数据时,没有设置midnum2,导致数据是初始值。

一直不理解o(log(m+n))是什么意思,感觉上面一般遍历已经很不错了,并且没额外开销。看到评论,捋了好久,发现更快的思路还是基于排序好的数组的条件。我是一个一个的判断跳转,既然是排序好的,完全可以一半一半的跳,不需要一个个的跳,这一样正好符合快速排序的思路,也符合o(log(m+n))的时间要求。既然一半一半的跳,怎么跳呢,官方和评论区中有两个思路,第一个是按照最小k值的思路求解,好理解,就是求第一半个数组的数值,如果是两个数组,就分别是一半的一半,然后处理一些特殊情况;另一个是根据滑动分割的思路,证明和解释看着好麻烦,实际上可以理解,中间有一道墙,把两个数组分别分割成两块,要求左边两块加起来的数组个数是总个数的一半,那么左边最大的两个值就可以得出我们要的数据。要求左边两个数组的所有数据全部小于右边两个数组的所有数据,如果是一个数组,很好做,两个数组的话,考虑到数组a与数组b不相关,所以要把两个数组左右移动一下,保证满足要求。

评论解一,最小k值思路,作者为了避免奇偶数的判断,做了调整,分别求总数加1和加2位置的数,奇数就是中间的求了两次,偶数就是分别求了中间两个数据。然后分别从两个数组的头开始,一半半的查找数据。如果其实索引已经大于数组的长度,那就说明这个数组的数据全都不符合要求,直接返回另一个数组中对应的数据。如果下次跳转的索引大于数组长度,那就不设置跳转,还是从这次数组的开始索引计算(这种情况是两个数组数据相差很大,只能慢慢过滤大的数组到合适的位置,小的数组暂时不变动)。如果获取最小值数据是1,那么就返回最小的一个。如果是正常,那么就获取两个数组的跳转位置的数值,因为是从前到后,从小到大排除,所以大的数据的位置不变,跳转小的数组的索引到当前step的一半。

class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        int m = nums1.size();
        int n = nums2.size();
        int left = (m + n + 1) / 2;
        int 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)
        {
            if(nums1[i] < nums2[j])
            {
                return nums1[i];
            }
            else
            {
                return nums2[j];
            }
        }
        int midval1 = 0;
        if((i + k / 2 - 1) < nums1.size())
        {
            midval1 = nums1[i + k / 2 - 1];
        }
        else
        {
            return findKth(nums1, i, nums2, j + k / 2, k - k / 2);
        }
        int midval2 = 0;
        if((j + k / 2 - 1) < nums2.size())
        {
            midval2 = nums2[j + k / 2 - 1];
        }
        else
        {
            return findKth(nums1, i + k / 2, nums2, j, k - k / 2);
        }
        if(midval1 > midval2)
        {
            return findKth(nums1, i, nums2, j + k / 2, k - k / 2);
        }
        else
        {
            return findKth(nums1, i + k / 2, nums2, j, k - k / 2);
        }
    }
};

坑一,我自己实现这个的时候,在传递i和j,也就是获取第几位数据,传递的是m+n和(m+n-1)/2,这样正好对应数组下标索引,但是发现实现的时候有问题,原来是理解问题,这个传递的是获取第几个数据,并不是针对数组索引,所以要加1加2处理。

坑二,在返回数据的时候,我们看到都会减一,因为c++数组索引从0开始,但是在递归调用的时候,并没有减一,因为表示过滤了多少个数据,从第几个开始判断,如果减一,导致排除了k/2个数据,然后又添加了一个,导致后面判断的时候,没办法判断最大的数据,出现错误

评论解二,这种方法是中间一个分界线,左边正好是k(数组个数的一半)个元素,左边的数据都比右边的小,因为有两个数组,所以需要数组一数组二的数组左右滑动挪一下来保证这个条件。左边是k个数据简单,难的是左边两个数组的数据都比右边的少。比如数组a,左边的数据是aleft,右边的数据是aright,那么aleft<=aright这个是成立的,但是bleft<=aright和aleft<=bright就需要判断。如果bleft>aright,那么就需要b的数组向右挪动,挪动了一个数据后,左边少了一个数据,那么a的数组向左挪动,以此类推。实际上这个算法并不符合o(log(m+n)),因为开始的时候从两个数组的中间各一半分割,但是碰到bleft>aright和aleft>bright的时候,需要一位一位数据挪动,这样是O(N)。这是就需要再利用题目的条件,数组是排序好的,我们就可以一半一半的跳,而不需要一个一个的跳,这样对于数组少的时候作用不大,对于数组很长时,效果更好。就是发现条件不符合的时候,就向剩下的一半的一半跳跃,因为保证左边加起来数据等于数组一半,所以另一个数组跳跃直接减掉上一个就好了。

#define max(a,b) (((a) > (b)) ? (a) : (b))
#define min(a,b) (((a) < (b)) ? (a) : (b))
class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        //因为要基于一个计算,如果用短的数组减长的,可能导致负数
        if (nums1.size() > nums2.size())
        {
            return findMedianSortedArrays(nums2, nums1);
        }
        //以nums1作为标准,nums1确认了,nums2的位置也确认了;
        int amin = 0;
        int amax = nums1.size();
        while (amin <= amax)
        {
            int amid = (amin + amax) / 2;
            int bmid = (nums1.size() + nums2.size() + 1) / 2 - amid;//加一除以二是为了把数据分到左边方便计算
            if (amid != 0 && bmid != nums2.size() && nums1[amid - 1] > nums2[bmid])
            {
                amax = amid - 1;
            }
            else if (amid != nums1.size() && bmid != 0 && nums2[bmid - 1] > nums1[amid])
            {
                amin = amid + 1;
            }
            else
            {
                int maxleft = 0;
                if (amid == 0)
                {
                    maxleft = nums2[bmid - 1];
                }
                else if (bmid == 0)
                {
                    maxleft = nums1[amid - 1];
                }
                else
                {
                    maxleft = max(nums1[amid - 1], nums2[bmid - 1]);
                }
                if ((nums1.size() + nums2.size()) % 2 == 1)
                {
                    return maxleft;
                }
                int minright = 0;
                if (amid == nums1.size())
                {
                    minright = nums2[bmid];
                }
                else if (bmid == nums2.size())
                {
                    minright = nums1[amid];
                }
                else
                {
                    minright = min(nums1[amid], nums2[bmid]);
                }
                return (maxleft + minright) / 2.0;
            }
        }
        return 0;
    }
};