1. 题目
给定两个大小为 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
2. 解答
方案: 最懒的一种方式,先进行所有对象排列,放入一个队列,然后去获取中位数。我这里的时间复杂度是 O(m+n)。
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
// 首先处理情况
if (m == 0) {
// 奇数
if (n % 2 == 1) {
return nums2[(n - 1) / 2];
}
// 偶数 取中间两数的平均值
else {
return (nums2[n / 2] + nums2[n / 2 - 1]) / 2.0;
}
}
if (n == 0) {
// 奇数
if (m % 2 == 1) {
return nums1[(m - 1) / 2];
}
// 偶数 取中间两数的平均值
else {
return (nums1[m / 2] + nums1[m / 2 - 1]) / 2.0;
}
}
// 正常情况
int s1 = 0, s2 = 0;
List<Integer> list = new ArrayList<>();
while (list.size() != (m + n)) {
if (nums1[s1] <= nums2[s2]) {
list.add(nums1[s1]);
if (s1 != m-1) {
s1++;
}else {
for (int i = s2; i < n; i++) {
list.add(nums2[i]);
}
}
} else {
list.add(nums2[s2]);
if (s2 != n-1) {
s2++;
}else {
for (int i = s1; i < m; i++) {
list.add(nums1[i]);
}
}
}
}
// 取数
if ((m + n) % 2 == 1) {
return list.get((m + n - 1) / 2);
}
// 偶数 取中间两数的平均值
else {
return (list.get((m + n) / 2) + list.get((m + n) / 2 - 1)) / 2.0;
}
}
3. 评论最佳方案
说完我偷懒不符合答案的方式后,说下评论最优解
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
int left = (m + n + 1) / 2;
int right = (m + n + 2) / 2;
return (cal(nums1, 0, nums2, 0, left) + cal(nums1, 0, nums2, 0, right)) / 2.0;
}
private double cal(int[] nums1, int i, int[] nums2, int j, int k) {
if (i >= nums1.length) return nums2[j + k - 1];//nums1为空数组
if (j >= nums2.length) return nums1[i + k - 1];//nums2为空数组
if (k == 1) {
return Math.min(nums1[i], nums2[j]);
}
int midVal1 = (i + k / 2 - 1 < nums1.length) ? nums1[i + k / 2 - 1] : Integer.MAX_VALUE;
int midVal2 = (j + k / 2 - 1 < nums2.length) ? nums2[j + k / 2 - 1] : Integer.MAX_VALUE;
if (midVal1 < midVal2) {
return cal(nums1, i + k / 2, nums2, j, k - k / 2);
} else {
return cal(nums1, i, nums2, j + k / 2, k - k / 2);
}
}
3.1 理解思路
- log(m+n) 的方式必须是二分法
- 考虑基数和偶数的取值方式必须一致
- 半数分割,可以取数组的值的中间值来判断。
- 递归舍去一半的值。
- 特殊情况处理
差不多就这五点。
3.2 解决奇数偶数的问题
奇数和偶数不同的时候可以去取m,n保持一致
- 奇数:获取的是中间值,最终坐标为 (m+n+1)/2
- 偶是:获取的是中间两个值的平均,最终坐标是 (m+n)/2 和 (m+n+2)/2 的平均值
- 统合奇数偶数:
由于计算机的特征除法会舍去余数。
m+n为奇数的时候 (m+n+1)/2 与(m+n+2)/2 相等。
m+n为偶数的时候 (m+n)/2 与 (m+n+1)/2 相等。
int left = (m + n + 1) / 2;
int right = (m + n + 2) / 2;
也就是说最终取的是 left 与 right 的平均值。剩下的就是如何获取左侧的值和右侧的值。
return (cal(nums1, 0, nums2, 0, left) + cal(nums1, 0, nums2, 0, right)) / 2.0;
3.3 解决特殊情况
在处理奇数偶是的前提下,当其中一个为空数组,或者两者的长度都为1。
if (i >= nums1.length) return nums2[j + k - 1];//nums1为空数组
if (j >= nums2.length) return nums1[i + k - 1];//nums2为空数组
由于中间是对半拆分,不会存在下标越界的状况,其实也可以修改为:
if (i == nums1.length) return nums2[i + k - 1];//nums1为空数组
if (j == nums2.length) return nums1[ j + k - 1];//nums2为空数组
if (k == 1) {
return Math.min(nums1[i], nums2[j]);
}
因为只有特殊数组为空的情况才会发生,求取另外一个数组的中间值。
这里有两个问题:
- 为什么使用i(j) + k - 1 而不是 k -1 ?
是因为会存在第一次判断不越界,后续才会发生越界的情况。 - 为什么 k == 1的时候取值Math.min() 而不是 Math.max() ?
是因为当两个数组长度都为1的情况只有取值left的时候k=1,right取值k=2,根本走不到判断k==1,而会在判断坐标是否越界的时候直接返回。取左侧值的时候使用Math.min()。
例如:num1= {1},而num2 = {2} 的情况。这时候取right值的时候,
入参 i,j,k 为 0,0,2,第二次为0,1,1 ,这个时候 num2 的数组最大坐标为0,也会发生下标越界的情况。
3.4 正常状况
每次取值:i(j)+k/2-1,减一的作用是坐标从0开始,而在位移坐标的时候是长度,这里需要增加 k/2。每次k-k/2的结果也是为了每次折半查找中间值。当然这里的中间值是近似。
if (midVal1 < midVal2) {
return cal(nums1, i + k / 2, nums2, j, k - k / 2);
} else {
return cal(nums1, i, nums2, j + k / 2, k - k / 2);
}
这里也有一种特殊的状况,假如长度折半的时候不能保证移动的长度是否发生下标越界,判断折半下标不存在的时候取一个比较大的值。
- 问题1:什么情况发生折半下标不存在?
举例:num1={1,2,3,4,5,6,7},num2={1},这个时候的 k = 4 or 5,num2取中间折半的时候取 num2[j + k / 2 - 1] = num2[1] 不存在。 - 问题2:为什么取值 Integer.MAX_VALUE ?
在问题1的基础上,因为num2足够短,num1的位置是正确位置的1/4到1/2之间,但是不到1/2,这里num1的中间值mid1肯定是在最终值的左侧,所以渠道一个足够大的值计算即可,这里舍去num1即可,num2中的值是多少并不重要。
int midVal1 = (i + k / 2 - 1 < nums1.length) ? nums1[i + k / 2 - 1] : Integer.MAX_VALUE;
int midVal2 = (j + k / 2 - 1 < nums2.length) ? nums2[j + k / 2 - 1] : Integer.MAX_VALUE;
此处使用示例讲解:
例如: num1 = {1,2,3,4,5},num2= {3,4,5,6}
获取两数组中间值进行比较,mid1 为 每次num1的中间值,如果mid1 < mid2 ,那么就是舍去mid1左侧的元素。这里示例取left值的步骤。这里的中间数是剩余的长度/2取左侧值的位置。
1:num1={1,2,3,4,5} ,中间数2,num2= {3,4,5,6},中间数4,比较2<4,舍去num1的{1,2}和num2的{5,6},这时候i,j,k的入参为0,0,5,完成之后为2,0,3
2: num1剩余{3,4,5},中间数3,num2剩余{3,4},中间数3,比较3==3,舍去num1的{5}和num2的{3},这个时候的i,j,k入参为2,0,3,计算之后为2,1,2
3:num1剩余{3,4},中间数3,num2剩余{4},中间数4,比较3<4,舍去num1的{3},这个时候i,j,k入参为2,1,2,计算之后为3,1,1
4: 这个时候取k == 1,取剩余的 min(num1[3],num2[1]), left = 4,获取左侧的最大值。
剩下右侧的值推导和左侧相同,入参也是i,j,k 为 0,0,5最终得到right=4,最终的中位数也是 (4+4)/2.0 等于4.0
4. 总结
这个问题的思路难点二分法查找的时候,主要在于奇数和偶数的相同取值k,还有就是比较的时候在于取哪个值进行比较,花了大约两天的时候总算是明白了。那个排序组合然后取中位数的方法倒是没有任何难度。