題目:在兩個排序數組中尋找第K小的數
舉例:
arr1=[1,2,3,4,5],arr2=[3,4,5],k=1
1是所有數中第一小的數,所以返回1
arr1=[1,2,3],arr2=[3,4,5,6],k=4
3是所有數中第4小的數,所以返回3
要求:如果arr1的長度為N,arr2的長度為M,時間復雜度請達到O(log(min{M,N}),額外空間復雜度為O(1)
思路:暴力解法坑定是將兩個數組放到一起再進行排序,然后再找出第K個,但這樣的時間復雜度肯定超了,一看到logN,就想起肯定與二分查找有關
看到這個題,我們先來看一個稍簡單的同類的題,如下:
題目:在兩個長度相同的排序數組中找到上中位數 給定兩個有序數組arr1和arr2,已知兩個數組的長度都為N,求兩個數組中的所有數的上中位數 舉例: arr1=[1,2,3,4].arr2=[3,4,5,6] 總共有8個數,那么上中位數是第4小的數,所以返回3 arr1=[0,1,2],arr2=[3,4,5] 總共有6個數,那么上中位數是第3小的數,所以返回2 要求:時間復雜度O(logN),額外空間復雜度為O(1)
先來分析一下這個題:根據時間復雜度的要求,我們首先利用二分的方式來尋找上中位數
1.假定兩數組分別為arr1[start1,end1] 、arr2[start2,end2]
初始時,start1=0,end1=N-1;start2=0,end2=N-1.
2.如果srart1==end1,那么也有start2==end2;
表明每個數組內此時各只有一個元素,總元素數為2,上中位數為其中較小的那個
所以直接返回 min(arr1[start1],arr2[start2]);
3.如果srart1!=end1,說明此時兩個數組的長度均大於1,
則令 mid1=(start1+end1)/2 ;mid2=(start2+end2)/2 .來表示兩個數組的中間位置
這個時候需要分情況討論了;
a.如果arr1[mid1]==arr2[mid2]時, 直接返回arr1[mid1]或arr2[mid2]
舉個例子來說明一下:
(1).當兩個數組的長度都為奇數時
arr1={a1,a2,a3,a4,a5} 、其中的a1表示第一個數,a5表示第5個數,(不表示值)下同
arr2={b1,b2,b3,b4,b5}
此時a3==b3,由於兩個數組本身是有序的,在a3前面壓着2個數,在b3前面壓着2個數,所以a3,b3前面共壓了4個數,現在要求第5小的數(總共有10個數,故上中位數為5),必然是a3或是b3,而a3==b3,所以直接返回這兩個數中任一個即可,即返回arr1[mid1]
(2).當兩個數組的長度都為偶數時
arr1={a1,a2,a3,a4}
arr2={b1,b2,b3,b4}
此時a2==b2,由於兩個數組本身是有序的,在a2前面壓着1個數,在b2前面壓着1個數,所以a2,b2前面共壓了2個數,現在要求第4小的數(總共有8個數,故上中位數為4),必然是a2或是b2,而a2==b2,所以直接返回這兩個數中任一個即可,即返回arr1[mid1]
b.如果arr1[mid1] > arr2[mid2]時,
舉個例子來說明一下:
(1).當兩個數組的長度都為奇數時
arr1={a1,a2,a3,a4,a5} 、其中的a1表示第一個數,a5表示第5個數,(不表示值)下同
arr2={b1,b2,b3,b4,b5}
此時a3>b3,由於兩個數組本身是有序的,在b3前面必然至少壓着2個數,而在a3前面至少壓着5個數(a1,a2,b1,b2,b3),所以a3至少應該是第6個數起(因為已經知道前面有5個數肯定比它要小),后面的a4最好情況下也是第7個數起,再后面的a5也必然是大於5的,(因為此時數組總長度為10,要尋找第5小的數),故此時對於arr1數組,第5小數必然要在{a1,a2}里面找,而對於arr2數組,b2 可能是第5小數嗎?不可能,因為在arr2數組中b2 前只壓了1個數,在ar1數組中,b2最多只能把2個數(a1,a2)壓在底下,所以b2最好情況下也只能是第4小數,而對於b1 ,由於壓得數更少所以跟不可能,所以從b3 開始才有可能是第5小數
由於兩數組長度要保持一致,現在來看一下兩數組中第5小數可能會出現的位置
{a1,a2,a3}
{b3,b4,b5}
現在我們來找一下這兩個新數組的共同的上中位數,也就是這6個數中第3小的數記為a,這個a 代表啥?就是a在這兩段數組中,會把2個數壓在下面,同時也自然會把原來的arr2數組中的b1,b2壓在下面,所以a 正好就是第5小的數,也就是我們要求的結果,所以解決問題的方法就是對新的兩個數組繼續求上中位數,具體做法就是,直接令 end1=mid1,start2=mid2,然后重復求解上中位數就行
(2).當兩個數組的長度都為偶數時
arr1={a1,a2,a3,a4}
arr2={b1,b2,b3,b4}
此時a2>b2,由於兩個數組本身是有序的,a2前面至少壓着3個數,所以a2可能是第4小的數,而對於后面的a3,前面都至少壓了4個數了,必然不是,后面的a4更不用看了,對於數組arr2,b2最好前面也是只壓了2個數(a1,b1),所以第4小數不可能是b2,更不可能是b1,
由於兩數組長度要保持一致,現在來看一下兩數組中第5小數可能會出現的位置
{a1,a2}
{b3,b4}
問題同樣轉化為了尋找新數組的上中位數,所以令end1=mid1,start2=mid2+1.
c.如果arr1[mid1] < arr2[mid2]時,
分析方法與b是一樣的(就像b中兩數組互換了下)
所以,數組長為奇數時,令start1=mid1,end2=mid2
數組長度為偶數時,令start1=mid1+1,end2=mid2,重復尋找上中位數就行
所以,我們可以給出整個算法的代碼:
1 public int getUpMedian(int[] arr1,int[] arr2){ 2 if(arr1==null||arr2==null||arr1.length!=arr2.length){ 3 throw new RuntimeException("Your arr is invalid"); 4 } 5 int start1=0; 6 int end1=arr1.length-1; 7 int start2=0; 8 int end2=arr2.length-1; 9 int mid1=0,mid2=0; 10 int offset=0;//用於判斷過程中數組的長度的奇偶 11 while(start1<end1){ 12 mid1=(start1+end1)/2; 13 mid2=(start2+end2)/2; 14 offset=((end1-start1+1)&1)^1; 15 //元素個數為奇數,offset為0,元素個數為偶數,offset為1 16 if (arr1[mid1] > arr2[mid2]){ 17 end1=mid1; 18 start2=mid2+offset; 19 }else if(arr1[mid1]<arr2[mid2]){ 20 end2=mid2; 21 start1=mid1+offset; 22 }else{ 23 return arr1[mid1]; 24 } 25 } 26 return Math.min(arr1[start1],arr2[start2]); 27 }
現在我們來看一下頭先的那個題,即尋找第K小數
思路:我們先記
長度較短的數組為shortArr,長度記為lenS
長度較長的數組記為longArr,長度記為lenL
假設shortArr長度為10,{a1,a2,a3,...a10}表示第一個數、第2個數、...
假設longArr長度為27,{b1,b2,...,b27}表示第一個數,...
1.當k<1或k>lenS+lenL,則k無效
2.如果k<=lenS.
那么在shortArr中選前面k個數,在longArr中也選前面k個數
則兩段數組的上中位數就是第k 小數(等價於轉化成了兩個長度相同的數組的形式)
3.如果k>lenL
如一共有37個數,求第33小的數(33>lenL==27)
在{a1,a2,..a10}中a5及a5以前的數都不可能是第33小的數,因為就算a5比b27都大,此時a5==32,所以不可能,a5前面的也不可能,對於a6,如果a6>b27,則a6必然是第33小的數,直接返回a6,否則a6不是。同理在{b1,b2,...,b27}中{b1,b2,...,b22}也必然不可能是第33小的數,因為b22最大也只能為22+10=32,所以應從b23開始找,只要b23>a10,則b23必然是第33小,否則b23也不是,如果a6和b23有一個滿足條件,則可以直接返回,否則說明{a1,a2,..,a6},{b1,b2,..,b23}都不可能是,應在{a7,..,a10},{b24,..,b27}這兩個數組里找他們的上中位數
4.lenS<k<=lenL時
如求第17小的數
在{a1,a2,..,a10}中每個數都有可能
在{b1,..b27}中b6以前的數必然是不可能的,因為對於b6,最大也只為6+10=16,b18以后的也不可能是,因為他本身就是長數組中的第18個了
所以長數組變成了{b7,...,b17}這11個數,如果此時b7>a10,則可以直接返回b7,否則b7不是
再求{b8,...,b17}和{a1,...,a10}上中位數,則為答案
實現代碼為:
1 public int getUpMedian(int[] arr1,int start1,int end1,int[] arr2,int start2,int end2){ 2 int mid1=0,mid2=0; 3 int offset=0;//用於判斷過程中數組的長度的奇偶 4 while(start1<end1){ 5 mid1=(start1+end1)/2; 6 mid2=(start2+end2)/2; 7 offset=((end1-start1+1)&1)^1; 8 //元素個數為奇數,offset為0,元素個數為偶數,offset為1 9 if (arr1[mid1] > arr2[mid2]){ 10 end1=mid1; 11 start2=mid2+offset; 12 }else if(arr1[mid1]<arr2[mid2]){ 13 end2=mid2; 14 start1=mid1+offset; 15 }else{ 16 return arr1[mid1]; 17 } 18 } 19 return Math.min(arr1[start1],arr2[start2]); 20 } 21 public int findKthNum(int[]arr1,int[]arr2,int kth){ 22 if(arr1==null||arr2==null){ 23 throw new RuntimeException("Your arr is invalind"); 24 } 25 if(kth<1||kth>arr1.length+arr2.length){ 26 throw new RuntimeException("k is invalid"); 27 } 28 int[]longs=arr1.length>=arr2.length?arr1:arr2; 29 int[]shorts=arr1.length<arr2.length?arr1:arr2; 30 int l=longs.length; 31 int s=shorts.length; 32 if(kth<=s){ 33 return getUpMedian(shorts,0,kth-1,longs,0,kth-1); 34 } 35 if(kth>l){ 36 if(shorts[kth-l-1]>=longs[l-1]) 37 return shorts[kth-l-1]; 38 if(longs[kth-s-1]>=shorts[s-1]) 39 return longs[kth-s-1]; 40 return getUpMedian(shorts,kth-1,s-1,longs,kth-s,l-1); 41 } 42 if(longs[kth-s-1]>=shorts[s-1]){ 43 return longs[kth-s-1]; 44 } 45 return getUpMedian(shorts,0,s-l,longs,kth-s,kth-1); 46 }
參考:《程序員代碼面試指南》左程雲