TopK問題算是面試中常考的,而且有實際價值的算法中比較有代表性的一個了,主要解決方法有堆、快速選擇、排序三種方案。下一次專門講一下快速選擇類的算法。
1、給定一個數組,求其最大(小)的K個元素
解題思路有多種,比如可以對數組排序,使用Arrays.sort();然後取前面K個數,就是最小或者最大的K個數,sort可以指定按升序還是降序排列。當然,這麼做的時間複雜度是Nlog(N),雖然的確很快,但是還是會被面試官拿來鞭屍。
其實他們想考的就是堆罷了,雖然都是Nlog(N)。使用堆可以得到topK問題的通用解法。就拿本題來講,以最大的topK個數爲例,
1、我們可以建一個小頂堆,當堆元素數量小於K個時,直接入堆,
2、當大於等於K個時,與堆頂部比較,如果大於堆頂,則堆頂出堆,然後執行入堆操作
3、將堆裏面的元素全部彈出,即爲最大的K個數
我們知道,堆一般都是用優先隊列實現的,在Java裏面,PriorityQueue默認就是小頂堆。
public int[] getTopK(int[] nums,int k){
PriorityQueue<Integer> heap=new PriorityQueue<>();
for(int i:nums){
if(heap.size()<k){
heap.offer(i);
}else if(heap.top()<i){
heap.poll();
heap.offer(i);
}
}
int [] result=new int[heap.size()];
for(int i=0;i<result.length;i++){
result[i]=heap.poll();
}
return result;
}
2、給定一個數組,求其第K大的數
這個問題和上個問題不同之處就在於本題只需要一個數,直接套上題代碼,彈出的第一個數就是第K大的數。
不過這時候面試官大概率會要求你寫出一個複雜度爲O(N)的算法,而上題複雜度Nlog(N),空間複雜度爲k,就不好用了。
這時候考察的其實是一個快排的思想,快排還有個兄弟,叫快速選擇,也就是quickSelect算法,時間複雜度可以降到O(N)。
還記得快排怎麼做的嗎?選取一個樞紐元,在一輪過後,左邊元素全部小於等於樞紐元,右邊元素全部大於等於樞紐元。然而我們找的是最小或者最大的第K個數,完全不用care另一部分的值,我們只對樞紐元左半部分或者右半部分遞歸的實現快速選擇就行了。
經過快速選擇後,所需要的數據就是數組中第k-1個元素。
//不考慮k不合法的情況了
public int getKthNumber(int nums[],int k){
quickSelect(nums,0,nums.length-1,k);
return nums[k-1];
}
private void quickSelect(int []nums,int left,int right,int k){
if(left<right){
int i=left,j=right;
int privot=nums[left];//選取樞紐元
while(i<j){
while(i<j&&nums[j]>=privot){j--;}//找到第一個小於樞紐元的值
while(i<j&&nums[i]<=privot){i++;}//找到第一個大於樞紐元的值
if(i<j){
swap(nums[i],i,j);
}
}
swap(nums,left,i);//恢復數紐元
if(k<=i){
quickSelect(nums,left,i-1,k);
}else{
quickSelect(nums,i+1,right,k);
}
}
}
private void swap(int[] nums,int i,int j){
int temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
3、給定一個數組求中位數
這個問題用堆實現也比較簡單,如果數組大小爲奇數,我們設置一個(數組長度+1)/2的堆,然後按照題目1來彈出堆頂就是中位數。如果是偶數,我們設置一個大小爲 數組長度/2 +1的一個堆,然後彈出兩個元素取平均值即可。
然而這樣時間複雜度是Nlog(N),面試官無法忍受。。。
想到題目2中快速選擇的思想,我們可以這樣做:
1、數組長度len爲奇數,我們使用2中代碼選擇第(len+1)/2小的數,即爲中位數
2、數組長度len爲偶數,我們選擇第len/2小的數和第len/2+1小的數,取均值即爲中位數
話不多說,直接放碼:
public void test(){
int[] nums={3,5,1,6,7};
if(nums.length%2==0){
int k1=(nums.length+1)>>1;
quickSelect(nums,k1);
int a=nums[k1-1];
int k2=(nums.length+1)>>1+1;
quickSelect(nums,k2);
int b=nums[k2-1];
System.out.println((a+b)>>1);
}else {
int k=(nums.length+1)>>1;
quickSelect(nums,k);
System.out.println(nums[k-1]);
}
}
public void quickSelect(int[] nums,int k){
quickSelect(nums,0,nums.length-1,k);
}
public void quickSelect(int nums[],int left,int right,int k){
if(left<right){
int i=left,j=right;
int pivot=nums[left];
while(i<j){
while(i<j&&nums[j]>=pivot){j--;}
while(i<j&&nums[i]<=pivot){i++;}
if(i<j){
swap(nums,i,j);
}
}
swap(nums,left,i);
if(k<=i){
quickSelect(nums,left,i-1,k);
}else {
quickSelect(nums,i+1,right,k);
}
}
}
private void swap(int[] nums, int i, int j) {
int temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}