徒手挖地球十五週目
NO.4 尋找兩個有序數組的中位數 困難
思路一:暴力法 直接合並兩個有序數組,然後根據奇偶性找到中位數。但是這種笨辦法不能滿足時間複雜度的要求。
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int[] num=new int[nums1.length+nums2.length];
int count=0,i=0,j=0;
//合併兩個有序數組
while (count<(nums1.length+nums2.length)){
if (i<nums1.length&&j<nums2.length){
if (nums1[i]<nums2[j]){
num[count++]=nums1[i++];
}else {
num[count++]=nums2[j++];
}
}else if (i<nums1.length){
num[count++]=nums1[i++];
}else if (j<nums2.length){
num[count++]=nums2[j++];
}
}
//判斷合併後的數組元素個數的奇偶性
if (count%2==0){
//注意這裏是2.0,如果是2會導致結果爲int類型丟失精度
return (num[count/2-1]+num[count/2])/2.0;
}else {
return num[count/2];
}
}
時間複雜度:O(n+m)
思路二:二分法 根據題目中要求的時間複雜度O(log(m+n))想到要使用二分法。因此我們就不能合併兩個數組了。
其實根據上一題我們就不難發現是否合併兩個數組並不重要,我們知道兩個數組的長度總和是count,知道中位數是第count/2個或者(num[count/2-1]+num[count/2])/2.0就夠了。我們困難的是怎樣在不同的數組之間進行二分法。
我們換個思考方向:我們把“找中位數”看作是”找第k小的數“的特殊情況。可以充分利用數組是有序的這一特點去找第k小的數,每次排除掉k/2個元素。
看一個”尋找第k小的數“例子:
-
假設我們現在要從A和B兩個有序數組中找第7小的數字,我們先比較兩個數組的第k/2個元素的大小。3<4所以A數組[1,2,3]這三個元素必然不是第7小的數字,所以排除掉。
-
已經排除了3個,所以我們現在需要在兩個數組剩餘的部分尋找第4小的數。同樣的,我們先比較兩個數組剩餘元素的第k/2個元素的大小,5>3所以B數組[1,3]這兩個元素必然不是第4小的元素,所以排除。
-
我們繼續在兩個數組剩餘部分尋找第2小的數。我們比較兩個數組剩餘元素的第k/2個元素,4=4去掉哪個都行,我們統一處理即可,去掉B的4元素。
-
此時k=1,只需要判斷兩個數組剩餘部分的第一個元素哪個小即可,找到A數組的4就是第7小的數。
按照上述例子中的算法,會出現一個問題:每次循環都需要取兩個數組剩餘部分的第k/2個元素進行比較,如果此時某個數組剩餘部分不足k/2個元素怎麼辦???
再看一個例子:
- 依然是找第7小的數,但是B數組不能取到第k/2個元素,此時取出B數組的最後一個元素和A數組的第k/2個元素作比較即可。
- 此時B數組已空,所以直接返回A數組的第5個元素即可。
回到本題“尋找中位數”!有了這個”尋找第k小的數“的算法,去尋找兩個有序數組的中位數就容易多了。可以看到無論是找第奇數個還是找第偶數個對上述算法並無影響,最終都會因爲k==1或一個數組空了,返回尋找結果。
最終,“尋找中位數”這個算法我們就以遞歸的方式進行,爲了防止數組長度小於k/2,所以每次比較數組的第min(k/2,數組剩餘len)個元素,將小的那部分排除之後,將兩個新數組繼續送入遞歸,並將k減去排除的元素個數。遞歸的出口就是k==1或其中一個數組剩餘長度爲0。
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int len1 = nums1.length;
int len2 = nums2.length;
//將奇數和偶數情況統一處理,如果是奇數情況就求兩次。這部分也可以用判斷分別處理
int Kth1=(len1+len2+1)/2,Kth2=(len1+len2+2)/2;
//注意最後結果是double,如果/2會丟失精度
return (findKth(nums1,0,len1-1,nums2,0,len2-1,Kth1)
+findKth(nums1,0,len1-1,nums2,0,len2-1,Kth2))/2.0;
}
public int findKth(int[] nums1,int start1,int end1,int[] nums2,int start2,int end2,int k){
//計算兩個數組剩餘部分長度
int len1=end1-start1+1;
int len2=end2-start2+1;
//很巧妙的一步,讓len1總是剩餘長度較小的那個,如果出現爲空的情況一定是len1
if (len1>len2)return findKth(nums2,start2,end2,nums1,start1,end1,k);
//遞歸的出口,當某個數組剩餘長度爲0或者k==1的時候
if (len1==0)return nums2[start2+k-1];
if (k==1)return Math.min(nums1[start1],nums2[start2]);
//比較兩個數組剩餘部分的第k/2個元素大小,如果越界則取數組最後一個元素進行比較即可
int i=start1+Math.min(len1,k/2)-1,j=start2+ Math.min(len2,k/2)-1;
//排除較小的元素部分,k減去排除元素的個數
if (nums1[i]<nums2[j]){
return findKth(nums1,i+1,end1,nums2,start2,end2,k-(i-start1+1));
}else {
return findKth(nums1,start1,end1,nums2,j+1,end2,k-(j-start2+1));
}
}
時間複雜度:O(log(n+m))
這個算法邏輯上並不難,但是細節還是需要注意的。
//很巧妙的一步,讓len1總是剩餘長度較小的那個,如果出現爲空的情況一定是len1
if (len1>len2)return findKth(nums2,start2,end2,nums1,start1,end1,k);
最初寫了一套很多三元表達式判斷的冗雜版實現,參考了大佬的實現之後發現這句確實巧妙。