通常,我們需要在一大堆數中求前K大的數,或者求前K小的。比如在搜索引擎中求當天用戶點擊次數排名
前10000的熱詞;在文本特徵選擇中求IF-IDF值按從大到小排名前K個的等等問題,都涉及到一個核心問
題,即TOP-K問題。
通常來說,TOP-K問題可以先對所有數進行快速排序,然後取前K大的即可。但是這樣做有兩個問題。
(1)快速排序的平均複雜度爲,但最壞時間複雜度爲,不能始終保證較好的複雜度。
(2)我們只需要前K大的,而對其餘不需要的數也進行了排序,浪費了大量排序時間。
除這種方法之外,堆排序也是一個比較好的選擇,可以維護一個大小爲K的堆,時間複雜度爲。
我們的目的是求前K大的或者前K小的元素,實際上有一個比較好的算法,叫做BFPTR算法,又稱爲中位數
的中位數算法,它的最壞時間複雜度爲,它是由Blum、Floyd、Pratt、Rivest、Tarjan提出。
該算法的思想是修改快速選擇算法的主元選取方法,提高算法在最壞情況下的時間複雜度。
在BFPTR算法中,僅僅是改變了快速排序Partion中的pivot值的選取,在快速排序中,我們始終選擇第一個元
素或者最後一個元素作爲pivot,而在BFPTR算法中,每次選擇五分中位數的中位數作爲pivot,這樣做的目的
就是使得劃分比較合理,從而避免了最壞情況的發生。算法步驟如下
(1)將輸入數組的個元素劃分爲組,每組5個元素,且至多隻有一個組由剩下的個元素組成。
(2)尋找個組中每一個組的中位數,首先對每組的元素進行插入排序,然後從排序過的序列中選出中位數。
(3)對於(2)中找出的箇中位數,遞歸進行步驟(1)和(2),直到只剩下一個數即爲這個元素
的中位數,找到中位數後並找到對應的下標。
(4)進行Partion劃分過程,Partion劃分中的pivot元素下標爲。
(5)進行高低區判斷即可。
本算法的最壞時間複雜度爲,值得注意的是通過BFPTR算法將數組按第K小(大)的元素劃分爲兩部分,而
這高低兩部分不一定是有序的,通常我們也不需要求出順序,而只需要求出前K大的或者前K小的。
另外注意一點,求第K大就是求第n-K+1小,這兩者等價。TOP K問題在工程中有重要應用,所以很有必要掌握。
import java.util.*;
public class FindK
{
private static int array[];
public static void main(String[] args)
{
Scanner cin=new Scanner(System.in);
array=new int[20];
System.out.println("原序列");
for(int i=0;i<20;i++)
{
array[i]=20-i;
System.out.print(array[i]+" ");
}
System.out.println();
System.out.println("請輸入你要搜索的數字");
int k=cin.nextInt();
System.out.println("第個"+k+"數是"+BFPTR(array,0,19,k));
System.out.println("處理後的序列:");
for(int i=0;i<20;i++)
{
System.out.print(array[i]+" ");
}
}
private static int BFPTR(int[] array,int left,int right,int k)
{
int mid=findMid(array,left,right);//尋找中位數的中位數並把它放到第一位去
int i=partion(array,left,right,mid);//快排固定中位數的位置,浮動的並不一定是理想狀態的位子
int num=i-left+1; //代表的是從中位數的位子到前面有多少個數
if(num==k)
return array[i];
if(num>k) //如果k在num個數裏面就深入查找k
return BFPTR(array,left,i-1,k);
//k是第k個數。是數量!所以是可以變動的。只要數到相應個數就好
return BFPTR(array,i+1,right,k-num);//如果k在num個數外面就直接挪到後面去查找k
}
//尋找中位數的中位數
private static int findMid(int[] array,int left,int right)
{
if(right==left)
return left;
int i=0,n=0;
for(i=left;i<right-5;i+=5)
{
insertSort(array,i,i+4);
n=i-left;
swap(array, i+n/5,i+2);
}
//處理剩餘元素
int num=right-i+1;
if(num>0)
{
insertSort(array,i,i+num-1);
n=i-left;
swap(array,left+n/5,i+num/2);
}
n/=5;
if(n==left)
return left;
return findMid(array,left,left+n);
}
//定位後,快排劃分兩邊
private static int partion(int[] array,int left,int right,int p)
{
swap(array,p,left);
int i=left;
int j=right;
int pivot=array[left];
while(i<j)
{
while(array[j]>=pivot&&i<j)
j--;
array[i]=array[j];
while(array[i]<=pivot&&i<j)
i++;
array[j]=array[i];
}
array[i]=pivot;
return i;
}
//部分插入排序
private static void insertSort(int[] array,int left,int right)
{
int[] temp=new int[right-left+1];
for(int i=0;i<temp.length;i++)
temp[i]=array[left+i];
Arrays.sort(temp);//當數量少於一定值的時候sort是插入排序。
for(int i=0;i<temp.length;i++)
array[left+i]=temp[i];
}
//用於交換
private static void swap(int[] array,int i,int j)
{
int temp;
temp=array[i];
array[i]=array[j];
array[j]=temp;
}
}