4. Median of Two Sorted Arrays(两个有序数组的中位数)

有两个有序数组nums1和nums2分别为m和n。

找到两个有序数组的中位数。 总运行时间复杂度应为O(log(m + n))。

您可以假设nums1和nums2不能都为空。

例 1:

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

The median is 2.0

例2:

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

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

方法1:递归方法(Approach 1: Recursive Approach)

要解决这个问题,我们需要了解“中位数有什么用”。 在统计(statistics)中,中位数用于:

将一组分成两个相等长度的子集,一个子集总是大于另一个子集。

如果我们理解使用中位数来划分,我们非常接近答案。

首先让我们在随机位置 i i i 将A切成两部分:

      left_A             |        right_A
A[0], A[1], ..., A[i-1]  |  A[i], A[i+1], ..., A[m-1]

因为A有m个元素,因此总共有m+1中划分方法(i=0~m)。并且我们知道

len(left_A)=i,len(right_A)=m−i.
注意:当i=0时,left_A是空的,当i=m时,right_A是空的

同样,我们在随机位置 j j j 将B切成两部分:

          left_B             |        right_B
    B[0], B[1], ..., B[j-1]  |  B[j], B[j+1], ..., B[n-1]

将left_A和left_B放到一个集合里面去,将right_A和right_B放到另一个集合里面去,把它们记作left_part和right_part:

          left_part          |        right_part
    A[0], A[1], ..., A[i-1]  |  A[i], A[i+1], ..., A[m-1]
    B[0], B[1], ..., B[j-1]  |  B[j], B[j+1], ..., B[n-1]

如果我们可以确定:

  1. len(left_part) = len(right_part)
  2. max(left_part) <= min(right_part)

然后,我们将{A, B}里面的所有元素划分成长度相等的两部分,并且一部分的元素总是大于另一部分的元素,接着
在这里插入图片描述
为了满足这些条件,我们仅需要保证:
在这里插入图片描述
ps.1 为了简便,我假设A[i-1],B[j-1],A[i],B[j]都总是有效的,即使i=0,i=m,j=0,j=n,我将讨论如何处理这些边缘值。( edge values)
ps.2 为什么n>=m? 因为确保j是非负的,0<=i<=m和j=(m+n+1)/2 - i
如果n<m,j可能是复数,这可能会导致错误的结果。

因此,我们需要这样做:
在这里插入图片描述
我们可以按照下面描述的步骤进行二叉搜索(binary search):

  1. 设定 imin=0, imax=m, 然后开始在 [imin, imax] 搜索
  2. 设定 i = i m i n + i m a x 2 , j = m + n + 1 2 − i i=\frac{imin+imax}{2},j=\frac{m+n+1}{2}-i i=2imin+imax,j=2m+n+1i
  3. 现在我们有len(left_part) = len(right_part),并且,我们可能会遇到3种可能的情况:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    java
class Solution {
    public double findMedianSortedArrays(int[] A, int[] B) {
        int m = A.length;
        int n = B.length;
        if (m > n) { // to ensure m<=n
            int[] temp = A; A = B; B = temp;
            int tmp = m; m = n; n = tmp;
        }
        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 && B[j-1] > A[i]){
                iMin = i + 1; // i is too small
            }
            else if (i > iMin && A[i-1] > B[j]) {
                iMax = i - 1; // i is too big
            }
            else { // i is perfect
                int maxLeft = 0;
                if (i == 0) { maxLeft = B[j-1]; }
                else if (j == 0) { maxLeft = A[i-1]; }
                else { maxLeft = Math.max(A[i-1], B[j-1]); }
                if ( (m + n) % 2 == 1 ) { return maxLeft; }

                int minRight = 0;
                if (i == m) { minRight = B[j]; }
                else if (j == n) { minRight = A[i]; }
                else { minRight = Math.min(B[j], A[i]); }

                return (maxLeft + minRight) / 2.0;
            }
        }
        return 0.0;
    }
}

python

def median(A, B):
    m, n = len(A), len(B)
    if m > n:
        A, B, m, n = B, A, n, m
    if n == 0:
        raise ValueError

    imin, imax, half_len = 0, m, (m + n + 1) / 2
    while imin <= imax:
        i = (imin + imax) / 2
        j = half_len - i
        if i < m and B[j-1] > A[i]:
            # i is too small, must increase it
            imin = i + 1
        elif i > 0 and A[i-1] > B[j]:
            # i is too big, must decrease it
            imax = i - 1
        else:
            # i is perfect

            if i == 0: max_of_left = B[j-1]
            elif j == 0: max_of_left = A[i-1]
            else: max_of_left = max(A[i-1], B[j-1])

            if (m + n) % 2 == 1:
                return max_of_left

            if i == m: min_of_right = B[j]
            elif j == n: min_of_right = A[i]
            else: min_of_right = min(A[i], B[j])

            return (max_of_left + min_of_right) / 2.0

在这里插入图片描述

##############################分割线##############################

方法2

由于所有的极端情况(the corner cases),这个问题很难实现(notoriously hard)。 大多数实现将奇数长度(odd-lengthed)和偶数长度(even-lengthed)的数组视为两种不同的情况并单独处理它们(treat them separately)。 事实上,带着一点点小技巧。 这两种情况可以合并为一种,从而形成一种非常简单的解决方案,其中(几乎)不需要特殊处理。

首先,让我们以一种略微不同寻常的方式(slightly unconventional way)看待“MEDIAN”的概念(concept)。 那是:

如果将一个有序数组切分成长度相等的两部分,然后median(中位数)就是 Max(lower_half)和Min(upper_half)的平均值

例如,对于[2 3 5 7],我们在3到5之间进行切割:

[2 3 / 5 7]

中位数median=(3+5)/2。请注意,我将使用’/'表示切割,(数字/数字)表示通过本文中的数字进行切割。

对于[2 3 4 5 6],我们通过4进行切割,如下所示:

[2 3 (4/4) 5 7]

由于我们将4分成两半,我们现在说下部和上部子数组都包含4.这个想法也也可以得到正确的答案:(4 + 4)/ 2 = 4;

为方便起见,我们使用L表示切割后的数字,R表示右边的数字。 例如,在[2 3 5 7]中,我们分别有L = 3和R = 5。

我们观察到L和R的索引与数组N的长度有以下关系:

N        Index of L / R
1               0 / 0
2               0 / 1
3               1 / 1  
4               1 / 2      
5               2 / 2
6               2 / 3
7               3 / 3
8               3 / 4

不难得出index(L) =(N-1)/ 2,index®=N / 2。 因此,中位数可以表示为

median=(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个位置(从0开始 0-based)。 由于在这种情况下index(L)=(N-1)/ 2和index(R)= N / 2,我们可以推断(infer)index(L)=(CutPosition-1)/ 2index(R)=(CutPosition))/ 2

现在针对双两个数组情况:

A1: [# 1 # 2 # 3 # 4 # 5 #]    (N1 = 5, N1_positions = 11)

A2: [# 1 # 1 # 1 # 1 #]     (N2 = 4, N2_positions = 9)

类似于一个数组(one-array)问题,我们需要找到一个将两个数组分成两半的分割

左半部分的任何元素 <= 右半部分的任何元素

我们也可以做如下观察:

  1. 共有2N1 + 2N2 + 2个位置。 因此,在切口的每一侧必须有正好N1 + N2位置,并且有切口2个位置。
  2. 因此,当我们在A2的C2位置分割时,A1的分割位置一定是C1 = N1 + N2 - k。举个例子,如果C2 = 2, 然后我们一定有 C1 = 4 + 5 - C2 = 7
 [# 1 # 2 # 3 # (4/4) # 5 #]    

 [# 1 / 1 # 1 # 1 #]  
  1. 进行分割后, 我们得到两个左边部分和两个右边部分. 他们是
 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

确保下半部分中的任何数<=上半部分中的任何数。 事实上,由于L1 <= R1和L2 <= R2自然得到保证,因为A1和A2是升序的,我们只需要确保:L1 <= R2和L2 <= R1。

现在我们可以使用简单的二叉搜索来找出结果。

If we have L1 > R2, it means there are too many large numbers on the left half of A1, then we must move C1 to the left (i.e. move C2 to the right); 
If L2 > R1, then there are too many large numbers on the left half of A2, and we must move C2 to the left.
Otherwise, this cut is the right one. 
After we find the cut, the medium can be computed as (max(L1, L2) + min(R1, R2)) / 2;

如果L1 > R2,意味着A1的左边部分有很多较大的数,然后我们把A1的分割位置C1左移
如果L2 > R1,意味着A2的左边部分有很多较大的数,然后我们把A2的分割位置C2左移
否则,这个分割位置就是我们要找的分割位置,然后中位数就是median=(max(L1, L2) + min(R1, R2)) / 2;

两个旁注:
在这里插入图片描述

double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
    int N1 = nums1.size();
    int N2 = nums2.size();
    if (N1 < N2) return findMedianSortedArrays(nums2, nums1);	// Make sure A2 is the shorter one.
    
    int lo = 0, hi = N2 * 2;
    while (lo <= hi) {
        int mid2 = (lo + hi) / 2;   // Try Cut 2 
        int mid1 = N1 + N2 - mid2;  // Calculate Cut 1 accordingly
        
        double L1 = (mid1 == 0) ? INT_MIN : nums1[(mid1-1)/2];	// Get L1, R1, L2, R2 respectively
        double L2 = (mid2 == 0) ? INT_MIN : nums2[(mid2-1)/2];
        double R1 = (mid1 == N1 * 2) ? INT_MAX : nums1[(mid1)/2];
        double R2 = (mid2 == N2 * 2) ? INT_MAX : nums2[(mid2)/2];
        
        if (L1 > R2) lo = mid2 + 1;		// A1's lower half is too big; need to move C1 left (C2 right)
        else if (L2 > R1) hi = mid2 - 1;	// A2's lower half too big; need to move C2 left.
        else return (max(L1,L2) + min(R1, R2)) / 2;	// Otherwise, that's the right cut.
    }
    return -1;
} 
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值