问题:
There are two sorted arrays nums1 and nums2 of size m and n respectively.
Find the median of the two sorted arrays. The overall run time complexity should be O(log (m+n)).
You may assume nums1 and nums2 cannot be both empty.
给定两个大小为 m 和 n 的有序数组 nums1
和 nums2
。
请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。
你可以假设 nums1
和 nums2
不会同时为空。
分析:
题目要求log复杂度,加上两个数组是有序的,很容易就想到二分查找的方法
第一,找到中位数的问题,其实就是找到排序为第k位(k从0开始)的数的问题
第二,二分查找法的思路就是取mid——比较大小——缩小查找区间的过程,其他都好说,比较大小怎么比?假设我们在nums1中查找第k位的数,两个数组都是有序的,所以nums1[mid1]这个元素,在nums1中有mid1个元素比它小,那么如果nums2中恰好有k-mid1个数比nums1[mid1]小,nums1[mid1]就是第k位数,如果nums2中比它小的数多于k-mid1个,nums1[mid1]就太大,反之太小
第三,如果第k位的数不在nums1中,需要在nums2里再查找吗?不用的,在nums1的查找过程中,lo1-1永远指向比第k位数小的数,当查找结束后,就表示nums1中有lo1个数是比第k位数小的,显而易见,第k位数就是nums2[k-lo1]
代码:
/**
* 寻找两个有序数组的中位数
* Median of Two Sorted Arrays
*
* @author DongWei
* @version 2019/5/31
*/
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
// 如果有空数组,直接返回结果
if (m == 0) {
if (n % 2 == 0) {
return (nums2[n >> 1] + nums2[(n >> 1) - 1]) / 2.0;
} else {
return nums2[n >> 1];
}
}
if (n == 0) {
if (m % 2 == 0) {
return (nums1[m >> 1] + nums1[(m >> 1) - 1]) / 2.0;
} else {
return nums1[m >> 1];
}
}
// 开始在 nums1 中查找第 k 位数,即中位数,对于偶数长度的数组而言,查找较大的那个中位数
int k = (m + n) >> 1;
int lo1 = 0;
int hi1 = Math.min(m - 1, k);
while(true) {
if (lo1 <= hi1) {
// 二分查找
int mid1 = (lo1 + hi1) >> 1;
double res = binarySearch(mid1, k, nums1, nums2);
if (res == Double.MAX_VALUE) {
hi1 = mid1 - 1;
} else if (res == Double.MIN_VALUE) {
lo1 = mid1 + 1;
} else {
return res;
}
} else {
// 中位数在 nums2 中
if ((n + m) % 2 == 0) {
// 长度为偶数,根据不同情况计算较小的中位数
if (k <= lo1) {
// 较大中位数是 nums2[0],较小中位数必然是 num1 的最后一个元素
return (nums2[k - lo1] + nums1[lo1 - 1]) / 2.0;
} else if (lo1 == 0) {
// nums1 中没有任何元素比较大中位数小,较小中位数必然在 nums2 中
return (nums2[k - lo1] + nums2[k - lo1 - 1]) / 2.0;
} else {
// 较小中位数可能是在 nums1 或 nums2 中,比较两个数的大小确定
return (nums2[k - lo1] + Math.max(nums2[k - lo1 - 1], nums1[lo1 - 1])) / 2.0;
}
} else {
// 长度为奇数,直接返回结果
return nums2[k - lo1];
}
}
}
}
/**
* 判断元素是不是中位数
*
* @param v 待比较元素
* @param need nums 中如果有 need 个元素比 v 小,说明 v 恰好是中位数
* @param nums2 另一个数组
* @return 判断这个元素比中位数大还是小
*/
private int compare(int v, int need, int[] nums2) {
// 处理数组边界情况
int val1 = Integer.MIN_VALUE;
int val2 = Integer.MAX_VALUE;
if (need > 0 && need < nums2.length) {
val1 = nums2[need - 1];
val2 = nums2[need];
} else if (need == 0) {
val2 = nums2[need];
} else if (need == nums2.length) {
val1 = nums2[need - 1];
} else {
val1 = Integer.MAX_VALUE;
}
// 如果 nums2[need] 比 v 大,nums2[need - 1] 比 v 小,则 nums2 中恰好有 need 个元素比 v 小
if (val2 >= v && val1 <= v) {
return 0;
} else if (val2 < v) {
return 1;
} else {
return -1;
}
}
/**
* 二分查找中位数
*
* @param mid 二分查找中点
* @param k 中位数是第 k 位元素
* @param nums1 数组 1
* @param nums2 数组 2
* @return 如果 mid 元素是中位数,依照数组长度奇偶,计算中位数结果,否则用 Double.MAX_VALUE 和 Double.MIN_VALUE 表示大了还是小了
*/
private double binarySearch(int mid, int k, int[] nums1, int[] nums2) {
// nums2 中需要恰好有 k - mid 个元素比 nums1[mid] 小
int need = k - mid;
int cmp = compare(nums1[mid], need, nums2);
if (cmp == 0) {
// 计算中位数
if ((nums1.length + nums2.length) % 2 == 0) {
if (need == 0) {
// 较大中位数恰好是 nums1 的第 k 位数,nums2 中没有比中位数小的元素
return (nums1[mid] + nums1[mid - 1]) / 2.0;
} else if(mid == 0) {
// 较大中位数恰好是 nums1 的首元素,较小中位数必然在 nums2 中
return (nums1[mid] + nums2[need - 1]) / 2.0;
} else {
// 较小中位数可能是在 nums1 或 nums2 中,比较两个数的大小确定
return (Math.max(nums1[mid - 1], nums2[need - 1]) + nums1[mid]) / 2.0;
}
} else {
return nums1[mid];
}
} else if (cmp == 1) {
return Double.MAX_VALUE;
} else {
return Double.MIN_VALUE;
}
}
}
这个代码跑起来速度10ms~14ms不等,一般是12ms左右
觉得自己这个算法写起来十分冗长, 不优雅,参考了别人的算法,用的另一种二分查找思路,不是在nums1和nums2里查找,而是在k上进行二分查找
第一,查找第k位数,数组中有k个元素比它小,这k个元素分布在nums1和nums2中,必然有一个数组中比中位数小的元素个数较大,且数量大于等于k/2个,相当于每次二分查找我们可以确定k/2个比中位数小的数字在哪。例如下图中,一共9个元素,中位数是第4位元素5,在数组2中有3个元素比5小,超过了4/2,确定了4/2个元素比中位数小,下次查找就可以排除灰色的范围
第二,怎么判断比中位数小的元素在哪个数组里分布的数量更多?比较一下两个数组的第k/2个元素,比较小的元素所在数组,必然有k/2个元素比中位数小
代码如下,比我的快1~2ms,简洁
/**
* 寻找两个有序数组的中位数
* Median of Two Sorted Arrays
*
* @author DongWei
* @date 2019/5/31
*/
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
int target = ((m + n) >> 1) + 1;
if ((m + n) % 2 == 0) {
return (getKth(target, 0, 0, nums1, nums2) + getKth(target - 1, 0, 0, nums1, nums2)) / 2.0;
} else {
return getKth(target, 0, 0, nums1, nums2);
}
}
/**
* 查找第 k 位数
*
* @param k 查找第 k 位数
* @param lo1 nums1 的查找起始范围
* @param lo2 nums2 的查找起始范围
* @param nums1 数组 1
* @param nums2 数组 2
* @return 第 k 位数
*/
private double getKth(int k, int lo1, int lo2, int[] nums1, int[] nums2) {
// 如果一个数组的查找起始范围超过了下标范围,说明第 k 位数在另一个数组里
if (lo1 >= nums1.length) return nums2[lo2 + k - 1];
if (lo2 >= nums2.length) return nums1[lo1 + k - 1];
// 查找第 1 位数,比较大小返回
if (k == 1) {
return Math.min(nums1[lo1], nums2[lo2]);
}
// 判断哪个数组中有 k / 2 个元素小于中位数
// 如果数组的剩余长度小于 k / 2,用 Integer.MAX_VALUE 处理,比较大小后判定结果一定是另一个数组
int value1 = Integer.MAX_VALUE;
int value2 = Integer.MAX_VALUE;
if (lo1 + (k >> 1) <= nums1.length) value1 = nums1[lo1 + (k >> 1) - 1];
if (lo2 + (k >> 1) <= nums2.length) value2 = nums2[lo2 + (k >> 1) - 1];
// 比较一下两个数组的第 k/2 个元素,比较小的元素所在数组,必然有 k/2 个元素比中位数小
if (value1 < value2) {
return getKth(k - (k >> 1), lo1 + (k >> 1), lo2, nums1, nums2);
} else {
return getKth(k - (k >> 1), lo1, lo2 + (k >> 1), nums1, nums2);
}
}
}
运行速度9ms~13ms