LeetCode ! !! 4. Median of Two Sorted Arrays

文章介绍了如何在O(log(m+n))的时间复杂度内找到两个已排序数组的中位数,提供了两种不同的思路:一种类似于合并两个有序链表,用双指针法;另一种通过递归函数,逐步缩小查找范围。这两种方法都需要处理奇数和偶数长度的情况,并确保正确计算中位数。
摘要由CSDN通过智能技术生成

参考资料: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]);
     }
可以使用二分查找算法来解决这个问题。 首先,我们可以将两个数组合并成一个有序数组,然后求出中位数。但是,这个方法的时间复杂度为 $O(m + n)$,不符合题目要求。因此,我们需要寻找一种更快的方法。 我们可以使用二分查找算法在两个数组中分别找到一个位置,使得这个位置将两个数组分成的左右两部分的元素个数之和相等,或者两部分的元素个数之差不超过 1。这个位置就是中位数所在的位置。 具体来说,我们分别在两个数组中二分查找,假设现在在第一个数组中找到了一个位置 $i$,那么在第二个数组中对应的位置就是 $(m + n + 1) / 2 - i$。如果 $i$ 左边的元素个数加上 $(m + n + 1) / 2 - i$ 左边的元素个数等于 $m$ 个,或者 $i$ 左边的元素个数加上 $(m + n + 1) / 2 - i$ 左边的元素个数等于 $m + 1$ 个,则这个位置就是中位数所在的位置。 具体的实现可以参考以下 Java 代码: ```java public double findMedianSortedArrays(int[] nums1, int[] nums2) { int m = nums1.length, n = nums2.length; if (m > n) { // 保证第一个数组不大于第二个数组 int[] tmp = nums1; nums1 = nums2; nums2 = tmp; int t = m; m = n; n = t; } int imin = 0, imax = m, halfLen = (m + n + 1) / 2; while (imin <= imax) { int i = (imin + imax) / 2; int j = halfLen - i; if (i < imax && nums2[j - 1] > nums1[i]) { imin = i + 1; // i 太小了,增大 i } else if (i > imin && nums1[i - 1] > nums2[j]) { imax = i - 1; // i 太大了,减小 i } else { // i 是合适的位置 int maxLeft = 0; if (i == 0) { // nums1 的左边没有元素 maxLeft = nums2[j - 1]; } else if (j == 0) { // nums2 的左边没有元素 maxLeft = nums1[i - 1]; } else { maxLeft = Math.max(nums1[i - 1], nums2[j - 1]); } if ((m + n) % 2 == 1) { // 总元素个数是奇数 return maxLeft; } int minRight = 0; if (i == m) { // nums1 的右边没有元素 minRight = nums2[j]; } else if (j == n) { // nums2 的右边没有元素 minRight = nums1[i]; } else { minRight = Math.min(nums1[i], nums2[j]); } return (maxLeft + minRight) / 2.0; } } return 0.0; } ``` 时间复杂度为 $O(\log\min(m, n))$。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值