给定两个大小为 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
示例 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
-106 <= nums1[i], nums2[i] <= 106
思路:(二分法,Leetcode官方题解)
1.一个有序数组的中位数
使用一条分界线将数组分为两个部分。
1)如果数组的元素是偶数,则中位数是介于这个分界线两边的两个与元素的平均数
2)如果数组的元素是奇数,让分隔线左边部分多一个元素,则此时分隔线的左边元素即为中位数。
2.两个有序数组的中位数
使用一条分界线将两个数组分为两个部分。
1)如果两个数组元素个数之和为偶数,让分隔线左边与右边的元素个数相等。
2)如果两个数组元素个数之和为奇数,让分隔线左边的元素比右边的元素多一个。
3)分隔线左边的所有元素的数值<=分隔线右边的所有元素的数值。
如果找见这条分隔线,那么中位数只与分隔线两侧的元素有关。
1)当两个元素的个数之和为奇数的时候,有size_left = size_right+1
此时两个数组的中位数为分隔线左边元素的最大值
2)当两个元素的个数之和为奇数的时候,有size_left = size_right
此时两个数组的中位数为分隔线左边最大元素与右边最小元素的平均值
3.定位分隔线
1)分隔线两边元素的个数
假设数组1,长度为len1,数组2,长度为len2。
当m+n为偶数,size_left=(len1+len2)/2=(len1+len2+1)/2//整数出发默认下取整
当m+n为奇数,size_left=(len1+len2+1)/2 //定义令左边元素个数多一个,向上取整
于是就可以不分奇偶数讨论,只需要确定其中一个数组的分隔线位置,就可以计算出另一个数组的分隔线位置。
定义i为分隔线在第一个数组右边的第一个元素下标,则i为分隔线在第一个数组左边的元素个数。
定义j为分隔线在第二个数组右边的第一个元素下标,则j为分隔线在第二个数组左边的元素个数。
于是i j满足i+j=(len1+len2+1)/2
2)保证分隔线左边所有元素的数值<=分隔线右边所有元素的数值
即满足交叉小于等于关系成立
第一个数组分隔线左边的元素<=第二个数组分隔线右边的元素
第二个数组分隔线左边的元素<=第一个数组分割线右边的元素
如果不符合交叉小于等于关系,就需要适当的调整分隔线的位置。
于是i.j满足nums1[i-1]<=nums2[j] && nums2[j-1]<=nums1[i]
3)分隔线的调整
由前两步可以知道,i,j需要满足i+j=(len1+len2+1)/2
与nums1[i-1]<=nums2[j] && nums2[j-1]<=nums1[i]
于是基于这两个条件二分来定位分隔线。
由于i,j需要满足nums1[i-1]<=nums2[j] && nums2[j-1]<=nums1[i]
于是二分的循环可以有两种写法:
1.以nums1[i-1]<=nums2[j]
为条件
第一个数组分隔线左边的数大于第二个数组分隔线右边的数,此时说明第一个数组分隔线左边的数太大,需要向左移动。
int left = 0, right = len1;
while (left < right) {
// left == right时 循环退出
//分隔线在第一个数组的位置 同理(left + right) / 2 有可能越界
//+1是为了不出现死循环,即left永远小于right的情况,同时保证不会出现下标越界,即i-1<0的情况。
int i = left + (right - left + 1) / 2; //第一个数组左边元素的个数
int j = totalLeft - i; //分隔线在第二个数组左边元素的个数
if (nums1[i - 1] > nums2[j]) {
//第一个数组分隔线左边元素大于第二个数组分隔线右边元素
//分隔线需要左移 下一轮在[left,i-1]里面寻找
right = i - 1;
} else {
// 此时说明条件满足,应当将左指针右移到i的位置
// 下一轮在[i,right]里面寻找
// [left(i),right] 如果i = left + (right - left) / 2
// 如果只有两个元素的时候,一旦左边界为i,区间就不会缩小了,即死循环,因此要在判断的时候向上取整,即int
// i = left + (right - left + 1) / 2 这里要加一
// 同时可以保证i不会取到0,即i-1不会越界
left = i;
}
}
2.以 nums2[j-1]<=nums1[i]
为条件
·第二个数组的分隔线左边元素>第一个数组分隔线右边元素,此时说明第一个数组分隔线右边的数太小,需要向右移动。
int left = 0, right = len1;
while (left < right) {
int i = left + (right - left) / 2;
int j = totalLeft - i;
if (nums2[j - 1] > nums1[i]) {
//第二个数组的分隔线左边元素大于第一个数组分隔线右边的元素
//说明分隔线需要向右移 下一轮在[i+1,right]里面查找
//因为这样不会导致系循环,于是这里不需要上取整,i的定义里不需要加一,同时i不会取到m导致越界
left = i + 1;
} else {
//否则下一轮搜索区间为[left,i]
right = i;
}
}
4)极端情况
1)两个数组不一样长时,有可能出现在较短的数组上,分隔线左边或右边没有元素。
由于我们要访问“中间数分隔线”左右两边的元素,因此应该在较短的数组上确定“中间数分隔线”位置。以避免出现访问数组下表越界的情况
if (len1 > len2) return findMedianSortedArrays(nums2, nums1);//令nums1为元素较少的元素,以nums1为基准找分隔线
2)两个数组长度相等
第一个数组所有元素小于第二个数组。则第一个数组在分隔线左边没有元素,第二个数组在分隔线右边没有元素。
第一个数组所有元素大于第二个数组。则第一个数组在分隔线右边没有元素,第二个数组在分隔线左边没有元素。
5.结果
由之前分析可以知道,最后的结果只与分隔线两边的元素有关,并且有可能存在分隔线两端没有元素的情况。
于是首先需要特殊处理一下分隔线两端的元素。
int i = left, j = totalLeft - i; //分隔线的位置;
// i=0说明第一个数组分隔线左边没有值,为了不影响其与nums2LeftMax的比较,将其设置为Int的最小值
int nums1LeftMax = i == 0 ? INT_MIN : nums1[i - 1];
// j=len1说明第一个数组分隔线左边没有值,为了不影响其与nums2RightMin的比较,将其设置为Int的最大值
int nums1RightMin = i == len1 ? INT_MAX : nums1[i];
// j=0说明第一个数组分隔线左边没有值,为了不影响其与nums1LeftMax的比较,将其设置为Int的最小值
int nums2LeftMax = j == 0 ? INT_MIN : nums2[j - 1];
// j=len2说明第一个数组分隔线左边没有值,为了不影响其与nums1RightMin的比较,将其设置为Int的最大值
int nums2RightMin = j == len2 ? INT_MAX : nums2[j];
之后分两种情况
1)两个数组元素之和为奇数,此时中位数为两数组分隔线左边最大元素的较大值
2)两个数组元素之和为偶数,此时中位数为分隔线左边最大元素与分隔线右边最小元素的平均值。(由于返回值为double,需要类型转换一下,否则整型除法会向下取整)。
if ((len1 + len2) % 2 == 1) {
//如果两数组长度之和为奇数
return max(nums1LeftMax, nums2LeftMax);
}
else {
//两数组长度之和为偶数
return (double)((max(nums1LeftMax, nums2LeftMax) + min(nums1RightMin, nums2RightMin))) / 2;
}
6.性能分析
时间复杂度O(log min(m,n))
空间复杂度O(1)
AC代码:(C++)
class Solution {
public:
double findMedianSortedArrays(vector<int> &nums1, vector<int> &nums2) {
/*
1.确定分割线两端数组元素个数
2.找见分割线
*/
int len1 = nums1.size(), len2 = nums2.size();
if (len1 > len2)
return findMedianSortedArrays(nums2, nums1); //保证第一个数组较短一些
//确定分隔线两边数组元素的个数 直接使用(len1 + len2 + 1) / 2 有可能越界
int totalLeft = len1 + (len2 - len1 + 1) / 2;
//在nums1的区间[0,len1]里查找恰当的分隔线
//使得nums1[i-1]<=nums2[j] && nums2[j-1]<=nums1[i]
int left = 0, right = len1;
/*
while (left < right) {
// left == right时 循环退出
//分隔线在第一个数组的位置 同理(left + right) / 2 有可能越界
//+1是为了不出现死循环,即left永远小于right的情况,同时保证不会出现下标越界,即i-1<0的情况。
int i = left + (right - left + 1) / 2; //第一个数组左边元素的个数
int j = totalLeft - i; //分隔线在第二个数组左边元素的个数
if (nums1[i - 1] > nums2[j]) {
//第一个数组分隔线左边元素大于第二个数组分隔线右边元素
//分隔线需要左移 下一轮在[left,i-1]里面寻找
right = i - 1;
} else {
// 此时说明条件满足,应当将左指针右移到i的位置
// 下一轮在[i,right]里面寻找
// [left(i),right] 如果i = left + (right - left) / 2
//
如果只有两个元素的时候,一旦左边界为i,区间就不会缩小了,即死循环,因此要在判断的时候向上取整,即int
// i = left + (right - left + 1) / 2 这里要加一
// 同时可以保证i不会取到0,即i-1不会越界
left = i;
}
}
*/
while (left < right) {
int i = left + (right - left) / 2;
int j = totalLeft - i;
if (nums2[j - 1] > nums1[i]) {
//第二个数组的分隔线左边元素大于第一个数组分隔线右边的元素
//说明分隔线需要向右移 下一轮在[i+1,right]里面查找
//因为这样不会导致系循环,于是这里不需要上取整,i的定义里不需要加一,同时i不会取到m导致越界
left = i + 1;
} else {
//否则下一轮搜索区间为[left,i]
right = i;
}
}
int i = left, j = totalLeft - i; //分隔线的位置;
// i=0说明第一个数组分隔线左边没有值,为了不影响其与nums2LeftMax的比较,将其设置为Int的最小值
int nums1LeftMax = i == 0 ? INT_MIN : nums1[i - 1];
// j=len1说明第一个数组分隔线左边没有值,为了不影响其与nums2RightMin的比较,将其设置为Int的最大值
int nums1RightMin = i == len1 ? INT_MAX : nums1[i];
// j=0说明第一个数组分隔线左边没有值,为了不影响其与nums1LeftMax的比较,将其设置为Int的最小值
int nums2LeftMax = j == 0 ? INT_MIN : nums2[j - 1];
// j=len2说明第一个数组分隔线左边没有值,为了不影响其与nums1RightMin的比较,将其设置为Int的最大值
int nums2RightMin = j == len2 ? INT_MAX : nums2[j];
if ((len1 + len2) % 2 == 1) {
//如果两数组长度之和为奇数
return max(nums1LeftMax, nums2LeftMax);
} else {
//两数组长度
//之和为偶数
return (double)((max(nums1LeftMax, nums2LeftMax) + min(nums1RightMin, nums2RightMin))) / 2;
}
}
};