LeetCode 刷题记录——04寻找两个有序数组的中位数
一 题目记录:
题目描述:
-
给定两个大小为m和n的数组nums1和nums2。要求找出着两个有序数组的中位数,并且要求时间复杂度为O(log(m+n))。且nums1和nums2不会为空
-
例:
-
Nums1 = [1,3] Nums2 = [2] 中位数:2.0
-
Nums1 = [1,2] Nums2 = [3,4] 中位数:(2+3)/2 = 2.5
-
思路:
- 暴力解法:将两个数组合并为一个数组,然后在新的数组中找中位数,根据奇偶性来取中位数。时间复杂度为O(m+n),因为要遍历两个数组,找中位数的时候,直接根据下标计算。
- 稍微好点的解法:和暴力解法的思路类似,对两个数组进行排序,但是不同的是,不需要将两个数组全部排序,从第一个元素开始,到(m+n)/2结束。时间开销比暴力解法好一点,但是仍然是O(m+n)量级
- 题目中出现了log(m+n),习惯性地往二分法的方向去想一想
- 二分实现:(看不懂的话请看后面的解疑部分)
- 首先,我们需要用到两个“指针”(其实是下标),i 和 j ,分别指向nums1和nums2中的元素,且保证 i + j = (m+n+1)/2 (这里记,K= (m+n+1)/2 )[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1XfPWoNA-1586251891845)(LeetCode 刷题记录——04寻找两个有序数组的中位数.assets/image-20200407161822674.png)]
- 【疑问0】:m 和 n 有讲究吗?需要确保m<n吗?
- 【疑问1】:为什么 要 i + j = (m+n+1)/2 ?
- 其次,i 初始化为(m)/2,然后的对应的 j = K - i ; (把 i 的左边和 j 的左边元素统称为左部分元素 ,其右边称为右部分元素)
- 然后,判断左部分元素是否都小于右部分元素,若是,就已经找到了中位数的位置;如不是,修改 i 和 j 的值,继续循环,直到满足条件为止
- 【疑问2】如何修改 i 和 j 的值?
- 【疑问3】满足条件指的是哪些?
代码:
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int m = nums1.size();
int n = nums2.size();
if(m>n){
return findMedianSortedArrays(nums2,nums1);//保证m<n
}
int m_start,m_end;
m_start = 0;
m_end = m; //较短数组的开始和结束部分
while(m_start <= m_end){
int i,j;
i = (m_start+m_end + 1)/2;
j = (m+n+1)/2 - i;
//判断
if(i!=0 && j!=n && nums1[i - 1] > nums2[j]){
m_end = i-1;
}
else if(j!=0 && i!= m && nums2[j-1] > nums1[i]){
m_start = i+1;
}
else{ //输出结果
int LeftMax;
//处理边界情况
if(i == 0)
LeftMax = nums2[j-1]; //取nusm2的左半部分的最大值
else if(j==0)
LeftMax = nums1[i-1]; //取nums1的左半部分的最大值
else
LeftMax = max(nums1[i-1] , nums2[j-1]); //如果不是处于边界上,就取左半部分最大值
//分奇偶讨论
if( (m+n)%2 ==1 ) //奇
return LeftMax;
int RightMin = 0;
if(i == m)
RightMin = nums2[j]; //取nusm2的右半部分的最小值
else if(j == n)
RightMin = nums1[i]; //取nusm1的右半部分的最小值
else
RightMin = min(nums1[i] , nums2[j]);//如果不是处于边界上,就取右半部分最小值
return (LeftMax+RightMin)/2.0; //偶数返回值
}
}
return -1.0;
}
};
-
可能会遇到的问题:当时我也是石乐志,居然没有注意到 if 判断的先后顺序,如下:
-
//位于循环体中的判断部分 //这是正确写法 if(i!=0 && j!=n && nums1[i - 1] > nums2[j]){ m_end = i-1; } else if(j!=0 && i!= m && nums2[j-1] > nums1[i]){ m_start = i+1; } //错误写法 if(nums1[i - 1] > nums2[j]&& i!=0 && j!=n){ m_end = i-1; } else if(nums2[j-1] > nums1[i] && j!=0 && i!= m){ m_start = i+1; } //错误写法中,会出现越界问题,因为此时有可能j==n 或者 i==m ,那么nums2[j]和nums1[i]就越界了
【解疑】
疑问0:
m 和 n 有讲究吗?需要确保m<n吗?
- 有讲究,显然一个小的数组能加快运行速度,因此这里选择小的数组作为nums1
疑问1:
为什么 i + j = (m+n+1)/2 ?
-
先看中位数的定义,即把一个数组分为左右两部分元素个数相等的部分,且左边元素的值 < 右边元素的值
-
假设nums1和nums2有序合并的为Num,那么Num的长度为m+n,其中位数需要分奇偶讨论,如下:
- 偶数:中位数就是(m+n)/2
- 奇数:中位数就是(m+n+1)/2
那么可以统一为:中位数(m+n+)/2,因为我们是用 Int 型,所以对于偶数 +1 或者不+1没有去区别
-
nums1和nums2是两个有序的数组,可以知道nums1的前 i 个元素和 nusm2的前 j 个元素都是比较小的元素,于是只要在 i + j = (m+n+1)/2 的前提下,再确保左部分元素 < = 右部分元素,那么就收工了
疑问2:
如何修改 i 和 j 的值?
- 在回答如何修改以前,我们先来看看会有多少种情况产生,然后再依次对各种情况进行操作
- 第一种:完美情况,左部分 <= 右部分
- 第二种:左部分 > 右部分
- 对于这种情况,有两种可能导致,如下:
- nums1的 i-1 位置上的元素 > nums2的 j+1 位置上的元素
- nums2的 j-1 位置上的元素 > nums1的 i+1 位置上的元素
- nums1的 i-1 位置上的元素 > nums2的 j+1 位置上的元素
- 对于这种情况,有两种可能导致,如下:
- 这就是不满足的情况,对于左部分 > 右部分 的情况,显然是需要增大左部分元素的最大值,减小右部分元素的最小值,针对上述的两种不同情况,解决如下:
- 对于第一种情况,我们需要减小 i 的值,而增大 j 的值
- 对于第二种情况,我们需要增大 i 的值,而减小 j 的值
- 增大多少,减小多少呢?
- 其实只需要解决一个问题,增大多少,因为 i + j = K,
- 回头看看 i 是如何确定的,我们一直在说二分法,到这里二分才出来,请准备好你的笔和纸,想不过来,就在纸上画出来。
- 以num1为例,首先我们直接二分,即 i = m/2 , 如果此时不满足条件,需要增大 i ,那么就意味着需要取nums1的后部分来进行二分,然后得到一个新的 i .文字很苍白,上图(由红色部分到绿色部分)[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-usTCFClq-1586251891846)(LeetCode 刷题记录——04寻找两个有序数组的中位数.assets/image-20200407170955269.png)]
疑问3:
满足条件指的是哪些?
-
除了之前说的条件,还要注意一些边界问题,比如当 i 或者 j 到了数组头;以及i 或者 j 到了数组末尾时
-
当 i 或者 j 到了数组头
- 以 i 到数组头为例,左部分的最大值就是nums2的 j-1 位置上的元素,直接比较nums2[j-1]和右部分最小值
-
当 i 或者 j 到了数组末尾
些? -
除了之前说的条件,还要注意一些边界问题,比如当 i 或者 j 到了数组头;以及i 或者 j 到了数组末尾时
-
当 i 或者 j 到了数组头
- 以 i 到数组头为例,左部分的最大值就是nums2的 j-1 位置上的元素,直接比较nums2[j-1]和右部分最小值
-
当 i 或者 j 到了数组末尾
- 以 i 到了数组末尾为例,右部分的最小值就是nums2[j+1],直接比较这个值与左部分的最大值即可