每日LeetCode_230903
4.寻找两个正序数组的中位数
(1)题目
给定两个大小分别为 m
和 n
的正序(从小到大)数组 nums1
和 nums2
。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n))
。
示例 1:
1 | 输入:nums1 = [1,3], nums2 = [2] |
示例 2:
1 | 输入:nums1 = [1,2], nums2 = [3,4] |
提示:
nums1.length == m
nums2.length == n
0 <= m <= 1000
0 <= n <= 1000
1 <= m + n <= 2000
-106 <= nums1[i], nums2[i] <= 106
(2)题解
给定两个有序数组,要求找到两个有序数组的中位数,最直观的思路有以下两种:
- 使用归并的方式,合并两个有序数组,得到一个大的有序数组。大的有序数组的中间位置的元素,即为中位数。第一种思路的时间复杂度是 O(m+n) ,空间复杂度是 O(m+n) ;
- 不需要合并两个有序数组,只要找到中位数的位置即可。由于两个数组的长度已知,因此中位数对应的两个数组的下标之和也是已知的。维护两个指针,初始时分别指向两个数组的下标 0 的位置,每次将指向较小值的指针后移一位(如果一个指针已经到达数组末尾,则只需要移动另一个数组的指针),直到到达中位数的位置。空间复杂度降到 O(1) ,时间复杂度仍是 O(m+n) ;
不合并数组,依次取数,直至中位数位置
中位数的位置一定在 (m+n)/2 附近:
- 如果是偶数,中位数就是最后取的两个数的平均数;
- 如果是奇数,中位数就是最后取的那个数。
取数阶段,循环 (m + n) / 2.0 + 1 次即可;判断中位数时,关键是奇数情况,可能有指针溢出,需要判断一下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 从 nums1 和 nums2 中每次取一个数,直到取出的数的个数 <= (m+n)/2 + 1
int m = nums1.size(), n = nums2.size();
int x = 0, y = 0; // x, y 分别是 nums1 和 nums2 的下标
for (int i = 1; i <= (m + n) / 2.0 + 1; ++i){
if (x < m && y < n){
if (nums1[x] < nums2[y]) ++x;
else ++y;
}else if (x < m) ++x;
else ++y;
}
if ((m + n) % 2 == 0){
return (nums1[x - 1] + nums2[y - 1]) / 2.0;
}else{
if (x >= m){
return nums1[x - 1];
}else if (y >= n){
return nums2[y - 1];
}else{
return min(nums1[x - 1], nums2[y - 1]);
}
}该方法测试用例可行,但 LeetCode 无法通过,可能是指针溢出形成空指针不被允许。
改进
我们首先计算出两个数组的长度,然后将它们的长度相加,得到总长度。接下来,我们使用一个循环来遍历这两个数组,直到到达中位数的位置。在循环中,我们维护两个指针
aStart
和bStart
,它们分别指向两个数组的下标 0 的位置。我们还维护两个变量left
和right
,它们分别表示当前的中位数和前一个元素。在每次循环中,我们将left
的值更新为right
,然后将指向较小值的指针后移一位。最后,如果总长度是偶数,则中位数是left
和right
的平均值,否则中位数是right
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19int m = nums1.size(), n = nums2.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 || nums1[aStart] < nums2[bStart])){
right = nums1[aStart++];
}else{
right = nums2[bStart++];
}
}
if (len % 2 == 0){
return (left + right) / 2.0;
}else{
return right;
}二分查找
如果对时间复杂度的要求有 log ,通常都需要用到二分查找:
根据中位数的定义,当 m+n 是奇数时,中位数是两个有序数组中的第 (m+n)/2 个元素,当 m+n 是偶数时,中位数是两个有序数组中的第 (m+n)/2 个元素和第 (m+n)/2+1 个元素的平均值。因此,这道题可以转化成寻找两个有序数组中的第 k 小的数,其中 k 为 (m+n)/2 或 (m+n)/2+1 。
假设两个有序数组分别是 A 和 B。要找到第 k 个元素,我们可以比较 A[k/2−1] 和 B[k/2−1] ,其中 / 表示整数除法。由于 A 和 B 的前面分别有 k/2−1 个元素,对于 A[k/2−1] 和 B[k/2−1] 中的较小值,最多只会有 (k/2−1)+(k/2−1)≤k−2 个元素比它小,那么它就不能是第 k 小的数了。
因此我们可以归纳出三种情况:
- 如果 A[k/2−1]<B[k/2−1] ,则比 A[k/2−1] 小的数最多只有 A 的前 k/2−1 个数和 B 的前 k/2−1 个数,即比 A[k/2−1] 小的数最多只有 k−2 个,因此 A[k/2−1] 不可能是第 k 个数,A[0] 到 A[k/2−1] 也都不可能是第 k 个数,可以全部排除。
- 如果 A[k/2−1]>B[k/2−1] ,则可以排除 B[0] 到 B[k/2−1] 。
- 如果 A[k/2−1]=B[k/2−1] ,则可以归入第一种情况处理。
可以看到,比较 A[k/2−1] 和 B[k/2−1] 之后,可以排除 k/2 个不可能是第 k 小的数,查找范围缩小了一半。同时,我们将在排除后的新数组上继续进行二分查找,并且根据我们排除数的个数,减少 k 的值,这是因为我们排除的数都不大于第 k 小的数。
有以下三种情况需要特殊处理:
- 如果 A[k/2−1] 或者 B[k/2−1] 越界,那么我们可以选取对应数组中的最后一个元素。在这种情况下,我们必须根据排除数的个数减少 k 的值,而不能直接将 k 减去 k/2。
- 如果一个数组为空,说明该数组中的所有元素都被排除,我们可以直接返回另一个数组中第 k 小的元素。
- 如果 k=1 ,我们只要返回两个数组首元素的最小值即可。
用一个例子说明上述算法。假设两个有序数组如下:
1
2A: 1 3 4 9
B: 1 2 3 4 5 6 7 8 9两个有序数组的长度分别是 4 和 9,长度之和是 13,中位数是两个有序数组中的第 7 个元素,因此需要找到第 k=7 个元素。
比较两个有序数组中下标为 k/2−1=2 的数,即 A[2]=4 和 B[2]=3 ,如下面所示:
由于 A[2]>B[2] ,因此排除 B[0] 到 B[2] ,即数组 B 的下标偏移(offset)变为 3,同时更新 k 的值:k=k−k/2=4。1
2A: 1 3 4 9
B: [1 2 3] 4 5 6 7 8 9下一步寻找,比较两个有序数组中下标为 k/2−1=1 的数,即 A[1]=3 和 B[4]=5 ,如下面所示,其中方括号部分表示已经被排除的数。
由于 A[1]<B[4],因此排除 A[0] 到 A[1],即数组 A 的下标偏移变为 2,同时更新 k 的值:k=k−k/2=2。1
2A: [1 3] 4 9
B: [1 2 3] 4 5 6 7 8 9下一步寻找,比较两个有序数组中下标为 k/2−1=0 的数,即比较 A[2]=4 和 B[3]=4 ,如下面所示,其中方括号部分表示已经被排除的数。
由于 A[2]=B[3],根据之前的规则,排除 A 中的元素,因此排除 A[2] ,即数组 A 的下标偏移变为 3 ,同时更新 k 的值: k=k−k/2=1 。1
2A: [1 3 4] 9
B: [1 2 3] 4 5 6 7 8 9由于 k 的值变成 1,因此比较两个有序数组中的未排除下标范围内的第一个数,其中较小的数即为第 k 个数,由于 A[3]=9>B[3]=4 ,因此第 k 个数是 B[3]=4。
1 | class Solution { |