力扣详解(4寻找两个正序数组的中位数)

一、题目描述

略,请自行查看力扣第 4 题

二、解题

(1)能通过,但是时间复杂度不符合要求

这种方法是最简单的一种,空间复杂度是 O( m + n )但是时间复杂度是 O( (m + n) log(m + n) ),题目要求的时间复杂度是 O( log(m + n) )。
思路很简单,先开辟一个辅助数组,长度为两数组长度之和,定义三个计数器,两个在旧数组上移动,一个在新数组上移动,然后比较两数组,每次将小的数放入新数组,最后根据新数组长度奇偶性返回数即可。注意最后要释放开辟的数组空间,还一点就是要考虑某一数组为空的情况。
代码:

class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) 
    {
        int m = nums1.size(),n = nums2.size();//两数组元素个数
        //某数组长度为 0 的情况
        if(m == 0)//nums1 为空数组
        {
            if(n % 2 == 0)  return (double)(nums2[n / 2 - 1] + nums2[n / 2]) / 2.0;
            else  return nums2[n / 2];
        }
        if(n == 0)//nums2 为空数组
        {
            if(m % 2 == 0)  return (double)(nums1[m / 2 - 1] + nums1[m / 2]) / 2.0;
            else  return nums1[m / 2];
        }
        //创建辅助数组
        int *p = new int[m + n];
        //并入新数组
        int k = 0;//新数组计数器
        int i = 0,j = 0;//两数组计数器
        while(i < m && j < n)  p[k++] = (nums1[i] < nums2[j]) ? nums1[i++] : nums2[j++];
        //剩余数据并入新数组
        while(i < m)  p[k++] = nums1[i++];
        while(j < n)  p[k++] = nums2[j++];
        //计算中位数
        if(k % 2 == 0)  return (double)(p[k / 2 - 1] + p[k / 2]) / 2.0;
        else  return p[k / 2];
        delete []p;
    }
};

(2)原地算法,但时间复杂度依旧为O(m + n)

什么意思?首先要注意题目说明了两数组有序,不需要考虑无序的问题,我们只要知道两个数组长度之和对半开是多少,然后定义两个计数器分别从两个数组的首部开始往后走,最终到达对半开的位置,这其中定义两个标记变量,每次循环都更新,最后根据两数组长度之和奇偶性返回值即可,语言可能稍显晦涩,先看代码再看几个主要的图:

class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) 
    {
        //原地算法,不归并入新数组,直接在两个数组上移动,找到中位数
        //定义两数组计数器
        int aindex = 0,bindex = 0;
        //定义记录总共移动了多少个数的变量
        int i;
        //定义记录中位数,因为偶数和奇数取中位数不一样,为了代码不复杂难看,直接定义两个标记变量,标记最中间一个和其后面一个,在返回中位数时奇数直接不使用后面那个即可
        int f1,f2;
        //两数组长度
        int m = nums1.size();
        int n = nums2.size();
        for(i = 0;i <= ((m + n) / 2);++i)
        {
            f1 = f2;
            if(aindex < m && (bindex >= n || nums1[aindex] < nums2[bindex]))  f2 = nums1[aindex++];
            else  f2 = nums2[bindex++];
        }
        if(((m + n) & 1) == 0)  return  (f1 + f2) / 2.0;
        else  return  f2;
    }
};

①刚开始,标记变量 f1 和 f2 都为 0,两数组计数器 aindex 和 bindex 都指向两数组首位,两数组长度和中位为 5,i 为 0,i 是用来记录在两数组总共走了多少路,相当于两数组合并到一起后,从数组首位到中位走了多少路:
在这里插入图片描述

②i = 0,满足循环条件,进入循环满足 else 语句,执行后,注意这里的 f2 已经更新,只是因为我们数组里元素本来就是 0,所以执行后 f2 的值还是 0,不要认为没有改变:
在这里插入图片描述
③第二次循环,i = 1,满足条件进入循环,满足 else 语句,执行后:
在这里插入图片描述
④第三次循环,i = 2,满足条件进入循环,满足 if 语句,执行后:
在这里插入图片描述
到这里应该就已经很清晰明了了。
⑤最后退出循环时,i = 5,f1 = 4,f2 = 5,因为两数组和为 11,是奇数,所以最后只返回 f2

一个问题:

问:为什么要两个标记变量
答:因为数组长度之和有奇数和偶数两种情况,如果按奇数和偶数来循环则代码隆长不好看。分析一下,如果是奇数,最后我们取的是 n / 2,如果是偶数,最后我们取的是 n / 2 - 1 和 n / 2 两个,可以发现奇数和偶数都取到了 n / 2,那么我们可以定义两个标记变量,一个标记 n / 2 - 1,一个标记 n / 2,最后返回中位数时,如果是奇数就返回 n / 2 位置的即可,如果是偶数就返回两个位置之和除二即可。这样循环时就不用分奇偶来写了,就可以奇偶都是一段代码来实现,从而代码更美观、更易读。

几个细节:
①注意循环内 if 条件的 || 处只能是 bindex <= n 在前,否则就会出错,为什么呢?先看一个案例:nums1[2] = {0,0},nums2[2] = {0,0}
首先,前面省略一些步骤,第二次循环结束是这样的:
在这里插入图片描述
可以注意到,bindex 此时等于 2,而 nums2[2] 是访问不到的,所以如果 if 条件内 || 的前部写的是 nums1[aindex] < nums2[bindex],而 || 的运算顺序是从左至右,因此判断 nums[2] 时出现数组越界,就会报错,改善方法就是将 bindex <= n 放在前部,先判断这个,此时 2 <= 2 成立,则 || 判断为 1,则后部不会再执行,也就不会出现数组越界问题了。
②可以用 & 代替 %,位运算的速度都要比正常运算快,数据量较小时看不出差别,数据量很大时就能看出明显差距了。我写了一篇叫“优雅的掌握位运算”的 blog,在《算法笔记》专栏里,不懂位运算的可以去我主页查看。
③除数是2.0。因为返回的要是 double 类型,因为 f1 + f2 肯定是整型,要算出小数就要求除数和被除数至少有一个是实型,所以要除以 2.0,不懂运算符的可以去我主页看看,我写了一篇叫“聊一聊运算符”的 blog,在《零零散散的小知识》专栏里。
④f1 和 f2。注意 f2 赋值给 f1 的操作必须实在第二次循环开始位置,也即第二次循环要改变 f2 的值之前将上一次 f2 的值赋值给 f1,如果在循环的最后就会有一个问题,在最后一次循环结束时,将最后得到的 f2 赋值给 f1,然后 ++i,接着循环条件不成立,退出循环,那退出循环后 f1 和 f2 的值就是相等的,

(3)二分法、排除法,递归实现,O(log(m + n))

题目要求时间复杂度为 O(log(m + n)),有 log,所以容易想到要用二分法。
循环一次减少 k / 2 个元素,所以时间复杂度是 O(log(k)),又因为 k = (m + n) / 2,所以最终的复杂也就是 O(log(m + n))。
怎么做?两个数组站在原地不动,通过一个变量 k 将放一起时数组首部到中位数位置的区间二分分配给两个数组,根据两数组对应 k / 2 位置上数的大小,排除小的那个数组的首部到 k / 2 位置的数,调用递归,如此不断减小 k 的值(即放一起的数组首部到中位数位置中间的元素个数),直到某个数组全部排除或 k == 0 时跳出递归。
先看代码再看图:

class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1,vector<int>& nums2) 
    {
        //二分查找,依次排除
        //两数组长度和 len,为了奇偶情况一起处理,设置两个变量 left 和 right 作为第 k 大,不懂先看图
        //递归处理,形参是两个数组首地址,排除后的首位置和末尾位置,以及第 k 大,可以认为 k 就是排除后的数组(是看作放一起的数组,不是单独的两个数组)从首位置到中位数剩余元素个数(包括中位数),与索引值区分开
        //递归函数考虑两数组大小问题,如果递归过程出现数组排除空的情况,一定是小的数组
        //递归出口有两个:一个是特殊情况,也就是一个数组为空后可以直接返回另一数组第 k 大减一位置的值
        //另一个是通常情况,当 k = 1 时,说明当前剩余 1 个数,即当前位置就是中位数位置,所以只要比较返回两数组该位置小的一个即可
        //递归体:设置两个变量,分别存两数组二分位置索引值,然后比较两数组该位置数值大小,根据大小选择排除的数组区间
        int m = nums1.size();
        int n = nums2.size();
        //处理奇偶情况
        int left = (m + n + 1) / 2;
        int right = (m + n + 2) / 2;
        return (findMedian(nums1,0,m,nums2,0,n,left) + findMedian(nums1,0,m,nums2,0,n,right)) / 2.0;
    }
private:
    double findMedian(vector<int>& nums1,int start1,int end1,vector<int>& nums2,int start2,int end2,int k)
    {
        //未排除数组长度
        int len1 = end1 - start1;
        int len2 = end2 - start2;
        //使长度小的作为第一个(nums1)
        if(len1 > len2)  return findMedian(nums2,start2,end2,nums1,start1,end1,k);
        //第一个数组全部排除的情况,出口一
        if(len1 == 0)  return nums2[start2 + k - 1];
        //出口二,一般情况
        if(k == 1)  return findMin(nums1[start1],nums2[start2]);
        //递归体
        //两数组离中位数最近的位置,为什么不直接用 k ?因为可能存在一种情况就是某一数组实际剩余元素个数不足 k,所以只能使用数组实际元素个数
        //因为 k 表示两个数组放一起后到中位数位置的元素个数,所以在 i 和 j 赋值时是 k / 2,注意也不是两数组平分 k,而是两数组都是 k 除以 2 后的值
        int i = start1 + findMin(len1,k / 2) - 1;
        int j = start2 + findMin(len2,k / 2) - 1;
        if(nums1[i] > nums2[j])  return findMedian(nums1,start1,end1,nums2,j + 1,end2,k - (j - start2 + 1));
        else  return findMedian(nums1,i + 1,end1,nums2,start2,end2,k - (i - start1 + 1));
    }
    int findMin(int a,int b){return (a > b) ? b : a;}
};

在 findMedianSortArrays 函数中,return 处调用两次 findMedian 函数,区别是传的 k 一个是 left 一个是 right,因为 m + n 是奇数,所以 left 和 right 相等,因此两次的返回值相同,再除以二,也相同,也就相当于是取了最中间那个(对比着来看,可以知道偶数的 left 和 right 不想等,所以返回最中间两个数,再除二):
在这里插入图片描述
在这里插入图片描述
调用 findMedian 函数时(因为相同,我们只看一种情况),刚开始:

在这里插入图片描述
前三个 if 判断都不满足,i = j = 2,可以看到 k = 6 时想当于两个数组放一起后从数组首部到中位数处有 6 个元素,所以将其分开后两个数组都占 3 个元素:
在这里插入图片描述
接着比较,关键的地方来了,因为虽然说两个数组相当于平分了 6 个元素,但因为两个数组数的分布不一样,所以要判断 i 和 j 位置的数的大小,从而排除一个数组的首部(start —> i / j),这里 nums1[i] < nums[j] 说明数组 nums1 的前面 start1 到 i 位置的数一定都小于数组 nums2 的 j 位置的数,所以可以将数组 nums1 的首部排除,后面就是不断缩小 k 然后执行重复操作了:
在这里插入图片描述
第二次递归刚开始的情况:
在这里插入图片描述
依旧前三个 if 判断不成立,接着 i = 3,j = 0,则:
在这里插入图片描述
接着 nums1[i] > nums2[j],执行 else 语句,依次类推,直到最后一层递归:
在这里插入图片描述
执行第三个 if 语句,返回 nums2[start2] = 2。
其实,将上面的栗子中两个数组放到一起再看看就很清晰明了了:
第一次排除
在这里插入图片描述
第二次排除
在这里插入图片描述
第三次排除
在这里插入图片描述
第四次返回中位数
在这里插入图片描述

对于我上面说的 “ 因为虽然说两个数组相当于平分了 6 个元素,但因为两个数组数的分布不一样,所以要判断 i 和 j 位置的数的大小,从而排除一个数组的首部(start —> i / j)” 还是看不懂?举个栗子吧:
如果说不按这句话这么做,就相当于不判断 i 和 j 位置数的大小随机排除一个数组的首部,就是这样:
在这里插入图片描述
放到一起看就是:
在这里插入图片描述
这明显是不合理的,所以你应该懂了吧。

解释几点:

①详细解释一下第 k 大
答:其实第 k 大就可以看作是从头开始中位数排第几,如上面的栗子,中位数在第 6 位,所以 k = 6,而中位数的索引就是 k - 1,这很好理解。

②为什么奇偶可以一起考虑?
答:我们知道,奇数个元素的数组其中位数在最中间,即 n / 2 位置,偶数个元素的数组中位数在 n / 2 - 1 和 n / 2 位置,而对于奇数,(n + 1) / 2 和 (n + 2) / 2 相等,也就是说这两个的索引值是一个位置,而我们知道 (x + x) / 2 = x,对于偶数,(n + 1) / 2 和 (n + 2) / 2 是相邻两个位置的索引,将两个索引值平均就是我们的中位数,所以奇数和偶数可以通过 (n + 1) / 2 和 (n + 2) / 2 一起实现,就不用再分开讨论了

③为什么要考虑 len1 > len2 ?
答:可以看到其下边有一个 if 语句判断 len1 == 0,首先,要考虑两个数组中有一个数组在 k = 1 之前就已经排除空了,这时可以直接返回另一个数组的未排除区间的 k 位置的值,为了便捷,我们想是否可以像前面考虑奇偶性一样,所以如果有可能某个数组被排除空,我们就让它一定是 nums1 数组,而为了达到这一目的,我们就要判断哪个数组长度小,然后把小的那个变为 nums1,因为如果有数组要被排除空,那肯定是短的那一个数组。

④为什么 i 和 j 那里是 k / 2 ?
答:因为 k 是相当于两个数组放一起后从数组首位置到中位数位置的元素个数,而现在是两个数组分开来,所以说是 k / 2。

(4)统计学方法,原地,O(log(min(m + n)))

统计学中中位数定义为:将一个集合划分为两个长度相等的子集,其中一个子集中的元素总是大于另一个子集中的元素。
题目要求返回两个数组放一起的中位数,我们想是否可以原地找到中位数,也就是说不合并数组。
以下先通过偶数情况(两数组之和为偶数)解释原理:
我们通过分割,分别将两个数组各自再分成两个区间,因为题目给的数组有序,所以左区间一定小于右区间,并且左区间的最大值在分割线左侧,右区间最小值在分割线右侧,而因为两个数组数据分布情况不同,所以要先将两个区间合法化,什么意思?
比如 nums1[5] = {0,1,1,2,3} nums2[5] = {5,6,7,8,9} 这时从中间切开,两个数组长度之和为偶数,两数组分别分为 0,1 加上 1,2,3 和 5,6,7 和 8,9,其中 1 和 8 与分割线重合,这时可以看到元素 1 和 7 都位于两数组最中间,按道理中位数是 (1 + 7) / 2 = 4
在这里插入图片描述
但是这明显不合理,两个数组放到一起看看,可以看到放到一起后 1 和 7 并不是数组最中间两个
在这里插入图片描述
所以说现在的区间划分不是正确的,我们考虑到不仅是两个数组分别都分割线的左边小于等于右边,我们还要交叉小于等于,即 绿色 <= 红色,黑色 <= 黄色,即左区间数组一的最大值小于等于右区间数组二的最小值(右区间左闭右开,左边包括边界,即包括分割线处的数),左区间数组二的最大值小于等于右区间数组一的最小值,
在这里插入图片描述
(奇数情况是下面这样的,可以看到左区间要比右区间多一个元素,所以奇数情况,达到理想状态后,中位数就是左区间的最大值)
在这里插入图片描述
所以我们考虑移动分割线达到这个目的,但是左右区间元素个数依旧不变,达到理想状态后,中位数就是左区间的最大值和右区间最小值之和除二
在这里插入图片描述

上面这个是一种特殊情况,即某个数组完全小于或大于中值(中位数),通常情况是下面这种
在这里插入图片描述
上面这种情况是怎么实现的呢?是通过移动 i,然后 j = (m + n + 1) / 2 - i 也随之改变,如果 i 往左边移动一次,则相应 j 往右边移动一次,因为前面讲了左右区间元素个数保持相等不变(这里说的都是偶数情况,奇数情况对应左区间比右区间多一个数),但是这里面临一种情况,改变一下栗子,如果两个数组元素个数不相等并且是数组一大于数组二会发生什么呢?不会发生什么,只是为了优化时间,因为我们是移动数组一的分割线 i,所以如果数组一越短,也就可能越快到达理想位置。
所以我们先判断两个数组的长度,将长度小的作为数组一。
为什么 j = (m + n + 1) / 2 - i,首先 (m + n + 1) / 2 就是合并后数组最中间,也是我们刚开始第一次分割的位置,- i 就是 i 改变了多少(偏离第一次分割的位置多少),j 也就对应偏离同样的距离,从而实现左右区间元素个数相等不变,这里是二分的思想,i 是不断二分的,直到 iMin > iMax
代码:

class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1,vector<int>& nums2) 
    {
        //首先从最中间用 i 和 j 去切开两个数组
        //i 和 j 左边的看作左区间,右边看作友区间,如果是偶数,左右区间元素个数相等,奇数左区间多一个数
        //对于两数组长度相加后为奇数的,中位数就是 i 和 j 左边中最大的一个数
        //偶数就再加上 i 和 j 右边最小的那个数再除二
        //因为两个数组数据分布情况是不一样的,比如 nums1[5] = {0,1,1,2,3}  nums2[5] = {5,6,7,8,9}  这时从中间切开,两个数组长度之和为偶数,两数组分别分为 0,1 加上 1,2,3 和 5,6,7 和 8,9
        //如果说直接返回 i 和 j 左边最大值和右边最小值之和除二就会出错,也就是说返回 (7 + 1) / 2 ,这明显不合理
        //偶数情况下,所以我们要保证左右区间元素个数相等不变的前提下,让 i 和 j 在两数组上相反移动,也就是说 i 往后走一步,j 就往前走一步,这样左右区间元素个数就始终相等不变
        //实现随着移动,最终 i 左边的数小于等于 j 右边的数,j 左边的数小于等于 i 右边的数,相当于将合并的数组放一起找到中位数位置,然后再将两个数组抽离出来后,也就是说此时 i 和 j 两侧的数在合并后的数组中是相邻的,所以中位数在这里取
        //奇数情况要保证左区间始终比右区间多一个数
        //两数组长度
        int m = nums1.size();
        int n = nums2.size();
        //移动的数组越短达到理想状态时间更短
        if(m > n)  return findMedianSortedArrays(nums2,nums1);
        //辅助移动 i 的变量,对于偶数情况,i 移动一定量,j = (m + n + 1) / 2 - i 也随之移动相等的相反距离,就保证了左右区间元素个数始终相等不变
        //j = (m + n + 1) / 2 - i 可以同时处理奇偶两种情况,因为偶数加一除二结果不变
        int iMin = 0,iMax = m;//刚开始将数组从中间切开
        while(iMin <= iMax)
        {
            int i = (iMin + iMax) / 2;
            int j = (m + n + 1) / 2 - i;
            // i 右移
            if(j != 0 && i != m && nums1[i] < nums2[j - 1])  iMin = i + 1;
            // i 左移
            else if(i != 0 && j != n && nums1[i - 1] > nums2[j])  iMax = i - 1;
            //符合要求
            else
            {
                //左区间最大值
                double maxLeft = 0;
                //特殊情况
                if(i == 0)  maxLeft = nums2[j - 1];
                else if(j == 0)  maxLeft = nums1[i - 1];
                //非特殊情况
                else  maxLeft = max(nums1[i - 1],nums2[j - 1]);
                if((m + n) & 1)  return maxLeft;

                double minRight = 0;
                if(i == m)  minRight = nums2[j];
                else if(j == n)  minRight = nums1[i];
                else  minRight = min(nums1[i],nums2[j]);
                return (maxLeft + minRight) / 2.0;
            }
        }
        return 0.0;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值