寻找两个正序数组的中位数
编号:0018
试题来源:leetcode
试题描述
给定两个大小为
m
m
m和
n
n
n的正序(从小大大)数组nums1
和nums2
。请找出这两个正序数组的中位数,并要求算法的算法的时间复杂度为
O
(
l
o
g
(
m
+
n
)
)
O(log(m+n))
O(log(m+n))
假设
num1
和nums2
不会同时为空
解答算法
暴力解法
算法思路
没做出来这道题,用的是两个有序数组的归并排序,合并的时间复杂度为 O ( m + n − 1 ) O(m+n-1) O(m+n−1)。
暴力解法的思路就是将两个数组合并到一个数组中,因为我只关心中位数,所以当合并到
m
+
n
2
\frac{m+n}{2}
2m+n以及
m
+
n
2
+
1
\frac{m+n}{2} + 1
2m+n+1这两个数就结束,同时我也不关心合并后的数组到底是怎样,因此我只需要设置两个变量mid[2]
来存储中间的两个数就好了。
如果m+n
是奇数,那么中位数就为
m
+
n
2
+
1
\frac{m+n}{2} + 1
2m+n+1,如果是偶数,那么就是那两个数的平均数。
代码实现
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int sumSize = nums1.size() + nums2.size(), nums1Cur = 0, nums2Cur = 0, nums = 0; //sumSize存储m+n,nums1Cur、nums2Cur存储当前遍历两个数组的位置,nums用来存储已经放到合并后数组的数目
int mid[2] = {}; //mid用来存放中间的两个树
while (nums1Cur < nums1.size() || nums2Cur < nums2.size()) //只要这两个数组没有都遍历到最后一个
{
nums++; //每一次遍历存放的数都自增
if (nums1Cur == nums1.size() || (nums2Cur != nums2.size() && nums1[nums1Cur] > nums2[nums2Cur])) //nums1遍历完,或者nums2当前数值更小
{
if (nums == sumSize / 2) //如果当前是中间两个,那么存储进mid中
mid[0] = nums2[nums2Cur];
if (nums == sumSize / 2 + 1)
mid[1] = nums2[nums2Cur];
nums2Cur++;
}
else
{
if (nums == sumSize / 2)
mid[0] = nums1[nums1Cur];
if (nums == sumSize / 2 + 1)
mid[1] = nums1[nums1Cur];
nums1Cur++;
}
}
if (sumSize & 1) //奇数返回中间的
return mid[1];
else //偶数返回中间两个的平均数
return ((double)mid[1] + mid[0]) / 2;
}
};
复杂度分析
- 时间复杂度:整个过程中对两个数组都进行了遍历,因此总的时间复杂度为 O ( m + n ) O(m+n) O(m+n)。当然可以进行一定的优化,如在找到中间的两个数后,退出遍历。
- 空间复杂度:整个过程只申请了常数的空间,因此空间复杂度为 O ( 1 ) O(1) O(1)
二分查找算法
算法思路
我们要寻找中位数,也就是寻找合并之后的数组中的第 m + n 2 \frac{m+n}{2} 2m+n小和第 m + n 2 + 1 \frac{m+n}{2}+1 2m+n+1小的,如果是奇数,只用后者,如果是偶数,那么要用到两者的平均数。因此这个问题可以变成求解数组中第 k k k小的数字。
如果我们想要求第 k k k个,那么可以找到 A [ k 2 − 1 ] A[\frac{k}{2} -1] A[2k−1]和 B [ k 2 − 1 ] B[\frac{k}{2} -1] B[2k−1],这两者前面分别由 k 2 − 1 \frac{k}{2}-1 2k−1个数,因此总共有 2 ∗ ( k 2 − 1 ) < = k − 2 2*(\frac{k}{2} - 1) <= k -2 2∗(2k−1)<=k−2,因此如果 A [ k 2 − 1 ] < B [ k 2 − 1 ] A[\frac{k}{2} -1] < B[\frac{k}{2}-1] A[2k−1]<B[2k−1],那么前者最多是第 k − 1 k-1 k−1小的数,因此 A [ 0 … k 2 − 1 ] A[0\dotso \frac{k}{2}-1] A[0…2k−1]全部不可能是第 k k k小的数。
然后我们就得到了新的要比较的两个数组,我们已经排除了 k 2 \frac{k}{2} 2k个数,因此新的比较数目就是 k − k 2 k - \frac{k}{2} k−2k
然后就相当于对新的数组继续这样的步骤
代码实现
class Solution {
public:
int getKthElement(const vector<int>& nums1, const vector<int>& nums2, int k) { //得到第k小的数字
/* 主要思路:要找到第 k (k>1) 小的元素,那么就取 pivot1 = nums1[k/2-1] 和 pivot2 = nums2[k/2-1] 进行比较
* 这里的 "/" 表示整除
* nums1 中小于等于 pivot1 的元素有 nums1[0 .. k/2-2] 共计 k/2-1 个
* nums2 中小于等于 pivot2 的元素有 nums2[0 .. k/2-2] 共计 k/2-1 个
* 取 pivot = min(pivot1, pivot2),两个数组中小于等于 pivot 的元素共计不会超过 (k/2-1) + (k/2-1) <= k-2 个
* 这样 pivot 本身最大也只能是第 k-1 小的元素
* 如果 pivot = pivot1,那么 nums1[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums1 数组
* 如果 pivot = pivot2,那么 nums2[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums2 数组
* 由于我们 "删除" 了一些元素(这些元素都比第 k 小的元素要小),因此需要修改 k 的值,减去删除的数的个数
*/
int m = nums1.size(); //得到量数组长度
int n = nums2.size();
int index1 = 0, index2 = 0; //偏移坐标
while (true) {
// 边界情况
if (index1 == m) { //如果一个到达边界,那么直接取另一个中的数字就好
return nums2[index2 + k - 1];
}
if (index2 == n) {
return nums1[index1 + k - 1];
}
if (k == 1) { //如果最小。那么只用前两个比较一下就行
return min(nums1[index1], nums2[index2]);
}
// 正常情况
int newIndex1 = min(index1 + k / 2 - 1, m - 1); //新的坐标应该为原来的偏转坐标加上排除的数字,同时要注意不能越界
int newIndex2 = min(index2 + k / 2 - 1, n - 1);
int pivot1 = nums1[newIndex1]; //比较两个中间数的大小
int pivot2 = nums2[newIndex2];
if (pivot1 <= pivot2) { //更新k,也就是排除掉了一部分
k -= newIndex1 - index1 + 1;
index1 = newIndex1 + 1; //更新偏转
}
else { //同上,情况不同
k -= newIndex2 - index2 + 1;
index2 = newIndex2 + 1;
}
}
}
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int totalLength = nums1.size() + nums2.size();
if (totalLength % 2 == 1) {
return getKthElement(nums1, nums2, (totalLength + 1) / 2);
}
else {
return (getKthElement(nums1, nums2, totalLength / 2) + getKthElement(nums1, nums2, totalLength / 2 + 1)) / 2.0;
}
}
};