在刷LeetCode的过程中,遇到求两个有序数组中位数的问题。刚开始看到题目标记为“困难”就被吓住了,最终也没能琢磨出来怎么实现题目要求的复杂度,看来这道题标记为困难还是有一定道理的2333~ 翻看官方题解以及网友给出的方法,才有点明白该怎么思考问题。不过官方题解也好,网友的方法也好,感觉都是先引入了一个好的思路,之后又绕来绕去地讨论。最终对以上方法中的一些细节还是没能理解的很透彻。邻近中午,赶着吃饭,就准备抛开网上的思路,看看能不能简单点处理,所幸,好像是把问题解决了。这里把思路写出来跟大家分享。
题目:
给定两个大小为 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
思路:
- 对一个求数组中位数的问题,首先能想到的是对数组排序,但排序操作的时间复杂度显然不能满足要求。这里不妨先考虑一下“中位数”的意义:中位数将一个序列分为长度相等的两个子序列,其中一个子序列里的所有元素都小于“中位数”,另一个子序列里的所有元素都大于“中位数”。注意这里的关键词是“长度相等”,在接下来的分析中会用到这个词。
- 下面再来讨论数组分割的问题:
对一个长度为k的有序数组A来说,将它分为两部分的方法有k+1种(图中#i为选定的分割位置)。当选择#i为分割位置,左侧部分的最大值应该为A[i-1],右侧部分的最小值为A[i]。这里有两种特殊情况:若选择#0作为分割位置,数组的左侧部分为空;选择#4作为分割位置时,也是类似的情况。这种特殊情况下左、右两侧部分的最值有特殊的规定,后面会进行讨论。
考虑到给出的两个数组nums1和nums2都是有序数组,将nums1从某一位置分割后左侧部分(记为Lpart1)将小于其右侧部分(记为Rpart1),nums2在某一位置分割后左侧部分(记为Lpart2)同样小于其右侧部分(记为Rpart1)。特别地,如果Lpart1加上Lpart2总共有(m+n)/2个数据,且满足Lpart1部分最大值(记为Lmax1)小于Rpart2部分的最小值(记为Rmin2),同时Lpart2部分最大值(记为Lmax2)小于Rpart1部分的最小值(记为Rmin1)。如下图这种情况:
将Lpart1和Lpart2放到一起,Rpart1和Rpart2放到一起。此时,合并后的数组就被分成了长度相等(或左侧比右侧差一个值)的两部分(记为Lpart和Rpart),并且左侧部分的每一个值都小于右侧部分的每一个值。——到这里已经离中位数不远了!!!
若合并后的数组长度n+m为奇数,按照前面定义的规则(Lpart比Rpart少一个值),那么Rpart中的最小值即为要求的中位数;若为偶数,Lpart中的最大值和Rpart中的最小值求平均即为要求的中位数。
现在问题转变为:
- 选定nums1中的一个分割位置,在nums2中找到合适的分割位置使得分割后两个数组的左侧部分加起来总共有(m+n)/2个元素。
- 判断Lmax1<Rmin2和Lmax2<Rmin1是否都成立,若成立,转4;若不成立,转3。特别地,若nums1的分割位置位于#0,记Lmax1 = INT_MIN;若位于#n,记Rmin1 = INT_MAX。上述处理将不影响结论。对于nums2也采取相同的处理。
- 若Lmax1<Rmin2不成立,说明nums1中的分割位置靠后,向前移动以减小Lmax1;若Lmax2<Rmin1不成立,说明nums1中的分割位置太靠前(导致nums2的分割位置靠后),向后移动以增大Rmin1。移动分割位置后,转2。
- 若n+m为奇数,Rmin1和Rmin2中的最小值即为要求的中位数;若为偶数,max(Lmax1,Lmax)和min(Rmin1,Rmin2)的平均值即为要求的中位数。
二分法选择分割位置:
对于nums1中分割位置的选择,为了保证效率,采用二分法,即选择所有可选位置的中间位置进行分割。此外,如果nums1的长度小于nums2的长度,那么二分法要讨论的范围可以进一步缩小。因此,程序中通过调换nums1和nums2的位置保证二分法的作用范围始终是n(n<m)。这里采用了二分法选取分割位置,可选位置共有n+1个。忽略常量次数的操作,只考虑最外层的while循环,程序的时间复杂度为log(n+1),可以满足题目要求。
C++程序
// 两个有序数组的中位数
#include <iostream>
#include <string>
#include <vector>
using namespace std;
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2)
{
int n = nums1.size();
int m = nums2.size();
cout << "vector1 长度:"<< n<<endl;
cout << "vector2 长度:"<< m<<endl;
double res;
if(n>m)
// 调换位置,使得接下来的while循环中用时最短
res = findMedianSortedArrays(nums2,nums1);
int cut1; //第一个数组的分割
int cut2; //第二个数组的分割
//nums1可供选择的分割共有n+1种,记为0 ~ n
int h1 = n;
int l1 = 0;
int Lmax1,Lmax2,Rmin1,Rmin2; //两个Lmax和两个Rmin
bool flag = false; //划分是否满足要求
while(!flag)
{
cut1 = (l1+h1)/2;
cut2 = (m+n)/2-cut1;
cout << "vector1的分割位置:"<< cut1 <<endl;
cout << "vector2的分割位置:"<< cut2 <<endl;
Lmax1 = (cut1 == 0)?INT_MIN:nums1[cut1-1]; //左侧最小值比分割位置下标小1
Rmin1 = (cut1 == n)?INT_MAX:nums1[cut1];
Lmax2 = (cut2 == 0)?INT_MIN:nums2[cut2-1]; //左侧最小值比分割位置下标小1
Rmin2 = (cut2 == m)?INT_MAX:nums2[cut2];
// cout << "Lmax1: "<< Lmax1 <<endl;
// cout << "Rmin1: "<< Rmin1 <<endl;
// cout << "Lmax2: "<< Lmax2 <<endl;
// cout << "Rmin2: "<< Rmin2 <<endl;
if((Lmax1<=Rmin2)&&(Lmax2<=Rmin1))
flag = true; //满足要求
if(Lmax1>Rmin2) //nums1的左侧过大,调整可选分割的上限,使cut1左移
h1 = cut1-1;
if(Lmax2>Rmin1) //nums2的左侧过大,调整可选分割的下限,使cut1右移
l1 = cut1+1;
}
int Lmax = (Lmax1>Lmax2)?Lmax1:Lmax2;
int Rmin = (Rmin1<Rmin2)?Rmin1:Rmin2;
//若加起来一共有奇数个数据,根据预定规则,右侧部分会比左侧部分多一个数字,则右侧最小值为中位数
if((m+n)%2)
res = Rmin;
//若加起来一共有偶数个数据,左侧最大值和右侧最小值的均值记为中位数
if((m+n)%2 == 0)
res = (Lmax+Rmin)/2.0;
return res;
}
int main()
{
vector<int> nums1; //nums1的数据
nums1.push_back(1);
nums1.push_back(2);
// nums1.push_back(7);
vector<int> nums2; //nums2的数据
nums2.push_back(3);
nums2.push_back(4);
//打印两个有序数组的元素
vector<int>::iterator iter1 = nums1.begin();
vector<int>::iterator iter2 = nums2.begin();
cout << "nums1中的数值为:"<<endl;
for(;iter1!=nums1.end();iter1++)
{
cout << *iter1 <<endl;
}
cout << "nums2中的数值为:"<<endl;
for(;iter2!=nums2.end();iter2++)
{
cout << *iter2 <<endl;
}
//求中位数
double middle = findMedianSortedArrays(nums1,nums2);
cout <<"合并后的中位数:" << endl;
cout << middle <<endl;
return 0;
}