leecode-4-寻找两个正序数组的中位数

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 理解思路

  1. log(m+n) 的方式必须是二分法
  2. 考虑基数和偶数的取值方式必须一致
  3. 半数分割,可以取数组的值的中间值来判断。
  4. 递归舍去一半的值。
  5. 特殊情况处理

差不多就这五点。

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]);
 }

因为只有特殊数组为空的情况才会发生,求取另外一个数组的中间值。
这里有两个问题:

  1. 为什么使用i(j) + k - 1 而不是 k -1 ?
    是因为会存在第一次判断不越界,后续才会发生越界的情况。
  2. 为什么 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,还有就是比较的时候在于取哪个值进行比较,花了大约两天的时候总算是明白了。那个排序组合然后取中位数的方法倒是没有任何难度。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值