参考资料:LeetCode某大佬的讲解
《程序员代码面试指南》上也有这道题的讲解,但是有点复杂,改天来补。
4. Median of Two Sorted Arrays
Given two sorted arrays nums1 and nums2 of size m and n respectively, return the median of the two sorted arrays.
The overall run time complexity should be O(log (m+n)).
Example 1:
Input: nums1 = [1,3], nums2 = [2]
Output: 2.00000
Explanation: merged array = [1,2,3] and median is 2.
Example 2:
Input: nums1 = [1,2], nums2 = [3,4]
Output: 2.50000
Explanation: merged array = [1,2,3,4] and median is (2 + 3) / 2 = 2.5.
思路1:
大体流程有点像合并两个有序链表,用两个指针分别指向两个数组的头部,哪个指针指的元素小,就移动哪个指针。只是,这里只需要记录中位数。
考虑到可以很方便地计算两个有序数组的长度,只需要找到第(nums1.len+nums2.len)/2位小的数即可。注意,如果(nums1.len+nums2.len)是奇数,那么答案就是 第(nums1.len+nums2.len)/2位小的数;
如果(nums1.len+nums2.len)是偶数,那么答案就是 第(nums1.len+nums2.len)/2位小的数 和 第(nums1.len+nums2.len)/2 + 1位小的数 的算术平均。于是,考虑用两个变量upper, lower记录上一个数和当前的数,表示合并数组的候选的上中位数和下中位数。
public double findMedianSortedArrays(int[] nums1, int[] nums2)
{
int m=nums1.length,n=nums2.length;
int len = m+n; // 5/2
int upper=-1, lower=-1;
int p1=0,p2=0;
for(int i=0;i<=len/2;i++){
upper=lower;
if(p1<m &&(p2>=n || nums1[p1]<nums2[p2])){// if p2>=n holds, then it means we used up nums2 and in this case "nums1[p1]<nums2[p2]" won't be excuted any more
lower = nums1[p1++];
}else{
lower = nums2[p2++];
}
}
return (len&1)==0?(upper+lower)/2.0:lower; //!! 2.0 not 2
}
思路2:
把问题泛化为求合并后数组的第k小的数,通过递归函数实现。
在递归函数内部,每一步从两个数组中的某一个数组确定下k/2个数(这些数因为太小而必然不是第k小的数),直到找到第k小的数为止。
注:我觉得设计的很巧妙的是 递归函数 base case. 随着递归函数的进行,k值逐渐减小、数组长度也逐渐减小。保证在递归函数中nums1[start1…end1]更短之后,分析nums1降到空的时候结论。
public double findMedianSortedArrays(int[] nums1, int[] nums2)
{
int n = nums1.length;
int m = nums2.length;
int upper = (m+n+1)/2;
int lower = (m+n+2)/2;
// 第upper小的数 是 合并后的上中位数
// 第lower小的数 是 合并后的下中位数
// if m+n is odd, then upper=lower
// if m+n is even, then upper+1=lower
return (getKth(nums1,0,n-1,nums2,0,m-1,upper)+getKth(nums1,0,n-1,nums2,0,m-1,lower))/2.0;
}// K starts from 1
public 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;
// we hope nums1 is a shorter one
// in this case, we can guarantee that it must be nums1 once some arr is empty
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; // nums1[start1,..,i] is ready to be determined
int j = start2 + Math.min(len2,k/2)-1;
if(nums1[i]>nums2[j]) // nums2[start2,...,j] was chosed as some part whose are less than the first kth
{
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));
}
}
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2)
{
int size = nums1.length+nums2.length;
boolean even = (size&1)==0;
if(nums1.length!=0 && nums2.length!=0)
{
if(even)
{
return (double)(findKthNum(nums1,nums2,size/2)+findKthNum(nums1,nums2,size/2+1))/2D;// 2D means Double 2
}
else
{
return findKthNum(nums1,nums2,size/2+1);
}
}
else if(nums1.length!=0)
{
if(even)
{
return (double)(nums1[(size-1)/2]+nums1[size/2])/2;
}
else
{
return nums1[size/2];
}
}
else if(nums2.length!=0)
{
if(even)
{
return (double)(nums2[(size-1)/2]+nums2[size/2])/2;
}
else
{
return nums2[size/2];
}
}
else
{
return 0;
}
}
public static int findKthNum(int[] arr1, int[] arr2, int kth)
{
// kth: 1,2,3,...
int[] longs = arr1.length>arr2.length?arr1:arr2;
int[] shorts = longs==arr1?arr2:arr1;
int s = shorts.length;
int l = longs.length;
if(kth<=s)// case 1: kth is too small
{
return getUpMedian(arr1,0,kth-1,arr2,0,kth-1);
}
if(kth>l) // case 2: kth is too large,所以要排除掉longs太首段的元素(即便是shorts全部在前面垫上,也不可能让这些元素有可能成为第kth个),也排除掉shorts中太首端的元素(即便是longs全部在前面垫上,也不可能让这些元素有可能成为第kth个)
{// 注意:这里之所以要人工地(用if语句)扣掉两个值shorts[kth-l-1]和longs[kth-s-1],是因为,这样的话才能保证getUpMedian(shorts,kth-l,s-1,longs,kth-s,l-1)返回的中位数恰好是 合并nums1&nums2后第kth个值,也就是我们想要的结果
// 具体地,扣掉两个值之后,从arr1确定下的值有 kth-l个, 从arr2确定下的值有kth-s个,getUpMedian(shorts,kth-l,s-1,longs,kth-s,l-1)返回确定下s-kth+l个,所以一共确定下kth个值,这正是我们想要的
// 然而,如果不扣掉两个值,从arr1确定下的值有 kth-l-1个, 从arr2确定下的值有kth-s-1个,getUpMedian(shorts,kth-l-1,s-1,longs,kth-s-1,l-1)返回确定下s-kth+l个,所以一共确定下kth-2个值,这不是我们想要的
// 1 2 3 4
// 1'2'3'4'5'6'7'
// shorts[kth-l-1..s-1], longs[kth-s-1..l-1]
// kth-l-1 + kth-s-1 + s+l-kth = kth-2 !=kth
// check
// shorts[kth-l-1] v.s. longs[l-1]
// longs[kth-s-1] v.s. shorts[s-1]
// then shorts[kth-l..s-1] and longs[kth-s..l-1]
// kth-l + kth-s + s+l-kth-1+1 = kth
if(shorts[kth-l-1]>=longs[l-1])
{
return shorts[kth-l-1];
}
if(longs[kth-s-1]>=shorts[s-1])
{
return longs[kth-s-1];
}
return getUpMedian(shorts,kth-l,s-1,longs,kth-s,l-1);
}
//case 3: kth适中,此时shorts中的所有元素都有可能做“合并后的第kth小”,而longs中位于太首段的、太尾端的元素都没有希望,因此要排除
// s<kth<=l
// 1 2 3 4
// 1'2'3'4'5'6'7'
// shorts[0..s-1] longs[kth-s-1..kth-1]
// check longs[kth-s-1] v.s. shorts[s-1]
// shorts[0..s-1] longs[kth-s..kth-1]
if(longs[kth-s-1]>=shorts[s-1])
{
return longs[kth-s-1];
}
return getUpMedian(shorts,0,s-1,longs,kth-s,kth-1);
}
public static int getUpMedian(int[] A, int s1, int e1, int[] B, int s2, int e2)
{
int mid1=0;
int mid2=0;
while(s1<e1)
{
mid1 = (s1+e1)/2;
mid2 = (s2+e2)/2;
if(A[mid1]==B[mid2])
{
return A[mid1];
}
else
{
if(((e1-s1+1)&1)==0)
{
// 1 2 3 4
// 1'2'3'4'
if(A[mid1]>B[mid2])
{
// 1 2 3' 4'
e1 = mid1;
s2 = mid2+1;
}
else
{
e2 = mid2;
s1 = mid1+1;
}
}
else// odd
{
// 1 2 3 4 5
// 1'2'3'4'5'
// 1 2 3' 4' 5'
if(A[mid1]>B[mid2])
{
if(B[mid2]>=A[mid1-1])
{
return B[mid2];
}
e1 = mid1-1;
s2=mid2+1;
}
else
{
if(A[mid1]>=B[mid2-1])
{
return A[mid1];
}
e2 = mid2-1;
s1 = mid1+1;
}
}
}
}
return Math.min(A[s1],B[s2]);
}
}
2023.08.25 看过书本后,又写了一遍。与上面的相比,思路相差不大,但是做了一些小优化,比如:一些if语句的写法上,还有getUpMedian函数中更新新搜索区间左端点时使用变量offset区分奇数、偶数情况。
思路:
把“找合并后数组的中位数”的问题(见findMedianSortedArrays函数)转成“找合并后数组第K小”的问题(见findKth函数),
而 对于“找合并后数组第K小”的问题, 根据kth的取值大小,分三种情况考虑(分别是kth<=s, s<kth<=l, kth>l),扣掉一些值(每个kth分支中的if语句),从而进一步转成 “找两个等长的数组的上中位数”的问题(见getUpMedian函数)。
调用getUpMedian函数的前提是end1-start1=end2-start2
,也就是两个数组等长.
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m=nums1.length,n=nums2.length;
if(((m+n)&1)==1){
return findKth(nums1,nums2,(m+n)/2+1);
}else{
return (findKth(nums1,nums2,(m+n)/2)+findKth(nums1,nums2,(m+n)/2+1))/2.0;
}
}
public double findKth(int[] nums1,int[] nums2,int kth){// kth \in {1,2....s+l}
int[] shorts=nums1.length<nums2.length?nums1:nums2;
int[] longs=shorts==nums1?nums2:nums1;
int s=shorts.length;
int l=longs.length;
if(s==0) return longs[kth-1];
if(kth<=s){// case 1: kth is too small
return getUpMedian(nums1,0,kth-1,nums2,0,kth-1);
}
// case 2: kth is too large,所以要排除掉longs太首段的元素(即便是shorts全部在前面垫上,也不可能让这些元素有可能成为第kth个),也排除掉shorts中太首端的元素(即便是longs全部在前面垫上,也不可能让这些元素有可能成为第kth个)
{// 注意:这里之所以要人工地(用if语句)扣掉两个值shorts[kth-l-1]和longs[kth-s-1],是因为,这样的话才能保证getUpMedian(shorts,kth-l,s-1,longs,kth-s,l-1)返回的中位数恰好是 合并nums1&nums2后第kth个值,也就是我们想要的结果
// 具体地,扣掉两个值之后,从arr1确定下的值有 kth-l个, 从arr2确定下的值有kth-s个,getUpMedian(shorts,kth-l,s-1,longs,kth-s,l-1)返回确定下s-kth+l个,所以一共确定下kth个值,这正是我们想要的
// 然而,如果不扣掉两个值,从arr1确定下的值有 kth-l-1个, 从arr2确定下的值有kth-s-1个,getUpMedian(shorts,kth-l-1,s-1,longs,kth-s-1,l-1)返回确定下s-kth+l个,所以一共确定下kth-2个值,这不是我们想要的
if(kth>l){
// s=10, l=23, kth=37
//drop lonogs[0...kth-s-2], left part: longs[kth-s-1....l-1]
//drop shorts[0...kth-l-2], left part: shortts[kth-l-1...s-1]
// kth-l-1 + s-1+1-kth+1+l =
if(longs[kth-s-1]>=shorts[s-1]){
return longs[kth-s-1];
}
if(shorts[kth-l-1]>=longs[l-1]){
return shorts[kth-l-1];
}
return getUpMedian(shorts,kth-l,s-1,longs,kth-s,l-1);
}
//case 3: kth适中,此时shorts中的所有元素都有可能做“合并后的第kth小”,而longs中位于太首段的、太尾端的元素都没有希望,因此要排除
// s<kth<=l, kth=17, drop kth-s
// shorts[0...s-1], longs[kth-s...kth-1]
if(longs[kth-s-1]>=shorts[s-1]){
return longs[kth-s-1];
}
return getUpMedian(shorts,0,s-1,longs,kth-s,kth-1);
}
public int getUpMedian(int[] nums1,int start1,int end1,int[] nums2,int start2,int end2){
// if(start1==end1){
// return Math.min(nums1[start1],nums2[start2]);
// }
int mid1=0,mid2=0;
while(start1<end1){
mid1=(start1+end1)/2;
mid2=(start2+end2)/2;
int offset=((end1-start1+1)&1)^1;
// 1 2 3 // 1 2 3 4
if(nums1[mid1]==nums2[mid2]) return nums1[mid1];
if(nums1[mid1]>nums2[mid2]){
// 1 [2 3], if offset=0
// mid
// 1 2 [3 4]
// mid
// 上面分别举出偶数、奇数的情况,
// when offset=0, 可以看到奇数情况时,重启区间(用[]括住的那部分)is [mid,end]
// when offset=1,that means 'even' case, and its restart-interval is [mid+1,end]
// summary, restart-interval is [mid+offset,end]
end1=mid1;
start2=mid2+offset;
}else{
end2=mid2;
start1=mid1+offset;
}
}
return Math.min(nums1[start1],nums2[start2]);
}