一、题目描述
给定两个大小分别为 m
和 n
的正序(从小到大)数组 nums1
和 nums2
。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n))
。
示例 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
提示:
nums1.length == m
nums2.length == n
0 <= m <= 1000
0 <= n <= 1000
1 <= m + n <= 2000
-10^6 <= nums1[i], nums2[i] <= 10^6
二、方法一
(一)解题思路
- 首先,确保
nums1
是较短的数组,这样可以保证在二分查找时,较短数组的索引不会超出范围。 - 初始化两个指针
left
和right
,以及halfLen
,halfLen
是合并后数组的中位数位置。 - 使用二分查找法,通过比较
nums1
的i
索引和nums2
的j
索引的元素,来缩小搜索范围。 - 当
nums1[i]
小于nums2[j-1]
时,说明nums1[i]
不可能是中位数,因此将left
指针向右移动。 - 当
nums1[i]
大于等于nums2[j-1]
时,说明nums1[i]
可能是中位数,将right
指针向左移动。 - 当
left
和right
相遇时,找到了合并后数组的中位数位置。 - 根据中位数位置,计算
nums1
和nums2
中可能的中位数。 - 如果合并后的数组长度是奇数,返回最大的那个中位数;如果是偶数,返回两个中位数的平均值。
(二)具体代码
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
// 确保 nums1 是较短的数组
if (nums1.length > nums2.length) {
int[] temp = nums1;
nums1 = nums2;
nums2 = temp;
}
int m = nums1.length;
int n = nums2.length;
int total = m + n;
boolean isEven = total % 2 == 0;
// 初始化二分查找的左右指针和中位数位置
int left = 0, right = m, halfLen = (total + 1) / 2;
// 二分查找,找到合并后数组的中位数位置
while (left < right) {
int i = left + (right - left) / 2;
int j = halfLen - i;
if (i < m && nums1[i] < nums2[j - 1]) {
left = i + 1;
} else {
right = i;
}
}
// 计算中位数
int i = left;
int j = halfLen - i;
int num1LeftMax = (i == 0) ? Integer.MIN_VALUE : nums1[i - 1];
int num1RightMin = (i == m) ? Integer.MAX_VALUE : nums1[i];
int num2LeftMax = (j == 0) ? Integer.MIN_VALUE : nums2[j - 1];
int num2RightMin = (j == n) ? Integer.MAX_VALUE : nums2[j];
if (isEven) {
// 如果总长度是偶数,返回两个中位数的平均值
return (Math.max(num1LeftMax, num2LeftMax) + Math.min(num1RightMin, num2RightMin)) / 2.0;
} else {
// 如果总长度是奇数,返回最大的中位数
return Math.max(num1LeftMax, num2LeftMax);
}
}
}
(三)时间复杂度和空间复杂度
1. 时间复杂度
- 时间复杂度是 O(log(min(m, n)))。
- 这是因为代码使用了二分查找算法来确定合并后数组的中位数位置。
- 在最坏的情况下,二分查找需要进行 log(min(m, n)) 次比较,其中 m 和 n 分别是两个数组的长度。
- 由于我们总是比较较短数组的元素,所以时间复杂度不会超过较短数组的长度。
2. 空间复杂度
- 空间复杂度是 O(1)。
- 代码中没有使用任何额外的数据结构来存储数据,所有的计算都是在常数空间内完成的。
- 变量
i
,j
,left
,right
,halfLen
,num1LeftMax
,num1RightMin
,num2LeftMax
, 和num2RightMin
都是在常数空间内进行操作。
(四)总结知识点
-
数组交换:通过临时变量
temp
实现了两个数组nums1
和nums2
的交换,确保较短的数组是nums1
。这有助于后续的二分查找算法,因为它减少了比较的次数。 -
二分查找:代码中的
while
循环实现了二分查找算法,通过不断缩小搜索范围来确定合并后数组的中位数位置。这是解决这个问题的关键步骤,因为它允许我们在 O(log(min(m, n))) 时间内找到中位数。 -
边界条件处理:在计算中位数时,代码正确处理了数组的边界条件,例如当
i
或j
为 0 或数组长度时,如何确定num1LeftMax
、num1RightMin
、num2LeftMax
和num2RightMin
的值。 -
数学逻辑:代码中使用了数学逻辑来确定中位数。当总长度为奇数时,中位数是较大的那个数;当总长度为偶数时,中位数是两个数的平均值。
-
条件判断:通过
isEven
变量来判断总长度是奇数还是偶数,然后根据这个条件返回正确的中位数。 -
数据类型处理:在计算平均值时,代码正确地将整数转换为浮点数,以确保结果的准确性。
-
代码优化:通过确保较短的数组用于二分查找,代码优化了算法的效率,减少了不必要的比较。
-
常数空间复杂度:整个算法没有使用额外的数据结构,所有操作都在常数空间内完成,保持了空间复杂度为 O(1)。
三、方法二
(一)解题思路
-
确保较短数组优先处理:首先,如果
nums1
的长度大于nums2
,交换两个数组,以确保较短的数组是nums1
。这有助于减少二分查找的迭代次数。 -
二分查找:使用二分查找法来确定合并后数组的中位数位置。初始化两个指针
imin
和imax
,分别表示搜索范围的最小值和最大值,即0
和m
(nums1
的长度)。 -
迭代搜索:在循环中,计算中间值
i
和对应的j
(j = (m + n + 1) / 2 - i
),然后比较nums1[i]
和nums2[j-1]
的大小。根据比较结果,调整imin
和imax
的值,缩小搜索范围。 -
找到中位数:当
imin
和imax
相遇时,找到了合并后数组的中位数位置。此时,根据i
和j
的值,可以确定nums1
和nums2
中的元素,这些元素可能是中位数。 -
计算中位数:如果合并后的数组长度是奇数,直接返回
maxLeft
(即nums1[i-1]
或nums2[j-1]
中的最大值)。如果长度是偶数,返回maxLeft
和minRight
(即nums1[i]
或nums2[j]
中的最小值)的平均值。 -
处理边界情况:在计算
maxLeft
和minRight
时,需要处理数组的边界情况,例如当i
或j
为0
或数组的末尾时,确保不会访问数组的越界位置。 -
返回结果:根据上述步骤,返回计算出的中位数。
(二)具体代码
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
// 获取两个数组的长度
int m = nums1.length;
int n = nums2.length;
// 如果 nums1 的长度大于 nums2,交换两个数组,确保 nums1 是较短的数组
if (m > n) {
return findMedianSortedArrays(nums2, nums1);
}
// 初始化二分查找的左右边界
int imin = 0, imax = m;
// 二分查找循环
while (imin <= imax) {
// 计算中间位置的索引
int i = (imin + imax) / 2;
// 计算合并后数组的中位数位置的索引
int j = (m + n + 1) / 2 - i;
// 如果 nums1 的中间值小于 nums2 的对应位置的值,说明 i 太小,需要增加 i
if (i < imax && nums2[j - 1] > nums1[i]) {
imin = i + 1;
// 如果 nums1 的中间值大于 nums2 的对应位置的值,说明 i 太大,需要减小 i
} else if (i > imin && nums1[i - 1] > nums2[j]) {
imax = i - 1;
// 如果找到了正确的划分,计算中位数
} else {
// 初始化左边的最大值
int maxLeft = 0;
// 如果 i 为 0,maxLeft 为 nums2 的对应位置的值
if (i == 0) {
maxLeft = nums2[j - 1];
// 如果 j 为 0,maxLeft 为 nums1 的对应位置的值
} else if (j == 0) {
maxLeft = nums1[i - 1];
// 否则,取两个数组对应位置值的最大值
} else {
maxLeft = Math.max(nums1[i - 1], nums2[j - 1]);
}
// 如果合并后的数组长度为奇数,返回 maxLeft 作为中位数
if ((m + n) % 2 == 1) {
return maxLeft;
}
// 初始化右边的最小值
int minRight = 0;
// 如果 i 为数组长度,minRight 为 nums2 的对应位置的值
if (i == m) {
minRight = nums2[j];
// 如果 j 为数组长度,minRight 为 nums1 的对应位置的值
} else if (j == n) {
minRight = nums1[i];
// 否则,取两个数组对应位置值的最小值
} else {
minRight = Math.min(nums1[i], nums2[j]);
}
// 如果合并后的数组长度为偶数,返回 maxLeft 和 minRight 的平均值作为中位数
return (maxLeft + minRight) / 2.0;
}
}
// 如果循环结束仍未找到中位数,抛出异常
throw new IllegalArgumentException("Input arrays are not sorted or of zero length");
}
}
(三)时间复杂度和空间复杂度
1. 时间复杂度
- 时间复杂度是 O(log(min(m, n)))。
- 这是因为代码使用了二分查找算法来确定合并后数组的中位数位置。
- 在最坏的情况下,二分查找需要进行 log(min(m, n)) 次比较,其中 m 和 n 分别是两个数组的长度。
- 由于我们总是比较较短数组的元素,所以时间复杂度不会超过较短数组的长度。
2. 空间复杂度
- 空间复杂度是 O(1)。
- 代码中没有使用任何额外的数据结构来存储数据,所有的计算都是在常数空间内完成的。
- 变量 i, j, imin, imax, maxLeft, 和 minRight 都是在常数空间内进行操作。
(四)总结知识点
-
递归:通过递归调用来处理数组长度的交换,确保较短的数组作为
nums1
。 -
二分查找:使用二分查找法来确定合并后数组的中位数位置,这是一种高效的搜索算法,适用于有序数组。
-
边界条件处理:在计算
maxLeft
和minRight
时,需要处理数组的边界情况,例如数组的开头和结尾。 -
数学逻辑:理解中位数的定义,即在有序数组中,中位数是将数组分为两个部分的元素,且这两部分的元素数量相等(或相差1)。
-
条件判断:根据数组的总长度是奇数还是偶数,决定返回单个中位数还是两个中位数的平均值。
-
数据类型处理:在计算平均值时,确保结果为浮点数,以处理可能出现的小数情况。
-
异常处理:在循环结束后,如果未找到中位数,抛出异常,提示输入数组可能未排序或长度为零。
-
算法优化:通过二分查找优化算法的时间复杂度,使其达到 O(log(min(m, n))),这是解决该问题所需的最低时间复杂度。
-
代码结构:清晰的代码结构和逻辑,使得算法易于理解和维护。
以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。