版本 | 说明 | 发布日期 |
---|---|---|
1.0 | 发布文章第一版 | 2021-06-03 |
寻找两个正序数组的中位数
题目链接
寻找算法
- 这道题不考虑时间复杂度的话,本身很简单,用一个归并排序就出来了,时间复杂度是
O(m+n)
,空间复杂度是O(m+n)
。 - 但是这道题难在需找时间复杂度为
O(log(m+n))
的解。 - 数组、有序、
O(log(m+n))
,这三个特点组合在一起,就很容易可以联想到二分查找。这道题如果没有时间复杂度的提示,能想到二分查找确实非常困难。
思路
总体思路
- 首先来思考一个有序数组:
- 可以用一个分割线把数组分为左右两个“尽可能均匀”的部分。所谓“尽可能平均”,在本题解中定义为:当元素个数为奇数时,让左边的元素比右边元素个数多一个。
- 元素个数为偶数时,中位数为分割线左右两个元素的平均值;
- 元素个数为奇数时,中位数为分割线左边的元素值。
- 可以用一个分割线把数组分为左右两个“尽可能均匀”的部分。所谓“尽可能平均”,在本题解中定义为:当元素个数为奇数时,让左边的元素比右边元素个数多一个。
- 按照这种思路,可以思考一下如果是两个有序数组,怎么找到他们共同的中位数?此时我们得想象这两个数组被排成一个数组的样子:
- 可以用一个分割线把两个数组分为左右两个“尽可能均匀”的部分。但是因为是两个数组,这种分法会有很多种,那哪一种是正确的呢?其实多画几个例子,就可以发现,正确的分割线,一定满足下面的条件:
- nums1在分割线左边的数 <= nums2在分割线右边的数;
- nums2在分割线左边的数 <= nums1在分割线右边的数。
- 为什么是这个样子?其实像下面这样思考一下就知道了:满足上面这个条件,实际上代表着将分割线划在了使一个数组“尽可能均匀”地被分割成两个部分的地方。
- 可以用一个分割线把两个数组分为左右两个“尽可能均匀”的部分。但是因为是两个数组,这种分法会有很多种,那哪一种是正确的呢?其实多画几个例子,就可以发现,正确的分割线,一定满足下面的条件:
- 此时怎么计算中位数呢?
- 如果个数为偶数,一个数组的时候,左右两个元素的平均值,其实可以理解为分割线左边最大值和右边最小值的平均值。那两个数组的时候也是一样的:分割线左边的最大值与右边的最小值的平均值。
- 如果个数为奇数,以同样的思考方式,可以得出结论:分割线左边的最大值就是中位数。
- 特殊情况
- 从上面这张图就能看出一种特殊情况:分割线可能会划在数组的边缘。此时会对大小的比较产生一定的难度。
- 这个问题很好解决,如图,因为在nums1的右边缘,所以分割线右边的值肯定是不会被选中的(因为不存在),所以我们可以假定这个值为正无穷大;
- 其余边缘的情况同理。
- 从上面这张图就能看出一种特殊情况:分割线可能会划在数组的边缘。此时会对大小的比较产生一定的难度。
寻找分割线
- 道理我都懂,但是怎么才能找到这个分割线呢?
- 其实把上面的三个条件拎出来,就能发现,这其实就是一个在一维有序数组里面找一个满足条件的值的过程,也就是说,二分查找就这么用上了:
- nums1在分割线左边的数 <= nums2在分割线右边的数;
- nums2在分割线左边的数 <= nums1在分割线右边的数。
- 该分割线始终让左右两边的数保持“尽可能均匀”。
- 怎么保持“尽可能均匀”?因为int运算的向下取整特性,不论数组是奇数个还是偶数个,我们只需要计算
int sizeLeft = (m + n + 1) / 2
即可。
- 怎么保持“尽可能均匀”?因为int运算的向下取整特性,不论数组是奇数个还是偶数个,我们只需要计算
- 因此具体步骤如下:
- 对于nums1,采用二分查找,来选中一个分割线的位置。由于sizeLeft的限制,nums2的分割线位置自然也就确定了;
- 对该分割线进行条件满足性校验,直到两个条件同时满足:
- 如果nums1左边的数大了,那么就在左半区间进行查找;
- 如果nums1右边的数小了,那么就在右半区间进行查找。
代码
- 有两个小细节:
- 当折半查找的left等于right的时候,此时不需要再判断了,此时的位置一定是目标分割线。至于原因小伙伴们可以自行验证一下。这样做的好处是可以避免nums1[wire1]访问越界。
- 我们始终将最短的数组作为nums1,可以避免nums2的访问越界。
- 该解的时间复杂度为折半查找效率:
O(log(m + n))
。空间复杂度为temp数组O(max(m, n))
。
public static double findMedianSortedArrays(int[] nums1, int[] nums2) {
// 是否是偶数
boolean isEven = (nums1.length + nums2.length) % 2 == 0;
// 左边的大小
int sizeLeft = (nums1.length + nums2.length + 1) / 2;
// 将短的数组作为num1。这样做的好处是:nums2的访问一定不会越界。
if (nums1.length > nums2.length) {
int[] temp = nums1;
nums1 = nums2;
nums2 = temp;
}
// 寻找分割线。
// left == right时,一定是目标分割线,所以不用判断。跳过该情况的好处是:nums1[wire1]一定不会越界。
int wire1 = 0, wire2 = 0;
for (int left = 0, right = nums1.length; left <= right; ) {
wire1 = (left + right) / 2;
wire2 = sizeLeft - wire1;
if(left == right){
break;
}
// 如果wire1左边的值大于wire2右边的值,则分割线需要左移
if (wire1 != 0 && nums1[wire1 - 1] > nums2[wire2]) {
right = wire1 - 1;
continue;
}
// 如果wire2左边的值大于wire1右边的值,则分割线需要右移
if (nums1[wire1] < nums2[wire2 - 1]) {
left = wire1 + 1;
continue;
}
// 如果都满足,则结束循环
break;
}
int nums1Left = wire1 == 0 ? Integer.MIN_VALUE : nums1[wire1 - 1];
int nums1Right = wire1 == nums1.length ? Integer.MAX_VALUE : nums1[wire1];
int nums2Left = wire2 == 0 ? Integer.MIN_VALUE : nums2[wire2 - 1];
int nums2Right = wire2 == nums2.length ? Integer.MAX_VALUE : nums2[wire2];
// 如果是偶数,则为平均值
if (isEven) {
return (Integer.max(nums1Left, nums2Left) + Integer.min(nums1Right, nums2Right)) / 2.0;
}
// 如果是奇数,则为左边的最大值
else {
return Integer.max(nums1Left, nums2Left);
}
}