题目描述:
给定两个已排序的整数数列(长度分别为m和n),求出他们的中位数。要求时间复杂度为O(log (m+n)).
如:
nums1 = [1, 3]
nums2 = [2]
The median is 2.0
nums1 = [1, 2]
nums2 = [3, 4]
The median is (2 + 3)/2 = 2.5
题目分析:
由于题目要求时间复杂度为O(log (m+n)), 看到log这个运算符,很容易想到要使用分治算法。
可以把题目进行转换:找到两个有序数列的第k个元素。
对于合并数列长度为奇数的,其中位数为合并后中间的数字;对于合并数列长度为偶数的,其中位数为合并后中间的两个数字的平均值。
典型的分治算法如二分法,每次选择中间的元素,与需要查找的元素进行比较,然后可以缩减一半的查找范围,时间复杂度为O(log(n)),每次减半的变量为搜索区间的长度。
对于本道题,首先可以想,需要将哪一个变量进行减半。可能能减半的变量有:两个序列的长度,查找合并数列的第k个元素的k.
这里选择从k入手,对k实行减半。
图一
为了简化计算,假设第一个数列的长度小于等于第二个数列的长度
我们从第一个数列尽可能选取k/2个数,sa = min(k/2, alen), alen为nums1上的查找长度,查找长度将为减少,记取出的数的最后一个数为A.
然后从第二个数列中取出k - sa个数,记取出的最后一个数为B
取出的数总长度为k
当A < B时,合并后的数列为:
图二
其中,点A前面黄色的部分为<=A的数,长度小于k; 而橙色部分为>=A且<=B的数。
在图一的第一个数列中,由于A<B, A后面还可能存在着<B的数,因此图二橙色部分的长度大于等于0
故黄色加橙色的长度大于等于k
所以第k大数在橙色部分,我们可以得知黄色部分肯定不存在第k大数,故可以在下次搜索时忽略黄色段。
但黄色段包括数列一和数列二的元素,要实现起来有点麻烦。
由有序性,黄色段包含了点A前面的元素,由之前的取法知这些元素个数尽可能接近k/2.
因此可以将k减半。
不失一般性A > B时也类似处理。
当A = B时,说明正好找到了第k大元素。
如何忽略前面的元素,可以记每次搜索的序列为nums1[i, nums1.size()), nums2[j, nums2.size())
示例代码:
class Solution {
public:
int kth(vector<int> &nums1, int i, vector<int> &nums2, int j, int k){
// [i, nums1.size())
// [j, nums2.size())
int alen = nums1.size() - i;
int blen = nums2.size() - j;
if (alen > blen)return kth(nums2,j,nums1,i,k);
if (alen == 0)return nums2[j + k - 1];
if (k == 1)return min(nums1[i], nums2[j]);
int sa = min(k / 2,alen);
int sb = k - sa;
int pa = i + sa - 1;
int pb = j + sb - 1;
if (nums1[pa] < nums2[pb])return kth(nums1, pa + 1, nums2, j, k - sa);
if (nums1[pa] > nums2[pb])return kth(nums1, i, nums2, pb + 1, k - sb);
return nums1[pa];
}
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int alen = nums1.size();
int blen = nums2.size();
int mid = (alen + blen) / 2;
if ((alen + blen) & 1){
//奇数
return kth(nums1, 0, nums2, 0, mid + 1);
}
//偶数
return (kth(nums1, 0, nums2, 0, mid) + kth(nums1, 0, nums2, 0, mid + 1))/ 2.0;
}
};