[LeetCode] 4. 两个排序数组的中位数

该题的难度分级是Hard,那么难在哪里呢?我们先来看题目。

给定两个大小为 m 和 n 的有序数组 nums1 和 nums2 

请找出这两个有序数组的中位数。要求算法的时间复杂度为 O(log (m+n)) 。

示例 1:

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

中位数是 2.0

示例 2:

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

中位数是 (2 + 3)/2 = 2.5

当你看到要求的时间复杂度为O(log (m+n)),你想到了什么?没错,二分法。

从示例上观察来看,如果两个数组的长度和是奇数,中位数就是大小在正中间的那个数;如果两个数组的长度和是偶数,那么中位数就是大小在正中间的两个数的和的平均数。

似乎我们需要分类讨论,并且考虑一些corner case才能完整的把这题解出来,那么有没有更加巧妙的方法呢?没错,我都这么说了,肯定是有的。

下面的解法是官网社区讨论组里面一个大神的解法,我来搬运过来翻译一下,帮助我自己理解,也帮助有缘人解惑。

原post:https://leetcode.com/problems/median-of-two-sorted-arrays/discuss/2471/Very-concise-O(log(min(MN)))-iterative-solution-with-detailed-explanation

话不多说,让我们开始看大神如何装逼。

这个解法需要一种思维转换,我们能不能把长度和是奇数和长度和是偶数这两种情况看作是同一种情况呢?Yes!

首先,我们来看一下中位数的概念:

如果我们将一个有序数组切一刀,分成2个等长的部分,那么中位数就是前半部分的最大值和后半部分的最小值,这两者和的平均数。

没明白?没关系,举个例子:对于[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切成了2份,前半部分数组和后半部分数组都包含了4。那么这时候中位数是多少?median = (4 + 4) / 2 = 4; 答案依旧正确。

好,现在为了方便,我们用L来表示切的那一刀左边第一个元素,R来表示切的那一刀右边的第一个元素。

也就是说,对于被切了一刀的数组 [2 3 / 5 7], 这个情况下,L = 3, R = 5。 

于是我们可以观察到,对于一个长度为N的有序数组,L和R的数组下标有如下规律:

N(数组长度)     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

不难发现,INDEXL = (N-1)/2 而 INDEXR = N/2. 那么根据前面的分析,对于任意一个数组A, 其中位数median = (L + R)/2 = (A[(N-1)/2] + A[N/2])/2 。

往下看之前,请先确保以上内容你已经完全ok了解no破布。


好,我们继续。现在我们来讨论两个数组情况,我们需要在数组中假想一些#(井号)出来,这些#把每一个数组中的数字都包裹了起来,并且,无论是“#”还是数字我们都称作是一个position。

[6 9 13 18]  ->   [# 6 # 9 # 13 # 18 #] (N = 4) position index 0 1 2 3 4 5 6 7 8 (N_Position = 9) 共有9个position [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) 共有11个position 

可以看到,无论N是多少,一定会有 2*N+1 个 'positions' . 因此,无论N是奇数还是偶数,从position的角度来看,假设第一个position下标为0,那么要在正中间切一刀,一定是在第N个position(这里称为CutPosition)。

由于我们已经得出在一个数组中 index(L) = (N-1)/2, index(R) = N/2 , 所以我们进一步得出 index(L) = (CutPosition-1)/2, index(R) = (CutPosition)/2

同样的,往下看之前,请先确保以上内容你已经完全ok了解no破布。


好,我们继续,对于2个数组的情况,

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

与单个数组的问题类似,我们要找到一个切法(就是在两个数组上各切一刀),可以将两个数组分别分成两个部分,使得

“两个左半部分中包含的任意数字” <= “两个右半部分中包含的任意数字”

我们可以观察得出,对于这两个数组(长度分别为N1和N2):

  1. 总共有 (2N1+1)+(2N2+1) =  2N1 + 2N2 + 2 个position. 因此切完之后,左半边和右半边应该各包含 N1 + N2 个 positions,每一刀的左右各有一个position

  2. 在必须满足第1条的原则下,假设我们在数组A2的position下标 C2 的地方切一刀, 那么A1的切分position下标就必须是 C1 = N1 + N2 - C2. 来举个例子, 如果 C2 = 2, 那么就一定有 C1 = 4 + 5 - C2 = 7.

                  A1  [# 1 # 2 # 3 # (4/4) # 5 #] N1 = 5 (切在数字4上,所以用(4/4)表示)
    position index 0 1 2 3 4 5 6 7 8 9 10 N_Positions = 11
     A2 [# 1 / 1 # 1 # 1 #] N2 = 4 (由于切在#处,就用/代替#)
    position index 0 1 2 3 4 5 6 7 8 N_Positions = 9
     
  3. 切完之后我们会得到2个L和2个R,分别是:

     L1 = A1[(C1-1)/2]; R1 = A1[C1/2];
     L2 = A2[(C2-1)/2]; R2 = A2[C2/2]; 

         代入到上面的例子里,L1和L2就是,

    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 and L2 <= R2 是一定可以满足的,我们只需要确保:

L1 <= R2 && L2 <= R1.

现在我们终于可以使用简单的二分搜索来找到合适的切法了:(这里逻辑很关键,暂时保留原文)

If (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);
// 意味着A1的左半边的大数字太多了,需要将切分position C1左移
} If (L2 > R1){
// then there are too many large numbers on the left half of A2, and we must move C2 to the left.
// 意味着A2的左半边的大数字太多了,需要将切分position C2左移
} Otherwise, this cut is the right one.
否则就满足了条件,是正确的切分,通过计算 (max(L1,L2) + min(R1,R2)) / 2 得出中位数 After we find the cut, the medium can be computed as (max(L1, L2) + min(R1, R2)) / 2;

两个要注意的地方:

A. 由于C1和C2存在互相依赖的关系(也就是,确定了C1就确定了C2,反之亦然), 我们可以先移动其中的一个,另一个随之移动。(没电了。。先这样,明天继续完成)we can just move one of them first, then calculate the other accordingly. However, it is much more practical to move C2 (the one on the shorter array) first. The reason is that on the shorter array, all positions are possible cut locations for median, but on the longer array, the positions that are too far left or right are simply impossible for a legitimate cut. For instance, [1], [2 3 4 5 6 7 8]. Clearly the cut between 2 and 3 is impossible, because the shorter array does not have that many elements to balance out the [3 4 5 6 7 8] part if you make the cut this way. Therefore, for the longer array to be used as the basis for the first cut, a range check must be performed. It would be just easier to do it on the shorter array, which requires no checks whatsoever. Also, moving only on the shorter array gives a run-time complexity of O(log(min(N1, N2))) (edited as suggested by @baselRus)

B. The only edge case is when a cut falls on the 0th(first) or the 2Nth(last) position. For instance, if C2 = 2N2, then R2 = A2[2*N2/2] = A2[N2], which exceeds the boundary of the array. To solve this problem, we can imagine that both A1 and A2 actually have two extra elements, INT_MAX at A[-1] and INT_MAX at A[N]. These additions don't change the result, but make the implementation easier: If any L falls out of the left boundary of the array, then L = INT_MIN, and if any R falls out of the right boundary, then R = INT_MAX.

ok,到此结束,源代码就不贴了,看明白之后用自己熟悉的语言写一下试试呗,实在不行再去原文链接看看,大神是怎么写的。

Good Luck!

猜你喜欢

转载自www.cnblogs.com/windyair/p/9102740.html