[LeetCode] 4. Median of Two Sorted Arrays ☆☆☆☆☆
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
与一个数组的中位数问题一样,我们需要对两个数组进行切割,使得左侧的所有数字 < 右侧的所有数字。
可以注意到:
- 总共有(2N1 + 2N2 + 2)个位置。因此切割点的左右两侧应该分别有(N1 + N2)个位置,切割点本身占了两个位置。
- 因此,假设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 #]
- 切割之后,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