题目来源
题目描述
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
}
};
题目解析
中位数:
- 如果某个有序数组长度是奇数,那么其中位数就是最中间那个
- 如果是偶数,那么就是最中间两个数字的平均值
合并取中
- 先将两个数组合并,两个有序数组的合并也是归并排序中的一部分。然后根据奇数,还是偶数,返回中位数
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int M = nums1.size(), N = nums2.size();
std::vector<int> arr(M + N);
int count = 0;
int i = 0, j = 0;
while (count != M + N){
if(i == M){
while (j < N){
arr[count++] = nums2[j++];
}
break;
}
if(j == M){
while (i < M){
arr[count++] = nums1[i--];
}
break;
}
if (nums1[i] < nums2[j]){
arr[count++] = nums1[i++];
}else{
arr[count++] = nums2[j++];
}
}
if (count %2 != 0){
return arr[count/2];
}else {
return (arr[count/2-1] + arr[count/2])/2.0;
}
}
};
- 时间复杂度:遍历全部数组 (m+n)
- 空间复杂度:开辟了一个数组,保存合并后的两个数组 O(m+n)
双指针
-
不需要合并两个有序数组,只要找到中位数的位置即可。
- 由于两个数组的长度已知,因此中位数对应的两个数组的下标之和也是已知的。
- 维护两个指针,初始时分别指向两个数组的下标 0 的位置,每次将指向较小值的指针后移一位(如果一个指针已经到达数组末尾,则只需要移动另一个数组的指针),直到到达中位数的位置。
-
tick:不需要区分m+n是奇数还是偶数:
- 分别找第 (m+n+1) / 2 个,和 (m+n+2) / 2 个,然后求其平均值即可,这对奇偶数均适用。若 m+n 为奇数的话,那么其实 (m+n+1) / 2 和 (m+n+2) / 2 的值相等,相当于两个相同的数字相加再除以2,还是其本身。
class Solution {
public:
double findMedianSortedArrays(vector<int>& A, vector<int>& B) {
int M = A.size(), N = B.size();
int len = M + N;
int left = -1, right = -1;
int aStart = 0, bStart = 0;
for (int i = 0; i <= len / 2; i++) {
left = right;
if (aStart < M && (bStart >= N || A[aStart] < B[bStart])) {
right = A[aStart++];
} else {
right = B[bStart++];
}
}
if ((len & 1) == 0)
return (left + right) / 2.0;
else
return right;
}
};
- 时间复杂度:遍历 len/2+1 次,len=m+n,所以时间复杂度依旧是 O(m+n)
- 空间复杂度:O(1)
二分
- 时间复杂度要求 O(log(m+n)。看到log,很明显,我们只有用到二分的方法才能达到。
- 我们不妨用另一种思路,题目是求中位数,其实就是求第k小的数的一种特殊情况,而求第小的数只有一种算法
- 第二种方法中,我们一次遍历就相当于去掉不可能是中位数的一个值,也就是一个个排除。由于数列是有序的,其实我们完全可以一半一半的排除。
- 假设我们要找的是第k小的数,我们每次循环排除掉 k / 2 k/2 k/2个数。举个例子
假设我们要找的是第7小的数。
- 我们比较两个数组的第k / 2个数字,如果k是奇数,向下取整。
- 也就是比较第3个数字,上边数组中的4和下边数组中的3,哪个小,就表明该数组的前k/2个数字都不是第k小的数字,所以可以排除
- 将1349和45678910两个数组作为新的数组进行比较
- 由于我们已经排除掉了3个数字,所以在两个新数组中,我们只需要找第7-3=4小的数字就可以了,也就是k=4
- 此时两个数组,比较低2个数字,因为3 < 5,所以我们可以将小的那个数组中的 1 ,3 排除掉了。
- 我们又排除掉 2 个数字,所以现在找第 4 - 2 = 2 小的数字就可以了。
- 此时比较两个数字中的第k/2=1个数,4==4,怎么办呢?由于两个数相等,所以我们无论去掉哪个数组中的都行,因为去掉1个总会保留1个的,所以没有影响。为了统一,我们就假设 4 > 4 吧,所以此时将下边的 4 去掉
- 由于又去掉 1 个数字,此时我们要找第 1 小的数字,所以只需判断两个数组中第一个数字哪个小就可以了,也就是 4。
- 所以第 7 小的数字是 4。
我们每次都是取k/2的数进行比较,有可能会遇到数组长度小于k/2的时候
- 此时k/2等于3,而上边的数组长度是2,我们此时将箭头指向它的末尾就可以了。
- 这样的话,由于2<3,所以就会导致上边的数组1,2都被排除。造成下边的情况
- 由于2个元素被排除,所以此时k=5
- 又因为上边的数组已经空了,所以我们只需要返回下边的数组的第5个数组就可以了
从上边可以看到,无论是找第奇数个还是第偶数个数字,对我们的算法并没有影响,而且在算法进行中,k的值都有可能从奇数变为偶数,最都会变为1或者由于一个数组空了,直接返回结果
所以我们采用递归的思路,为了防止数组长度小于k/2,所以每次比较min(k / 2, len(数组))对应的数字,把小的那个对应的数组的数字排除,将两个新数组进入递归,并且k要减去排除的数组的个数。递归出口就是当k==1或者其中一个数组长度是0了
class Solution {
int getKth(vector<int>& nums1, int start1, int end1, vector<int>& nums2, int start2, int end2, int k){
int len1 = end1 - start1 + 1;
int len2 = end2 - start2 + 1;
//让 len1 的长度小于 len2,这样就能保证如果有数组空了,一定是 len1
if(len1 > len2){
return getKth(nums2, start2, end2, nums1, start1, end1, k);
}
if(len1 == 0){
return nums2[start2 + k - 1];
}
if(k == 1){
return std::min(nums1[start1], nums2[start2]);
}
int i = start1 + std::min(len1, k / 2) - 1;
int j = start2 + std::min(len2, k/2) - 1;
if (nums1[i] > nums2[j]) {
return getKth(nums1, start1, end1, nums2, j + 1, end2, k - (j - start2 + 1));
}
else {
return getKth(nums1, i + 1, end1, nums2, start2, end2, k - (i - start1 + 1));
}
}
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int n = nums1.size(), m = nums2.size();
int left = (n + m + 1) / 2;
int right = (n + m + 2) / 2;
//将偶数和奇数的情况合并,如果是奇数,会求两次同样的 k 。
return (getKth(nums1, 0, n - 1, nums2, 0, m - 1, left) + getKth(nums1, 0, n - 1, nums2, 0, m - 1, right)) * 0.5;
}
};
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int m = nums1.size(), n = nums2.size(), left = (m + n + 1) / 2, right = (m + n + 2) / 2;
return (findKth(nums1, 0, nums2, 0, left) + findKth(nums1, 0, nums2, 0, right)) / 2.0;
}
int findKth(vector<int>& nums1, int i, vector<int>& nums2, int j, int k) {
if (i >= nums1.size()) return nums2[j + k - 1];
if (j >= nums2.size()) return nums1[i + k - 1];
if (k == 1) return min(nums1[i], nums2[j]);
int midVal1 = (i + k / 2 - 1 < nums1.size()) ? nums1[i + k / 2 - 1] : INT_MAX;
int midVal2 = (j + k / 2 - 1 < nums2.size()) ? nums2[j + k / 2 - 1] : INT_MAX;
if (midVal1 < midVal2) {
return findKth(nums1, i + k / 2, nums2, j, k - k / 2);
} else {
return findKth(nums1, i, nums2, j + k / 2, k - k / 2);
}
}
};
切割(不会)
为了解决这个问题,我们需要理解 “中位数的作用是什么”。在统计中,中位数被用来:将一个集合划分为两个长度相等的子集,其中一个子集中的元素总是大于另一个子集中的元素。
所以我们只需要将数组进行切割。
一个长度为m的数组,有0~m总共m+1个位置可以切
我们把数组A和数组B分别在i和j进行切割
将 i 的左边和 j 的左边组合成「左半部分」,将 i 的右边和 j 的右边组合成「右半部分」。
- 当A数组和B数组的总长度是偶数时,如果我们能够保证:
- 左半部分长度等于右半部分:i + j = m - i + n - j , 也就是 j = ( m + n ) / 2 - i
- 左半部分最大的值小于等于右半部分最小的值:max ( A [ i - 1 ] , B [ j - 1 ])) <= min ( A [ i ] , B [ j ]))
- 那么,中位数就可以表示如下:
- (左半部分最大值 + 右半部分最小值 )/ 2。
- (max ( A [ i - 1 ] , B [ j - 1 ])+ min ( A [ i ] , B [ j ])) / 2
- 当 A 数组和 B 数组的总长度是奇数时,如果我们能够保证
- 左半部分的长度比右半部分大 1: i + j = m - i + n - j + 1也就是 j = ( m + n + 1) / 2 - i
- 左半部分最大的值小于等于右半部分最小的值 max ( A [ i - 1 ] , B [ j - 1 ])) <= min ( A [ i ] , B [ j ]))
- 那么,中位数就是:
- 左半部分最大值,也就是左半部比右半部分多出的那一个数。
- m a x ( A [ i − 1 ] , B [ j − 1 ] ) max ( A [ i - 1 ] , B [ j - 1 ]) max(A[i−1],B[j−1])
- 左半部分的长度比右半部分大 1: i + j = m - i + n - j + 1也就是 j = ( m + n + 1) / 2 - i
- 上边的第一个条件我们其实可以合并为 j = ( m + n + 1 ) / 2 − i j = ( m + n + 1) / 2 - i j=(m+n+1)/2−i,因为如果 m + n m + n m+n 是偶数,由于我们取的是 int值,所以加 1 1 1 也不会影响结果。