力扣-4-寻找两个正序数组的中位数
题目要求O(log (m+n))
的时间复杂度
知道了两个数组的长度,那么中位数的下标以及如何计算是可以确定的,给出的是两个正序数组,如果使用双指针,从两个数组头开始扫描并比较,找出合并后第 K 小的数字,时间复杂度是多少?
时间复杂度是O((M+N)/2)
,这个目标还不及题目的要求,看到logN
就会想到二分,但是以往的二分是在有序数组中查找指定元素,这里是查找合并后的中位元素,或者说查找合并后第 K 小的元素,似乎不太一样
官方二分
这种思路的精髓在于:每一次循环都能排除掉当前比较中较小的那部分元素,从而在不断缩小的范围内寻找到中位数,而不需要对整个数组进行排序
这个二分思路中 k 的更新规则是什么,以及循环退出条件是什么?
k 的更新规则是每次循环结束后减去排除掉的元素数量,以更新我们在剩下的数组中查找新的第k小的元素
为什么这么做是正确的呢?
因为我们只会从数组头排除,也就是排除小的那一部分;结束条件是k=1,即我们需要第一小的元素,就是当前指针指向的两个元素。
但是很明显就算当k=1时,也有两个指针指向了两个数字,我们选哪一个呢?为什么选较小的那个作为中位数是正确的?
因为我们需要第一小的元素,就是更小的那一个
这个二分思路跟常规二分不一样的地方在于:首先它只会排除左边的部分,但是是两个数组中的某一个左半部分,用两个指针来实现。
我想这个思路可能更像是:查找合并数组后第 k 小的元素,更像是转化为 -> 排除掉合并后数组中最小的 k-1 个元素,并且每次排除 k/2 个元素,我觉得这样解释这个思路更能让人理解
如果能够理解这个思路了,那么就可以尝试自己写
指针越界问题
为什么指针下标定位 k/2-1 而不是 k/2 ?因为第 k 小的元素对应的下标是 k-1。已知当两个数组都不为空时长度至少为 2,这时候 k/2 就会出问题,因为如果两个数组长度都为 1 最多下标只到 0,而 k 可以取 2
但是比如k=7,k/2=3正好就对应了中间元素的下标,同时比如一个数组长度为 1,另一个很长 13,k=7,这时候就算 k/2-1 也无法避免在第一个数组中的越界问题,这个如何解决呢?所以取下标之前要对数组长度做判断
double getKth(vector<int>& nums1, vector<int>& nums2, int k) {
int m = nums1.size(), n = nums2.size();
// 需要考虑两个数组中存在空数组的情况
// 因为m+n>=1,所以不考虑同时为空
if (m == 0)return nums2[k - 1];
if (n == 0)return nums1[k - 1];
int index1 = k / 2 - 1, index2 = k / 2 - 1, offset;
// 处理越界的情况,第一次初始化索引时有可能会越界
if (index1 > m - 1)return nums2[n - m - 1];
if (index2 < n - 1) return nums1[m - n - 1];
while (k > 1) {
offset = k / 2;
k = k - offset;// 这里不能直接/2,否则奇数情况会丢一个
if (nums1[index1] > nums2[index2]) {
// 更新两个索引
// 每一次更新索引,向右移动偏移的索引下标也有可能越界
index1 = k / 2 - 1;
index2 = index1 + offset;
if (index2 > n - 1)return nums1[m - offset - 1];
}
else {
index2 = k / 2 - 1;
index1 = index2 + offset;
if (index1 > m - 1) return nums2[n - offset - 1];
}
}
return min(nums1[index1], nums2[index2]);
}
尝试……但是上面肯定是错的,因为 k、offset、m、n 的关系我理不清,更新 index 并处理越界那里肯定是错的
官方题解
首先 index 都初始化为 0,并把 index 的初始化和更新统一,并放在了循环内部
并且当index==数组长度
作为循环退出条件之一
这样巧妙在:
- 统一处理了其中一个数组为空的情况
- 统一处理了 index 的初始化和更新
- 统一处理了当更新偏移量的索引后索引越界的问题
if (index1 == m) return nums2[index2 + k - 1];
这一句可以保证当初始化后数组为空的正确性,但是怎么保证下标越界情况的正确性?
这个索引更新策略的正确性又如何证明?int newIndex1 = min(index1 + k / 2 - 1, m - 1);
每次都加上 k/2-1,第一次肯定是正确的,后面更新又怎么证明?
index 和 newIndex 分别分别代表了什么?它们之间什么关系?代表了什么意义?
newIndex 很明显是每一次更新后的索引,比较时用的也是这个索引,那么为什么要保存旧的index,甚至还要去更新它?
每一次 newIndex 的更新依赖的不是上一次 newindex 而是+newIndex+1
后的 index,其中newIndex+1
代表的就是偏移量
如何证明min(index1 + k / 2 - 1, m - 1)
这个更新策略的正确性?
当跟新后的下标越界就取末尾元素,我觉得这样可能也是为什么用 newIndex+1 替换 k/2 作为偏移量的原因
当上一次下标越界只能取末尾元素时,偏移量是小于 k/2 的,具体是多少呢?就是整个数组的长度,不用再考虑之前的偏移累加
所以说== index 代表的其实是偏移累加,而之所以要用 newIndex+1 替换 k/2 作为偏移量的原因是因为越界情况会导致可偏移量是小于 k/2 的
我本来是这么写的,但是后面遇到[1,2] [3,4]
这个测试用例的时候我发现,k1 的条件必须写到循环里面==,否则会出现越界
double getKth(vector<int>& nums1, vector<int>& nums2, int k) {
int m = nums1.size(), n = nums2.size();
int index1 = 0, index2 = 0;
while (k > 1) {
if (index1 == m) return nums2[index2 + k - 1];
if (index2 == n)return nums1[index1 + k - 1];
int newIndex1 = min(index1 + k / 2 - 1, m - 1);
int newIndex2 = min(index2 + k / 2 - 1, n - 1);
if (nums1[newIndex1] > nums2[newIndex2]) {
k -= newIndex2 - index2 + 1;
index2 = newIndex2 + 1;
}
else {
// 当索引不越界的时候,下面这句和k -= k / 2;是等价的
k -= newIndex1 - index1 + 1;
index1 = newIndex1 + 1;
}
}
return min(nums1[index1], nums2[index2]);
}
能不能把两句越界判断提出来?
不行,因为这两句同时承担了第一次判空,所以结论是最后一句不能写到外面来
官解这代码我一点都改不动
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int len = nums1.size() + nums2.size();
if (len & 1)return getKth(nums1, nums2, (len + 1) / 2);
else return (getKth(nums1, nums2, len / 2) + getKth(nums1, nums2, len / 2 + 1)) / 2.0;
}
double getKth(vector<int>& nums1, vector<int>& nums2, int k) {
int m = nums1.size(), 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);
if (nums1[newIndex1] > nums2[newIndex2]) {
k -= newIndex2 - index2 + 1;
index2 = newIndex2 + 1;
}
else {
// 当索引不越界的时候,下面这句和k -= k / 2;是等价的
k -= newIndex1 - index1 + 1;
index1 = newIndex1 + 1;
}
}
}