给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。 请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。 你可以假设 nums1 和 nums2 不会同时为空。
示例 1:
nums1 = [1, 3] nums2 = [2]
则中位数是 2.0
示例 2:
nums1 = [1, 2] nums2 = [3, 4]
则中位数是 (2 + 3)/2 = 2.5
例如,对于[2 3 5 7],我们在3到5之间进行切割:
[2 3/5 7]
那么中位数=(3 + 5)/ 2。
请注意,我将使用'/'表示剪切,(数字/数字)表示通过本文中的数字进行剪切。
对于[2 3 4 5 6],
我们像这样直接通过:[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
不难得出关于L与R的索引: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个位置(从0开始)。 由于在这种情况下索引(L)=(N-1)/ 2和索引(R)= N / 2,我们可以推断索引(L)=(CutPosition-1)/ 2,索引(R)=(CutPosition))/ 2。
现在对于双阵列情况:
A1: [# 1 # 2 # 3 # 4 # 5 #] (N1 = 5, N1_positions = 11)
A2: [# 1 # 1 # 1 # 1 #] (N2 = 4, N2_positions = 9)
与单阵列问题类似,我们需要找到一个将两个阵列分成两半的切割,
使之满足“左半部分中的任何数字“<=”右边两个中的任何数字半”。
我们还可以做出以下观察:
1.总共有2N1 + 2N2 + 2个位置。 因此,在切口的每一侧必须有正好N1 + N2位置,并且在切口上直接有2个位置。
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.切割时,我们有两个L's和两个R. 他们是
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。现在我们可以使用简单的二进制搜索来找出结果。
如果我们有L1> R2,这意味着A1的左半部分有太多的大数字,那么我们必须向左移动C1(即向右移动C2);如果L2> R1,那么A2的左半部分有太多的大数字,我们必须向左移动C2。否则,这次切割是正确的。在找到切口后,切口可以计算为(max(L1,L2)+ min(R1,R2))/ 2;
两个旁注:
A.由于C1和C2可以相互确定,我们可以先移动其中一个,然后相应地计算另一个。但是,首先移动C2(较短阵列上的那个)更为实际。原因是在较短的阵列上,所有位置都可能是中位数的切割位置,但是在较长的阵列上,左侧或右侧太远的位置对于合法切割来说根本不可能。例如,[1],[2 3 4 5 6 7 8]。显然,2到3之间的切割是不可能的,因为较短的阵列没有那么多元素来平衡[3 4 5 6 7 8]部分,如果你这样切割。因此,对于要用作第一次切割的基础的较长阵列,必须执行范围检查。在较短的阵列上进行操作会更容易,不需要任何检查。此外,仅在较短的阵列上移动会产生O的运行时复杂度(log(min(N1,N2)))
B.唯一的边缘情况是切割落在第0(第一)或第2N(最后)位置。例如,如果C2 = 2N2,则R2 = A2 [2 * N2 / 2] = A2 [N2],其超过阵列的边界。为了解决这个问题,我们可以想象A1和A2实际上都有两个额外的元素,A_-的INT_MAX和A [N]的INT_MAX。这些添加不会改变结果,但会使实现更容易:如果任何L落在数组的左边界之外,则L = INT_MIN,如果任何R落在右边界之外,则R = INT_MAX。
代码实现:
public class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length, 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, right = 2 * n;
while (left <= right) {
int mid2 = (left + right) / 2;
int mid1 = m + n - mid2;
double L1 = mid1 == 0 ? Double.MIN_VALUE : nums1[(mid1 - 1) / 2];
double L2 = mid2 == 0 ? Double.MIN_VALUE : nums2[(mid2 - 1) / 2];
double R1 = mid1 == m * 2 ? Double.MAX_VALUE : nums1[mid1 / 2];
double R2 = mid2 == n * 2 ? Double.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;
}
return -1;
}
}
参考资料:链接一:LeetCode
链接二:LeetBook