LeetCode(4): Median of Two Sorted Arrays
【题目】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)).
【中文描述】两个已排序数组(默认为从小到大排序)nums1 和 nums2, 大小分别为m,n。 要求找到这两个数组合并后的中位数。总的运行时间复杂度不能超过O(log(m+n))。
——————————————————————————————————————————————————————————————————————————————————————
【初始思路】
实际上一开始并没有认真看题目,心想这个很简单嘛,先merge,然后找中位数,分分钟kill掉。结果做了一半的时候才看见时间要求。显然,merge数组的最优时间复杂度是O(K) (K为M和N中小者+两者只差)。 远远大于题目要求O(log(m+n))。
【灵光一现!】
一看到log的复杂度要求,应该立马想到二分法,这是一个CSer基本的素养。
【中位数 / Median】
一个奇数长度数组中最中间的那个数。如果数组长度为偶数,那就是最中间2个数的平均数。
【重整思路】
如何二分?既然是找中位数,必然与数组中各数的位置(index)有关,我们可以借助二分法不断缩小index的范围,最后在极小范围内找到中位数。
想要更加通俗易懂的解释?以下用奇数长度数组作解释,偶数可类比:
(1) M+N个元素的中位数,位于两数组merge后的(M+N)/2+1位置。我们把这个位置记为K。又由于merge后数组中找位置K的元素,等同于找第K小元素:find the Kth minimum element!
(2) 我们对两个数组各自二分,取一半,弃一半,在剩下的元素中继续找?但是注意,这里的一半不能取数组长度的一半。 因为我们现在要找Kth,所以二分的实际尺度应该取决于K的大小。在去掉K/2后,由于前K个元素中的K/2已经被舍弃,所以在剩余数组中查找:Jth = K - K/2。(倘若取了数组的一半,那么弃掉的元素个数和K无关,那就无法确定 J 的位置。)
(3) 同时,应当注意,由于已经去掉了某个数组的前k/2个元素,那么这个数组参与查找J的范围起始点应当从当前start点往前移动k/2个位置,所以,start = start + k/2。
举个笼统的例子,要求找第8小元素,我们先找第4小,确定了前面4小后,把前4小舍弃,然后从第5小位置起,找第8小其实变成了找当前的第4小(5->END)。继续二分,找第2小,然后把前2小舍弃,实际变成从第3小位置找第2小(7->END)。最后找第1小,得到总的第7小,那么第8小也就变成了从8->END子数组的第1小,可轻松得到。
(4) 每次在两个数组中找前K/2元素,有如下一些情况需要考虑:
(4.1) 如果在数组A中找K/2的时候,K/2 >= A.length,此时可以直接丢弃B中前K/2,为什么?
反证:当K/2 >= A.length时,而我们要找的K就在B的前K/2元素中。我们假设 k 所在的数组下标记为p,那么B中含有的属于merge后数组前K个元素的元素有p+1个(请自行考虑为何)。显然,A中必然含有另外 k-(p+1)个元素。由此,得到如下不等式:
· p <= k/2 - 1 (kth 实际所处位置为p,在B的前k/2个元素里。-1是因为现在算的是数组下标,从0开始)
· 所以,p + 1 <= k/2;
· 所以,k - (p+1) >= k - k/2。
显然,A.length >= k - (p+1) >= k/2 ,这与上面的假设,k/2 >= A.length是矛盾的。
得证,且反之亦然。
(4.2) 如果4.1情况未出现,也即我们找到了在A/B中各自的前k/2个元素,我们记为其最后一个元素为A[mid]和B[mid],显然mid=k/2 - 1(因为mid为位置,而k/2是个数,所以需-1)。那么剩下的问题就是确定下一步该如何舍弃元素缩小下一步检索范围。有如下结论:
如果A[mid]>=B[mid],那么B的前k/2个元素可以直接舍弃。如果A[mid]<B[mid],那么A的前k/2个元素可以直接舍弃。
反证:当A[mid] >= B[mid],并且第k元素在B的前 k/2 个元素里,假设其值为maxK, 位置在p, 说明B中包含前k个元素中的 p + 1个元素,且p <= k/2 - 1。那么A中必然包含前k个元素中的 k - ( p + 1)个元素。有如下不等式:
· p < = k/2 -1 可得 p + 1 <= k/2
· 所以, k - (P + 1) >= k/2
· 也即,在A中,实际属于全部数组中前k个元素的元素个数为k - (P + 1),假设某元素数组下标为A[k-(p+1) - 1],那么它必然大于A[mid]。而显然,由于A[k - (p+1) -1] < maxk,所以,得到 A[mid] < maxk。 而由于maxk在B的前k/2个元素里,所以B[mid] > maxk。 得到:A[mid] < B[mid],与题设矛盾。得证!
也可以看下面图帮助理解这个证明过程:
(4.3) 此外,基准条件需要考虑。由于每一步查找第J小得时候,J = k - k/2,那么J=1的时候,程序如何处理。显然,根据定义来看,J=1说明只需要在A/B两个数组中找第1小即可。那实际就是直接返回当前位置元素即可。但是,A/B各存在一个起始当前元素,如何取舍?可以这么考虑,merge后的数组是从小到大的,从上面分析来看,我们实际上要找的是第k小。也就是说,如果在前面各步,我们已经丢弃了全部 (k-1) 个元素,剩下要选的元素就是第k小。很显然,应该选两者中小的那个,大的那个元素在merge后实际是第k+1小。所以:
return Math.min(A[starta], B[startb]);
【Show me the Code!!!】
有了上面的分析,我们可以实现主程序:
1 public static double findMedianSortedArrays(int[] nums1, int[] nums2) { 2 int lengthA = nums1.length; 3 int lengthB = nums2.length; 4 int len = lengthA + lengthB; 5 if(len % 2 == 0) { 6 return (findKth(nums1, nums2, 0, 0, len/2) + findKth(nums1, nums2, 0, 0, len/2+1))/ 2.0; 7 } else { 8 return findKth(nums1, nums2, 0, 0, len/2 + 1); 9 } 10 }
主程序实际调用了方法 findKth(int[] nums1, int[]nums2, int starta, int startb, int k),它主要负责在两个数组中找第k个元素,并且弃掉无用元素,并前移下一次查询的起始位置。具体代码如下:
1 public static double findKth(int[] nums1, int[]nums2, int starta, int startb, int k){ 2 int len1 = nums1.length; 3 int len2 = nums2.length; 4 5 if(starta >= len1){ 6 //If the start point is beyond the length of this array, then this array will be eliminated at once. 7 //Because, starta moves when we 'cut' elements off array nums1. When the starta is larger than the len1. 8 //That means it is not necessary need nums1 any more. 9 //Same with the nums2. 10 return nums2[startb + k - 1];//index starts from 0 11 } 12 if(startb >= len2){ 13 return nums1[starta + k - 1];//index starts from 0 14 } 15 if(k == 1){ 16 //This block means: 17 //When we need to find the 1th min element in two Array 18 //We just return the first element which is the element pointed by the 'start index' in each Array and compare them. 19 //The reason for we picking the smaller one is when the two arrays merged, the smaller one will stand in front of the bigger one; 20 //The smaller one will be the Kth, and the bigger one actually become the (k+1)th element in the merge Array. 21 return Math.min(nums1[starta], nums2[startb]); 22 23 } 24 25 int mid = k/2 - 1;//index starts from 0, so the mid of K is K/2 - 1 26 int keypoint1 = starta + mid >= len1? Integer.MAX_VALUE : nums1[starta + mid];//keypoint1 is the k/2 one of nums1 27 int keypoint2 = startb + mid >= len2? Integer.MAX_VALUE : nums2[startb + mid];//keypoint2 is the k/2 one of nums2 28 29 if(keypoint1 > keypoint2){ 30 //When we cut off some elements from one array, the 'start' index moves forward by [start + k/2] 31 //k-k2 means that we have eliminated K/2 elements, so k-k/2 elements left 32 return findKth(nums1, nums2, starta, startb + k/2, k - k/2); 33 } else { 34 return findKth(nums1, nums2, starta + k/2, startb, k - k/2); 35 } 36 }
【O分析】
空间复杂度很显然是O(1),着重来看看时间复杂度:
由于 中位数 实际是两个数组merge后的最中间那个数,所以k = ( m+n )/2
我们只需要考虑findKth方法总共跑了多少次,就可以知道O是多少了。
由于每一次跑findKth,我们都实际上把k缩小了一半。那么总执行次数为N,那么总处理元素个数为:2N = k。又由于 k = (m+n)/2。
显然,N = log2 ((m+n)/2) = O(log2(m+n))。 满足题目要求。