-->

[LeetCode] 4. Median of Two Sorted Arrays ☆☆☆

2020-12-05 13:58发布

 

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)).

Example 1:

nums1 = [1, 3]
nums2 = [2]

The median is 2.0

Example 2:

nums1 = [1, 2]
nums2 = [3, 4]

The median is (2 + 3)/2 = 2.5

 

解法1: 将问题转化为经典的“求两个有序数组中的第k小值”问题,即:

  首先假设有序数组A和B的长度都大于k/2(下取整),比较A[k/2-1]和B[k/2-1]两个元素,这两个元素分别表示A和B中的第k/2小的元素。这两个元素比较共有三种情况:>、<和=。

  (1)如果A[k/2-1]<B[k/2-1],说明A[k/2-1]及其之前的元素肯定都在A和B所有元素的前(k-1)小元素中,也就是说,A[k/2-1]不可能大于两个数组所有元素的第k小值。因此,将A的前(k/2)个元素删去之后(设剩余部分为A'),进一步求A'和B中的第(k-k/2)大的数。

  (2)如果A[k/2-1]<B[k/2-1],同理。

  (3)如果A[k/2-1]=B[k/2-1],如果k为偶数,则A[k/2-1]和B[k/2-1]即为第k小数,因为两个数组中分别有(k/2-1)个元素小于该值。但考虑到k可能不为偶数,可将其中一个的前(k/2)个元素删去之后,求剩余部分的第(k-k/2)大的数。

  需要注意的是,如果A的长度m<k/2,应取A[m-1]与B[k/2-1](或B[k-m-1])比较;B的长度小于k/2时同理。

  通过上面的分析,我们即可以采用递归的方式实现寻找第k小的数。此外我们还需要考虑几个边界条件: 

    • 如果A或者B为空,则直接返回B[k-1]或者A[k-1];
    • 如果k为1,我们只需要返回A[0]和B[0]中的较小值。
public class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int len1 = nums1.length;
        int len2 = nums2.length;
        int left = (len1 + len2 + 1) / 2;
        int right = (len1 + len2 + 2) / 2;
        return (findKthSmallest(nums1, nums2, left) + findKthSmallest(nums1, nums2, right)) / 2.0;
    }
    
    // 将问题转化为求两个数组的第k小数
    public int findKthSmallest(int[] nums1, int[] nums2, int k) {
        int len1 = nums1.length;
        int len2 = nums2.length;
        int half = k / 2;
        
        // 如果有其中一个长度为零,直接返回另一个的第k小数
        if (len1 == 0)  return nums2[k - 1];
        if (len2 == 0)  return nums1[k - 1];
        // 如果k为1,则直接返回两个数组的最小数
        if (k == 1)  return Math.min(nums1[0], nums2[0]);
        
        // 判断half是否超过了数组长度
        int cutPoint1 = Math.min(len1, half);
        int cutPoint2 = Math.min(len2, half);
        
        // 判断两个数组切割点处的值大小,将小的数组从切割点处截去
        if (nums1[cutPoint1 - 1] < nums2[cutPoint2 - 1]) {
            return findKthSmallest(Arrays.copyOfRange(nums1, cutPoint1, len1), nums2, k - cutPoint1);
        } else {
            return findKthSmallest(nums1, Arrays.copyOfRange(nums2, cutPoint2, len2), k - cutPoint2);
        }
    }
}

 

解法2:

-------------------- 准备工作 --------------------

  对于长为N的数组A来说,用L和R分别表示中位数切割点的值(N为奇数)或者左右两侧的值(N为偶数),则L=(N-1)/2, R=N/2, 所以中位数可以表示为: (L+R) / 2 = (A[(N-1)/2] + A[N/2]) / 2。

 

  在数组的每两个数字之间添加“虚拟位置”(用#表示),同时把数字也当成“位置”,如:

[6 9 13 18]  ->   [# 6 # 9 # 13 # 18 #]    (N = 4)
position index     0 1 2 3 4 5  6 7  8     (N_Position = 9)
		  
[6 9 11 13 18]->   [# 6 # 9 # 11 # 13 # 18 #]   (N = 5)
position index      0 1 2 3 4 5  6 7  8 9 10    (N_Position = 11)

  可以看出,对于长度为N的数组,总共有(2*N+1)个位置,无论N为奇数还是偶数,而中位数切割点的位置也总是第N个(下标从0开始)。

 

-------------------- 算法原理 --------------------

  设有序数组A1和A2,A1的长度>A2的长度:

A1: [# 1 # 2 # 3 # 4 # 5 #]    (N1 = 5, N1_positions = 11)
pos: 0 1 2 3 4 5 6 7 8 9 10 A2: [# 1 # 1 # 1 # 1 #] (N2 = 4, N2_positions = 9)
pos: 0 1 2 3 4 5 6 7 8

  与一个数组的中位数问题一样,我们需要对两个数组进行切割,使得左侧的所有数字 < 右侧的所有数字。

  可以注意到:

  1. 总共有(2N1 + 2N2 + 2)个位置。因此切割点的左右两侧应该分别有(N1 + N2)个位置,切割点本身占了两个位置。
  2. 因此,假设A2的切割点 C2 = k,那么A1的切割点位置必为 C1 = N1 + N2 -k。例如:如果 C2 = 2,则 C1 = 4 + 5 - C2 = 7。
     [# 1 # 2 # 3 # (4/4) # 5 #]    
    
     [# 1 / 1 # 1 # 1 #]  
  3. 切割之后,A1切割为L1+R1,A2切割为L2+R2,即
     L1 = A1[(C1-1)/2]; R1 = A1[C1/2];
     L2 = A2[(C2-1)/2]; R2 = A2[C2/2];

    在上述例子中,

        L1 = A1[(7-1)/2] = A1[3] = 4; R1 = A1[7/2] = A1[3] = 4;
        L2 = A2[(2-1)/2] = A2[0] = 1; R2 = A1[2/2] = A1[1] = 1;

  现在如何确定切割点是不是我们想要的?由于L1和L2是左侧的两个最大值,而R1和R2是右侧的两个最小值,我们只需满足:

L1 <= R1  &&  L1 <= R2  &&  L2 <= R1  &&  L2 <= R2

从而保证左侧的数字 <= 右侧的数字。由于A1和A2是有序的,即满足L1 <= R1 和 L2 <= R2,因此只需满足

L1 <= R2 和 L2 <= R1即可。

  现在我们可以应用二分法进行查找:

  • 如果 L1 > R2,说明A1左侧有较多大数,因此必须将C1左移(C2右移);
  • 如果 L2 > R1,说明A2左侧有较多大数,因此必须将C2左移(C1右移);
  • 除了以上两种情况,得到的切割点即为正确的,此时可以计算出中位数为 (max(L1, L2) + min(R1, R2)) / 2。

  需要注意的是:

  • 由于C1和C2可以由彼此推出,一般选择较短的A2采用二分法确定C2的位置之后,再计算C1的位置,因此时间复杂度为O(log(min(N1, N2)))。
  • 边界情况:边界情况出现在当切割点位于 0th 或者 2Nth 的时候。例如,当 C2 = 2N2 时,R2 = A2[2 * N2 / 2] = A2[N2] 将超出数组下标范围。为解决此问题,假设在数组两侧有两个额外的数,即 A[-1] = Integer.MIN_VALUE, A[N] = Integer.MAX_VALUE。
public class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int m = nums1.length;
        int n = nums2.length;
        if (m < n) return findMedianSortedArrays(nums2, nums1);
        if (n == 0)  return (nums1[(m - 1) / 2] + nums1[m / 2]) / 2.0;
        
        int left = 0;
        int right = 2 * n;
        while (left <= right) {
            int mid2 = (left + right) / 2;
            int mid1 = m + n - mid2;
            int L1 = mid1 == 0 ? Integer.MIN_VALUE : nums1[(mid1 - 1) / 2];
            int R1 = mid1 == m * 2 ? Integer.MAX_VALUE : nums1[mid1 / 2];
            int L2 = mid2 == 0 ? Integer.MIN_VALUE : nums2[(mid2 - 1) / 2];
            int R2 = mid2 == n * 2 ? Integer.MAX_VALUE : nums2[mid2 / 2];
            
            if (L1 > R2)
                left = mid2 + 1;
            else if (L2 > R1)
                right = mid2 - 1;
            else
                return (Math.max(L1, L2) + Math.min(R1, R2)) / 2.0;
        }
        return -1;
    }
}

 

ps: 如果没有时间复杂度为 O(og(m+n)) 的限制,也可以定义两个指针,分别从两个数组的头部开始,比较指向元素的大小,较小的指针往后移,然后再次比较。。。。若 (m+n) 为奇数,则移动 (m+n-1)/2 次后,指针指向的数为中位数;若(m+n) 为偶数,则移动 (m+n)/2-1 和 (m+n)/2 次后,指针分别指向的两个数的平均值为中位数。 

 

参考资料:

https://discuss.leetcode.com/topic/16797/very-concise-o-log-min-m-n-iterative-solution-with-detailed-explanation

标签: