题目
给定两个大小分别为 m
和 n
的正序(从小到大)数组 nums1
和 nums2
。请你找出并返回这两个正序数组的 中位数 。
示例 1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
示例 2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
示例 3:
输入:nums1 = [0,0], nums2 = [0,0]
输出:0.00000
示例 4:
输入:nums1 = [], nums2 = [1]
输出:1.00000
示例 5:
输入:nums1 = [2], nums2 = []
输出:2.00000
提示:
nums1.length == m
nums2.length == n
0 <= m <= 1000
0 <= n <= 1000
1 <= m + n <= 2000
-10 <= nums1[i], nums2[i] <= 10
进阶:你能设计一个时间复杂度为O(log (m+n))
的算法解决此问题吗?
解题思路
1、使用归并
使用归并的方式,合并两个有序数组,得到一个大的有序数组。大的有序数组的中间位置的元素,即为中位数。
时间复杂度是 O(m+n),空间复杂度是 O(m+n)
2、双指针法
不需要合并两个有序数组,只要找到中位数的位置即可。由于两个数组的长度已知,因此中位数对应的两个数组的下标之和也是已知的。维护两个指针,初始时分别指向两个数组的下标 0 的位置,每次将指向较小值的指针后移一位(如果一个指针已经到达数组末尾,则只需要移动另一个数组的指针),直到到达中位数的位置。
虽然可以将空间复杂度降到 O(1),但是时间复杂度仍是 O(m+n)
3、二分查找
如何把时间复杂度降低到 O(log(m+n)) 呢?如果对时间复杂度的要求有 log,通常都需要用到二分查找,这道题也可以通过二分查找实现。
根据中位数的定义,当 m+n 是奇数时,中位数是两个有序数组中的第 (m+n)/2 个元素,当 m+n 是偶数时,中位数是两个有序数组中的第 (m+n)/2 个元素和第 (m+n)/2+1 个元素的平均值。因此,这道题可以转化成寻找两个有序数组中的第 k 小的数,其中 k 为 (m+n)/2 或 (m+n)/2+1。
由于两个数组长度之和 m+n 的奇偶不确定,因此需要分情况来讨论。
对于奇数的情况,直接找到最中间的数即可,偶数的话需要求最中间两个数的平均值。
假设两个有序数组分别是 A 和 B。要找到第 k 个元素,我们可以比较 A[k/2-1] 和 B[k/2-1],其中 / 表示整数除法。由于 A[k/2-1] 和 B[k/2-1] 的前面分别有 A[0..k/2-2] 和 B[0..k/2-2],即 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 小的数,查找范围缩小了一半。同时,我们将在排除后的新数组上继续进行二分查找,并且根据我们排除数的个数,减少 kk 的值,这是因为我们排除的数都不大于第 kk 小的数。
有以下三种情况需要特殊处理:
- 如果 A[k/2−1] 或者 B[k/2−1] 越界,那么我们可以选取对应数组中的最后一个元素。在这种情况下,我们必须根据排除数的个数减少 kk 的值,而不能直接将 kk 减去 k/2。
- 如果一个数组为空,说明该数组中的所有元素都被排除,我们可以直接返回另一个数组中第 k 小的元素。
- 如果 k=1,我们只要返回两个数组首元素的最小值即可。
答案
1、使用归并
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
// 结果
double result = 0;
// 两个 int 数组的长度
int length1 = nums1.length;
int length2 = nums2.length;
// 两个 int 数组的长度之和为奇数或偶数 奇数为1 偶数为0
int isOdd = (length1 + length2) % 2;
// 中位数下标 如果长度之和为奇数,则中位数下标就为 flag ,如果为偶数,中位数为 flag 和 flag - 1 之和 / 2
int flag = (length1 + length2) / 2;
// 缓存数组
int[] temp = new int[nums1.length + nums2.length];
// 遍历下标
int i = 0, j = 0, k = 0;
while (i < length1 || j < length2) {
// System.out.println("i: " + i);
// System.out.println("j: " + j);
// System.out.println("k: " + k);
// System.out.println();
if (i < length1 && j < length2) {
// nums1 和 nums2 都未处理完
// 判断 A 和 B 哪个小
if (nums1[i] <= nums2[j]) {
// A 小等于 B
temp[k] = nums1[i];
i++;
} else {
// B 比 A 小
temp[k] = nums2[j];
j++;
}
// 判断是否为中位数
if (isOdd == 1) {
// 奇数个
if (k == flag) {
// 是中位数
result = temp[k];
break;
}
} else {
// 偶数个
if (k == flag) {
// 是中位数
result = (temp[k] + temp[k - 1]) / 2.0;
break;
}
}
k++;
continue;
}
if (i < length1) {
// nums1 尚未处理完 , nums2 已处理完
// 插入temp
temp[k] = nums1[i];
i++;
// 判断是否为中位数
if (isOdd == 1) {
// 奇数个
if (k == flag) {
// 是中位数
result = temp[k];
break;
}
} else {
// 偶数个
if (k == flag) {
// 是中位数
result = (temp[k] + temp[k - 1]) / 2.0;
break;
}
}
k++;
continue;
}
if (j < length2) {
// nums2 尚未处理完 , nums1 已处理完
// 插入temp
temp[k] = nums2[j];
j++;
// 判断是否为中位数
if (isOdd == 1) {
// 奇数个
if (k == flag) {
// 是中位数
result = temp[k];
break;
}
} else {
// 偶数个
if (k == flag) {
// 是中位数
result = (temp[k] + temp[k - 1]) / 2.0;
break;
}
}
k++;
continue;
}
}
// System.out.println("-----------------------------");
// System.out.println("length1: " + length1);
// System.out.println("length1: " + length2);
// System.out.println("isOdd: " + isOdd);
// System.out.println("flag: " + flag);
// System.out.println(Arrays.toString(temp));
return result;
}
}
2、双指针法
…
3、二分查找
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
// 两个数组的元素个数
int length1 = nums1.length;
int length2 = nums2.length;
// 第 k 小值
int k = (length1 + length2) / 2;
if ((length1 + length2) % 2 == 1) {
// 奇数个
return find(nums1, nums2, k);
} else {
// 偶数个
return (find(nums1, nums2, k) + find(nums1, nums2, k + 1)) / 2.0;
}
}
// 求两个排序好的数组中,第 k 小的数
public double find(int[] nums1, int[] nums2, int k) {
// nums1 与 nums2 的游标
int i = 0, j = 0;
// 两个数组的元素个数
int length1 = nums1.length;
int length2 = nums2.length;
while (true) {
// 边界
if (i >= length1) {
return nums2[j + k - 1];
}
if (j >= length2) {
return nums1[i + k - 1];
}
if (k == 1) {
return Math.min(nums1[i], nums2[j]);
}
// 正常情况
int halfK = k / 2;
int newIndexI = Math.min(i + halfK, length1) - 1;
int newIndexJ = Math.min(j + halfK, length2) - 1;
if (nums1[newIndexI] <= nums2[newIndexJ]) {
k = k - (newIndexI - i + 1);
i = newIndexI + 1;
} else {
k = k - (newIndexJ - j + 1);
j = newIndexJ + 1;
}
}
}
}