2020-08-15

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

给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。
请你找出这两个正序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。
你可以假设 nums1 和 nums2 不会同时为空。
示例 1:
nums1 = [1, 3]
nums2 = [2]
则中位数是 2.0
示例 2:
nums1 = [1, 2]
nums2 = [3, 4]
则中位数是 (2 + 3)/2 = 2.5。

中位数和两个有序数组的长度之和有关:
当两个有序数组的长度之和为奇数的时候,中位数只有1个,将其返回
当两个有序数组的长度之和为偶数的时候,中位数有两个,返回合并,排序以后位于中间的两个数的平均值

思路:
方法一:暴力法:先合并两个有序数组,然后排序找到中位数,没有使用到数组有序这一条件,时间复杂度O((m+n)log(m+n)),空间复杂度O(m+n),存储合并以后的数组,需要m+n这么多空间
方法二:二分查找
中位数:在只有一个有序数组的时候,中位数把数组分割成两个部分
当数组长度为偶数时,中位数有两个,其中一个是左边数组的最大值,另一个是右边数组的最小值
在这里插入图片描述

当数组长度为奇数时,中位数有1个,可以将中位数分到左边数组

在这里插入图片描述
中位数:在两个有序数组的时候,仍然可以把两个数组分割成两个部分
在这里插入图片描述
在这里插入图片描述
特殊情况:
在这里插入图片描述
上面的特殊情况中,如果分割线的右边没有元素,分割线的左边包含了第一个数组的所有元素,我们需要在
第一个数组的左闭右闭区间[0,m]里查找 恰当分割线,这个分割线要满足交叉小于等于的关系,使得
nums1[i-1] <= nums2[j] && nums2[j-1] <= nums1[i]

public double findMedianSortedArrays(int[] nums1, int[] nums2) {
	if (nums1.length > nums2.length) {
		int[] temp = nums1;
		nums1  = nums2;
		nums2 = temp;
	}
	int m = nums1.length;
	int n = nums2.length;
	// 分割线左边的所有元素需要满足的个数
	int totalLeft = (m + n + 1) / 2;  // m + (n - m + 1)/2
	// 在nums1的区间[0, m]里查找恰当的分割线, 使得nums1[i-1] <= nums2[j] && nums2[j-1] <= nums1[i]
	int left = 0;
	int right = m;
	while (left < right) {
		int i = left + (right - left + 1)/2;
		int j = totalLeft - i;
		if (nums1[i-1] >  nums2[j]) {
			// 下一轮搜索的区间[left, i-1]
			right = i - 1;
		}else {
		    
		    // 看到这样的表达式要小心,当我们上面的int i = left + (right - left)/2;,在这个区间只有两个元素的时候 [left(i), right]一旦进入这个分支,左边界
		    // 成为i的时候,这个区间就不会在缩写,这个循环是个死循环,
		    // 所以要把i在取中位数的时候,变为int i = left + (right - left + 1)/2, 
		    // 既然在取中位数i的时候是left + (right - left + 1)/2,
		    //那么在循环体内部就不会执行到区间为0的下标,所以nums1[i-1]是不会越界的
		    
			left = i;// 下一轮搜索区间[i, right]
		}	
	}
	// 在退出循环的时候,就找到了满足nums1[i-1] <= nums2[j] && nums2[j-1] <= nums1[i]
	int i = left;
	int j = totalLeft - i;
	// 定义分割线左右两侧的四个元素, 这里注意,既然是下标的访问就要考虑数组下标越界的情况
// 这个表达式在i==0的时候没有意义,就需要设置为整型的最小值,好让在比较最大值的过程中不被选中
	int nums1LeftMax = i == 0 ? Interger.MIN_VALUE : nums1[i-1];
	// 当i==m时没有意义,同理
	int nums1RightMin  = i == m ? Interger.MAX_VALUE :nums1[i];
	int nums2LeftMax = j == 0 ? Interger.MIN_VALUE : nums2[j-1];
	int nums2RightMin =  j == n ? Interger.MAX_VALUEnums2[j];
// 当数组的长度为奇数的时候,返回的是分割线左边元素的最大值,
// 当数组的长度为偶数的时候,返回的是分割线左边元素的最大值和右边元素的最小值的平均数

	if (((m + n) & 1)== 1) {
		return Math.max(nums1LeftMax, nums2LeftMax);
	}else{
		return (double)(Math.max(nums1LeftMax, nums2LeftMax) + Math.max(nums1RightMin, nums2RightMin))/2;
	}
}

或者用nums2[j-1] <= nums1[i]条件取反作为判断条件

public double findMedianSortedArrays(int[] nums1, int[] nums2) {
	if (nums1.length > nums2.length) {
		int[] temp = nums1;
		nums1  = nums2;
		nums2 = temp;
	}
	int m = nums1.length;
	int n = nums2.length;
	// 分割线左边的所有元素需要满足的个数
	int totalLeft = (m + n + 1) / 2;  
	int left = 0;
	int right = m;
	while (left < right) {
		int i = left + (right - left + 1)/2;
		int j = totalLeft - i;
		if (nums2[j-1] > nums1[i]) {
			// 下一轮搜索的区间[i+1,right]
			left = i + 1; // 一旦看到left = i + 1这样的表达式, 就表示我们在取中位数的时候,不需要上取整
			//将int i = left + (right - left + 1)/2;变为int i = left + (right - left)/2
			// 由于在中位数不是上取整,也就表示在循环体内部不会取到m这个值,因此nums2[j-1]就不会发生数组下标越界
			//
		}else{
		// 下一轮搜索区间为[left, i]
			right = i;
		}
	}
	int i = left;
	int j = totalLeft - i;
	int nums1LeftMax = i == 0 ? Interger.MIN_VALUE : nums1[i-1];
	int nums1RightMin  = i == m ? Interger.MAX_VALUE :nums1[i];
	int nums2LeftMax = j == 0 ? Interger.MIN_VALUE : nums2[j-1];
	int nums2RightMin =  j == n ? Interger.MAX_VALUEnums2[j];
	if (((m + n) & 1)== 1) {
		return Math.max(nums1LeftMax, nums2LeftMax);
	}else{
		return (double)(Math.max(nums1LeftMax, nums2LeftMax) + Math.max(nums1RightMin, nums2RightMin))/2;
	}
}

时间复杂度:在这里我们是在较短的数组确定时间复杂度的,
所以为O(log min(m, n))这里m和n分别为数组nums1和nums2的长度

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int leftLength = nums1.length;
        int rightLength = nums2.length;
        // 为了保证第一个数组比第二个数组小(或者相等)
        if (leftLength > rightLength) {
            return findMedianSortedArrays(nums2, nums1);
        }
        // 分割线左边的所有元素需要满足的个数 m + (n - m + 1) / 2;
        // 两个数组长度之和为偶数时,当在长度之和上+1时,由于整除是向下取整,所以不会改变结果
        // 两个数组长度之和为奇数时,按照分割线的左边比右边多一个元素的要求,此时在长度之和上+1,就会被2整除,会在原来的数
        //的基础上+1,于是多出来的那个1就是左边比右边多出来的一个元素
        int totalLeft = (leftLength + rightLength + 1) / 2;
        // 在 nums1 的区间 [0, leftLength] 里查找恰当的分割线,
        // 使得 nums1[i - 1] <= nums2[j] && nums2[j - 1] <= nums1[i]
        int left = 0;
        int right = leftLength;
        // nums1[i - 1] <= nums2[j]
        //  此处要求第一个数组中分割线的左边的值 不大于(小于等于) 第二个数组中分割线的右边的值
        // nums2[j - 1] <= nums1[i]
        //  此处要求第二个数组中分割线的左边的值 不大于(小于等于) 第一个数组中分割线的右边的值
        // 循环条件结束的条件为指针重合,即分割线已找到
        while (left < right) {
            // 二分查找,此处为取第一个数组中左右指针下标的中位数,决定起始位置
            // 此处+1首先是为了不出现死循环,即left永远小于right的情况
            // left和right最小差距是1,此时下面的计算结果如果不加1会出现i一直=left的情况,而+1之后i才会=right
            // 于是在left=i的时候可以破坏循环条件,其次下标+1还会保证下标不会越界,因为+1之后向上取整,保证了
            // i不会取到0值,即i-1不会小于0
            // 此时i也代表着在一个数组中左边的元素的个数
            int i = left + (right - left + 1) / 2;
            // 第一个数组中左边的元素个数确定后,用左边元素的总和-第一个数组中元素的总和=第二个元素中左边的元素的总和
            // 此时j就是第二个元素中左边的元素的个数
            int j = totalLeft - i;
            // 此处用了nums1[i - 1] <= nums2[j]的取反,当第一个数组中分割线的左边的值大于第二个数组中分割线的右边的值
            // 说明又指针应该左移,即-1
            if (nums1[i - 1] > nums2[j]) {
                // 下一轮搜索的区间 [left, i - 1]
                right = i - 1;
                // 此时说明条件满足,应当将左指针右移到i的位置,至于为什么是右移,请看i的定义
            } else {
                // 下一轮搜索的区间 [i, right]
                left = i;
            }
        }
        // 退出循环时left一定等于right,所以此时等于left和right都可以
        // 为什么left一定不会大于right?因为left=i。
        // 此时i代表分割线在第一个数组中所在的位置
        // nums1[i]为第一个数组中分割线右边的第一个值
        // nums[i-1]即第一个数组中分割线左边的第一个值
        int i = left;
        // 此时j代表分割线在第二个数组中的位置
        // nums2[j]为第一个数组中分割线右边的第一个值
        // nums2[j-1]即第一个数组中分割线左边的第一个值
        int j = totalLeft - i;
        // 当i=0时,说明第一个数组分割线左边没有值,为了不影响
        // nums1[i - 1] <= nums2[j] 和 Math.max(nums1LeftMax, nums2LeftMax)
        // 的判断,所以将它设置为int的最小值
        int nums1LeftMax = i == 0 ? Integer.MIN_VALUE : nums1[i - 1];
        // 等i=第一个数组的长度时,说明第一个数组分割线右边没有值,为了不影响
        // nums2[j - 1] <= nums1[i] 和 Math.min(nums1RightMin, nums2RightMin)
        // 的判断,所以将它设置为int的最大值
        int nums1RightMin = i == leftLength ? Integer.MAX_VALUE : nums1[i];
        // 当j=0时,说明第二个数组分割线左边没有值,为了不影响
        // nums2[j - 1] <= nums1[i] 和 Math.max(nums1LeftMax, nums2LeftMax)
        // 的判断,所以将它设置为int的最小值
        int nums2LeftMax = j == 0 ? Integer.MIN_VALUE : nums2[j - 1];
        // 等j=第二个数组的长度时,说明第二个数组分割线右边没有值,为了不影响
        // nums1[i - 1] <= nums2[j] 和 Math.min(nums1RightMin, nums2RightMin)
        // 的判断,所以将它设置为int的最大值
        int nums2RightMin = j == rightLength ? Integer.MAX_VALUE : nums2[j];
        // 如果两个数组的长度之和为奇数,直接返回两个数组在分割线左边的最大值即可
        if (((leftLength + rightLength) % 2) == 1) {
            return Math.max(nums1LeftMax, nums2LeftMax);
        } else {
            // 如果两个数组的长度之和为偶数,返回的是两个数组在左边的最大值和两个数组在右边的最小值的和的二分之一
            // 此处不能被向下取整,所以要强制转换为double类型
            return (double) ((Math.max(nums1LeftMax, nums2LeftMax) + Math.min(nums1RightMin, nums2RightMin))) / 2;
        }
    }
}

&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
从暴力到优化
方法一:暴力法:先将两个数组合并,然后根据奇偶,返回中位数

public double findMedianSortedArrays(int[] nums1,  int[] nums2) {
	int[] nums;
	int m = nums1.length;
	int n = nums2.length;
	nums = new int[n + m];
	if (m == 0) {
		if ((n & 1) == 0) {
			return (nums2[n/2 - 1] + nums2[n/2]) / 2.0;
		}else {
			return nums2[n/2];
		}
	}
	if (n == 0) {
		if ((m & 1) == 0) {
			return (nums1[m/2 - 1] + nums1[m/2]) / 2.0;
		}else {
			return nums1[m/2];
		}
	}
	int count = 0;
	int i = 0, j = 0;
	while (count != (m+n)) {
		if (i == m) {
			while (j != n) {
				nums[count++] = nums2[j++];
			}
			break;
		}
		if (j == n) {
			while (i != m) {
				nums[count++] = nums1[i++];
			}
			break;
		}
		if (nums1[i] < nums2[j]) {
			nums[count++] = nums1[i++];
		}else{
			nums[count++] = nums2[j++];
		}
	}
	if ((count & 1) == 0) {
		return (nums[count/2 - 1] + nums[count/2])/2.0;
	}else{
		return nums[count/2];
	}
}

时间复杂度:遍历全部数组(m+n);
空间复杂度,开辟了一个数组,保存合并后两个数组O(m+n)

方法二:优化暴力解法:我们不需要将两个数组真的合并,我们只需要找到中位数在哪里就可以了

首先是怎么将奇数和偶数的情况合并一下。
用 len 表示合并后数组的长度,如果是奇数,我们需要知道第 (len+1)/2 个数就可以了,如果遍历的话需要遍历 int(len/2 ) + 1 次。如果是偶数,我们需要知道第 len/2和 len/2+1 个数,也是需要遍历 len/2+1 次。所以遍历的话,奇数和偶数都是 len/2+1 次。
返回中位数的话,奇数需要最后一次遍历的结果就可以了,偶数需要最后一次和上一次遍历的结果。所以我们用两个变量 left 和 right,right 保存当前循环的结果,在每次循环前将 right 的值赋给 left。这样在最后一次循环的时候,left 将得到 right 的值,也就是上一次循环的结果,接下来 right 更新为最后一次的结果。
循环中该怎么写,什么时候 A 数组后移,什么时候 B 数组后移。用 aStart 和 bStart 分别表示当前指向 A 数组和 B 数组的位置。如果 aStart 还没有到最后并且此时 A 位置的数字小于 B 位置的数组,那么就可以后移了。也就是aStart<m&&A[aStart]< B[bStart]。
但如果 B 数组此刻已经没有数字了,继续取数字 B[ bStart ],则会越界,所以判断下 bStart 是否大于数组长度了,这样 || 后边的就不会执行了,也就不会导致错误了,所以增加为 aStart<m&&(bStart) >= n||A[aStart]<B[bStart]) 。

public double findMedianSortedArrays(int[] A, int[] B) {
	int m = A.length, m = B.length, len = m + n;
	int left = -1, right = -1;
	int aStart = 0, bStart = 0;
	for (int i = 0; i < len/2; i++) {
		left = right;
		if (aStart < m && (bStart >= n || A[aStart] < B[bStart])) {
			right = A[aStart++];
		}else {
			right = B[bStart++];
		}
	}
	if ((len & 1) == 0) {
		return (left + right)/2.0;
	}else {
		return right;
	}

}

在这里插入图片描述

方法三:上边的两种思路,时间复杂度都达不到题目的要求 O(log(m+n)。看到 log,很明显,我们只有用到二分的方法才能达到。我们不妨用另一种思路,题目是求中位数,其实就是求第 k 小数的一种特殊情况,而求第 k 小数有一种算法。

解法二中,我们一次遍历就相当于去掉不可能是中位数的一个值,也就是一个一个排除。由于数列是有序的,其实我们完全可以一半儿一半儿的排除。假设我们要找第 k 小数,我们可以每次循环排除掉 k/2 个数。看下边一个例子。

假设我们要找第 7 小的数字。

在这里插入图片描述
我们比较两个数组的第 k/2 个数字,如果 k 是奇数,向下取整。也就是比较第 3 个数字,上边数组中的 4 和下边数组中的 3,如果哪个小,就表明该数组的前 k/2 个数字都不是第 k 小数字,所以可以排除。也就是 1,2,3 这三个数字不可能是第 7 小的数字,我们可以把它排除掉。将 1349 和 45678910 两个数组作为新的数组进行比较。

在这里插入图片描述
由于我们已经排除掉了 3 个数字,就是这 3 个数字一定在最前边,所以在两个新数组中,我们只需要找第 7 - 3 = 4 小的数字就可以了,也就是 k = 4。此时两个数组,比较第 2 个数字,3 < 5,所以我们可以把小的那个数组中的 1 ,3 排除掉了。

在这里插入图片描述
我们又排除掉 2 个数字,所以现在找第 4 - 2 = 2 小的数字就可以了。此时比较两个数组中的第 k / 2 = 1 个数,4 == 4,怎么办呢?由于两个数相等,所以我们无论去掉哪个数组中的都行,因为去掉 1 个总会保留 1 个的,所以没有影响。为了统一,我们就假设 4 > 4 吧,所以此时将下边的 4 去掉。
在这里插入图片描述
由于又去掉 1 个数字,此时我们要找第 1 小的数字,所以只需判断两个数组中第一个数字哪个小就可以了,也就是 4。

所以第 7 小的数字是 4。

我们每次都是取 k/2 的数进行比较,有时候可能会遇到数组长度小于 k/2的时候。

在这里插入图片描述
此时 k / 2 等于 3,而上边的数组长度是 2,我们此时将箭头指向它的末尾就可以了。这样的话,由于 2 < 3,所以就会导致上边的数组 1,2 都被排除。造成下边的情况。

在这里插入图片描述
由于 2 个元素被排除,所以此时 k = 5,又由于上边的数组已经空了,我们只需要返回下边的数组的第 5 个数字就可以了。

从上边可以看到,无论是找第奇数个还是第偶数个数字,对我们的算法并没有影响,而且在算法进行中,k 的值都有可能从奇数变为偶数,最终都会变为 1 或者由于一个数组空了,直接返回结果。
所以我们采用递归的思路,为了防止数组长度小于 k/2,所以每次比较 min(k/2,len(数组) 对应的数字,把小的那个对应的数组的数字排除,将两个新数组进入递归,并且 k 要减去排除的数字的个数。递归出口就是当 k=1 或者其中一个数字长度是 0 了。

public double findMedianSortedArrays(int[] nums1, int[] nums2) {
    int n = nums1.length;
    int m = nums2.length;
    int left = (n + m + 1) / 2;
    int right = (n + m + 2) / 2;
    //将偶数和奇数的情况合并,如果是奇数,会求两次同样的 k 。
    return (getKth(nums1, 0, n - 1, nums2, 0, m - 1, left) + getKth(nums1, 0, n - 1, nums2, 0, m - 1, right)) * 0.5;  
}
    
    private int getKth(int[] nums1, int start1, int end1, int[] nums2, int start2, int end2, int k) {
        int len1 = end1 - start1 + 1;
        int len2 = end2 - start2 + 1;
        //让 len1 的长度小于 len2,这样就能保证如果有数组空了,一定是 len1 
        if (len1 > len2) return getKth(nums2, start2, end2, nums1, start1, end1, k);
        if (len1 == 0) return nums2[start2 + k - 1];

        if (k == 1) return Math.min(nums1[start1], nums2[start2]);

        int i = start1 + Math.min(len1, k / 2) - 1;
        int j = start2 + Math.min(len2, k / 2) - 1;

        if (nums1[i] > nums2[j]) {
            return getKth(nums1, start1, end1, nums2, j + 1, end2, k - (j - start2 + 1));
        }
        else {
            return getKth(nums1, i + 1, end1, nums2, start2, end2, k - (i - start1 + 1));
        }
    }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值