题目描述
给定两个大小分别为 m
和 n
的正序(从小到大)数组 nums1
和 nums2
。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n))
。
题解
如果不考虑时间复杂度,可以将两个数组遍历一遍,升序合并成一个新的数组,返回下标为n/2的数;这个过程中,会将两个数组中的数逐个比较,其实只需要比较到 (m+n)/2 时就可以了,但时间复杂度还是O(m+n)
换一种想法(此处[]内的数不是下标,是第几个数)
求中位数,也就是求第K大的数,先比较两个数组第K/2个数,如果A[K/2]大于B[K/2],那么B[K/2]之前的数字(包括B[K/2])都不可能是第K大的数,可以直接不看,从B[K/2+1:m]中找到 K = K-K/2(这个结果不一定是K/2,因为还会向下取整)大的数。递归进行,直到K=1,比较两个数组有效长度的首个数字,最小值为结果。
需要特殊处理的情况
- 如果 A[k/2-1] 或者 B[k/2-1] 越界,那么我们可以选取对应元素数组中的最后一个元素。在这种情况下,排除数字的个数就不是 k/2 了,而是 A.length - index_a
- 如果一个数组为空,说明该数组中的所有元素都被排除,我们可以直接返回另一个数组中第 K 小的元素
我的思维误区
我一直想着,把A[K/2]和B[K/2]之前的数字都删掉,这虽然不违背找中位数,但是,没法下一步递归。
代码
#include <iostream>
using namespace std;
int find_mid(int nums1[],int nums2[],int k,int index1,int index2,int m,int n)
{
//如果一个数组为空,也就是说,说明该数组的所有元素都被排除,我们可以直接返回另一个数组中第K小的元素
if(index1 >= m )
{
return nums2[k];
}
if(index2 >= n)
{
return nums1[k];
}
if(k == 1)
{
return nums1[index1] < nums2[index2] ? nums1[index1]:nums2[index2];
}
//如果加上 k/2 后数组越界
if(k/2+index1-1 >= m)
{
if(nums1[m-1] > nums2[k/2+index2-1])
{
return find_mid(nums1,nums2,k - (m-index1),index1,index2+k/2,m,n);
}
else
{
k = k -(m-index1);
return nums2[k];
}
}
if(k/2+index2-1 >= n)
{
if(nums2[n-1] > nums1[k/2+index1-1])
{
return find_mid(nums1,nums2,k - (n-index2),index1+k/2,index2,m,n);
}
else
{
k = k -(n-index2);
return nums1[k];
}
}
//先比较数组中第 k/2 的数
if(nums1[k/2+index1-1] > nums2[k/2+index2-1])
{
//nums2可以去掉前几个数,并更新index
return find_mid(nums1,nums2,k - k/2,index1,index2+k/2,m,n);
}
if(nums1[k/2+index1-1] <= nums2[k/2+index2-1])
{
//nums1可以去掉前几个数,并更新index
return find_mid(nums1,nums2,k - k/2,index1+k/2,index2,m,n);
}
}
int main()
{
int m;
int n;
cin >> m;
cin >> n;
int nums1[m];
int nums2[n];
for(int i = 0 ; i < m ; i++)
{
cin >> nums1[i];
}
for(int j = 0 ; j < n ; j++)
{
cin >> nums2[j];
}
double result ;
//当数组个数为奇数时,
if((m+n) % 2 == 1)
{
result = find_mid(nums1,nums2,(m + n) / 2 + 1,0,0,m,n);
}
else
{
result = find_mid(nums1,nums2,(m + n) / 2 + 1,0,0,m,n) + find_mid(nums1,nums2,(m + n) / 2,0,0,m,n);
result = result / 2.0;
}
cout << result << endl;
return 0;
}
方法二
分割线
首先:思考中位数的作用,分割线的作用,中位数左边的数全部小于中位数右边的数,并将集合划分成长度相等的两个部分。
分割效果如图所示(left_part中有若干个数,right_part中有若干数)
max(left_part) <= min(right_part)
如果两个数组长度之和为奇数,那么max{A[i-1],B[j-1]}即为所求(因为在奇数的情况下,我们让左边多了一个)。如果数组长度之和为偶数,那么求max{A[i-1],B[j-1]}和min{A[i],B[j]}的平均值。
i 与 j 的关系
- 当A和B的总长度是偶数时,len(left_part) = len(right_part)
- 当A和B的总长度是奇数时,len(left_part) = len(right_part)
故:i + j = m - i + n - j (当 m+n 为偶数时) 或者 i + j = m - i + n - j +1 (当 m+n 为奇数时),我们可以化简并统一成 : i + j = (m + n + 1) / 2 {因为,对于偶数来讲,a/2和(a+1)/2最后的结果是一样的},j = (m + n + 1)/2 - i {注意,如果 i 的范围是 m 和 n 中更大的那个数,最后可能减出一个负数,所以,要减去的是数组元素个数少的那个}
所以,我们枚举 i 在 [0,m] 范围内,由等式得到 j
需要找的关系
我们需要找的是
它等价于{为什么等价? ->把i+1代入A[i-1]<=B[j],其实就是 B[j-1] >= A[i],那么就不满足条件1 }
所以,我们使用折半查找,如果遇到 A[i - 1] > B[j] 的,说明 i 选的太大了,right = i-1 继续选;如果遇到 A[i - 1] <= B[j] 记录此时的meda左 和 meda右 (最接近分割线的两个值),left = i + 1,继续选,直到 left > right ,就找了最大的 i 。
边界情况
为了简化分析,当 i = 0 时,A[i - 1] 怎么办 : 我们的表达式,必会有 max{A[i - 1], B[i-1]}这一步,当前一部分没有那个数组时,分割线左边只剩下另一个数组的元素,将此时的A[i - 1]设为负无穷,不影响找出左边最大值。
来个三目判断,当 i=0时,A[i - 1] = INT_MIN
时间复杂度
O(log(m))