力扣【二分查找】:4. 寻找两个正序数组的中位数
- 题目描述
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。
- 算法思路:
第一次尝试,
首先,两个有序数组要找到中位数需要先知道数组长度,当m + n的值为奇数时中位数的次序是(m + n + 1)/ 2(向上取整),为偶数时中位数有两个,次序分别为(m + n)/ 2和(m + n)/2 + 1,
于是可以区分两种情况并分别求出结果,
然后时间复杂度要求为O(log(m + n)),可以选择用二分查找,考虑对两个数组分别做二分查找,分别用mid = left + (right - left) / 2,找到各自的中位数并比较大小,
对于较小的那个中位数以及之前的数放弃,
但是存在一个问题,这样寻找中位数的方式找不到淘汰一段数据的依据,数组中数值的大小可以用来区分两个数组中某一个的一段数据更小(因为数组有序),但如果数字本身是该数组的中位数,
其实不能严格说较小的那个中位数之前的可以淘汰,比如[1, 2]和[3, 4],在寻找(m + n)/2时按照这种思路,会把1和2都淘汰而这显然是错的,
按照left,right,mid那套方法需要同时管理至少六个指针,算法实现过于复杂并且我找不到一种合适的调整left,right的方法,
但是第一次尝试中找到了一个思路方向:可以把二分随机访问查找和寻找顺序上的第x个元素结合,我们要找的中位数本质上是有序数组中第中位数个元素或者两个中位数的算数平均
第二次尝试,
这是读了力扣标准题解后知道的,我们要找的中位数的排序定为k,m + n是奇数时k = (m + n + 1) / 2,偶数时k1 = (m + n) / 2,k2 = k1 + 1,
思考问题中两个数组特点可以发现,中位数的两个数很有可能不在同一个数组中,甚至很可能不相邻,所以偶数时不能简单地找出k1后加一得到k2,
同时为了减少重复代码考虑做一个封装,用来在nums1和nums2中找到目标序数对应的数值,
接着思考k的问题,目前的难点在于如何达到O(log (m + n))的时间复杂度,那么可以想到对数时间复杂度一般就是靠除以2实现的,
现在我要找的是两个数组中第k小的元素,并且两个数组都是有序的,那么可以想到用 k / 2来解决问题,
按照第一次尝试中我们用两个数组各自中位数进行大小比较的方法,我们这次比较的是nums1[k/2 - 1] 和 nums2[k/2 - 1],
如果nums1[k/2 - 1] < nums2[k/2- 1]那么nums1[k/2 - 1]一定不是第k小的元素,这是因为即便假设nums2从0到k/2 - 2的元素都小于nums1[k/2 - 1],
最多也只有k/2 - 1 + k / 2 - 1 = k - 2个元素小于nums1[k/2 - 1],于是这里就可以抛弃nums1的前k/2个元素,
当排除了k/2个元素后,需要从k中减去对应的元素个数,这样下一次寻找的第k个元素就不断变得更靠前,
同样当nums1[k/2 - 1] > nums2[k/2 - 1]时,需要抛弃的元素就变成了nums2的前k/2个元素,
nums1[k/2 - 1] == nums2[k/2 - 1]时可以分到前面两种情况的任意一种,
到这里就完成了二分的过程:每次抛弃当前k对应的k/2个元素,然后对缩小后的k重新求k/2重复这一过程。
但是到这里我们并没有解决循环终止的条件,即到什么时候我们可以直接找到中位数?
我的思考是:由于每次我们抛弃k/2的元素,那么k的值一定会不断缩小到1,两个数组的大小一定会不断缩小,所以我们可以用最少最简单的情况来反推
第一种情况是某一个数组为空,在这种情况下自然只需要返回另一个数组的第k小元素即可,由于每次循环都有可能导致某一个数组为空,因此我们需要在每次循环开始时检查,
第二种情况是当两个数组都不为空时,如果k等于1,这说明要找的是两个数组中第1小的元素,很显然由于数组有序,返回两个数组中首元素较小的即可,
第三种情况比较复杂容易忽略,按照之前的算法,我们应该维护两个指针分别指向当前nums1和nums2数组的首位,然后用这两个指针加k/2来找到需要判断的两个k/2 - 1,
但是某个数组剩余的元素个数可能不够k/2个,那么这时候对应数组就应该选择最后一位作为"k/2 - 1"处的元素并与另一个进行比较,
所以第三种情况简单来说就是如果我们在依次循环开始,完成了情况一和情况二的检查后,计算下一轮用于比对大小的地址时可能会遇到越界的情况,
此时我们将越界那个数组剩余的所有元素都放入到k/2 - 1的比对过程中。
总结一下,新的方法是对k(代表第k大的元素)做“二分”,通过比对nums1[k/2 - 1]和nums2[k / 2 - 1]大小关系可以一次性排除k/2个一定不是第k大的元素,
并且k每次都会减少k/2,然后在得到的新k1的继承上继续做k1/2,最终必然会出现某个ki == 1,或者其中一个数组所有元素都被淘汰的两种情况,
这两种情况分别代表寻找两个数组中的第1小元素和其中一个数组的第k小元素,那么循环最终是可以通过这两个方向结束的,不会出现死循环的情况,
而计算k/2时,有可能某个数组(或两个数组)剩余的元素都不足k/2个,这时就需要把剩余不足k/2个元素都用来比较,即比较其中最大的那个,也就是最后一个。
到这里可以尝试编写代码了。
- 代码
奇数个元素个数只需要调用一次FindKLargest,target_k初始为(m + n + 1)/2,每次减少的元素个数小于等于 target_k/2,所以循环 log(target_k)向上取整次结束,
时间复杂度为 log (m + n),偶数个元素类似。
时间复杂度:O(log (m + n))
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int m = nums1.size(), n = nums2.size();
int k;
if ((m + n) % 2 == 0) {
k = (m + n) / 2;
return (FindKLargest(nums1, nums2, k) + FindKLargest(nums1, nums2, k + 1)) / 2;
} else {
k = (m + n + 1) / 2;
return FindKLargest(nums1, nums2, k);
}
}
double FindKLargest(vector<int> &nums1, vector<int> &nums2, int target_k) {
int m = nums1.size(), n = nums2.size();
int start_1 = 0, start_2 = 0;
while (true) {
if (start_1 >= m) {
return nums2[start_2 + target_k - 1];
}
if (start_2 >= n) {
return nums1[start_1 + target_k - 1];
}
if (target_k == 1) {
return std::min(nums1[start_1], nums2[start_2]);
}
int pos_1 = std::min(start_1 + target_k / 2, m) - 1;
int pos_2 = std::min(start_2 + target_k / 2, n) - 1;
if (nums1[pos_1] < nums2[pos_2]) {
target_k -= (pos_1 - start_1 + 1);
start_1 = pos_1 + 1;
} else {
target_k -= (pos_2 - start_2 + 1);
start_2 = pos_2 + 1;
}
}
}
浙公网安备 33010602011771号