1. 程式人生 > >【程式設計之美】讀書筆記:尋找最大的K個數

【程式設計之美】讀書筆記:尋找最大的K個數

問題:查詢大量無序元素中最大的K個數。

         解法一:該解法是大部分能想到的,也是第一想到的方法。假設資料量不大,可以先用快速排序或堆排序,他們的平均時間複雜度為O(N*logN),然後取出前K個,時間複雜度為O(K),總的時間複雜度為O(N*logN)+O(K).
        當K=1時,上面的演算法的時間複雜度也是O(N*logN),上面的演算法是把整個陣列都進行了排序,而原題目只要求最大的K個數,並不需要前K個數有限,也不需要後N-K個數有序。可以通過部分排序演算法如選擇排序和交換排序,把N個數中的前K個數排序出來,複雜度為O(N*K),選擇哪一個,取決於K的大小,在K(K<logN)較小的情況下,選擇部分排序。

/*將陣列a[s]...a[t]中的元素用一個元素劃開,儲存中a[k]中*/  
void partition(int a[], int s,int t,int &k)    
{    
    int i,j,x;    
    x=a[s];    //取劃分元素     
    i=s;        //掃描指標初值     
    j=t;    
    do    
    {    
        while((a[j]<x)&&i<j) j--;   //從右向左掃描,如果是比劃分元素小,則不動  
        if(i<j) a[i++]=a[j];           //大元素向左邊移     
        while((a[i]>=x)&&i<j) i++;      //從左向右掃描,如果是比劃分元素大,則不動   
        if(i<j) a[j--]=a[i];            //小元素向右邊移     
    
    }while(i<j); //直到指標i與j相等      
a[i]=x;      //劃分元素就位     
k=i;    
}    
/*查詢陣列前K個最大的元素,index:返回陣列中最大元素中第K個元素的下標(從0開始編號),high為陣列最大下標*/  
int FindKMax(int a[],int low,int high,int k)  
{   
     int q;  
int index=-1;  
   if(low < high)    
      {    
        partition(a , low , high,q);    
        int len = q - low + 1; //表示第幾個位置      
        if(len == k)    
         index=q; //返回第k個位置     
        else if(len < k)     
         index= FindKMax(a , q + 1 , high , k-len);       
       else   
        index=FindKMax(a , low , q - 1, k);    
      }    
    return index;  
  
}  
int main()  
{  
     int a[]={20,100,4,2,87,9,8,5,46,26};    
    int Len=sizeof(a)/sizeof(int);   
     int K=4;  
 FindKMax(a , 0 , Len- 1 , K) ;      
    for(int i = 0 ; i < K ; i++)    
      cout<<a[i]<<" ";    
 return 0;  
}  



         解法二(掌握)避免對前K個數進行排序來獲取更好的效能(利用快速排序的原理)。
        假設N個數儲存在陣列S中,從陣列中隨機找一個元素X,將陣列分成兩部分Sa和Sb.Sa中的元素大於等於X,Sb中的元素小於X。
    出現如下兩種情況:
   (1)若Sa組的個數大於或等於K,則繼續在sa分組中找取最大的K個數字 。
   (2)若Sa組中的數字小於K ,其個數為T,則繼續在sb中找取 K-T個數字 。
   一直這樣遞迴下去,不斷把問題分解成小問題,平均時間複雜度為O(N*logK)。
   程式碼如下:co

         解法三:(掌握)用容量為K的最小堆來儲存最大的K個數。最小堆的堆頂元素就是最大K個數中的最小的一個。每次掃描一個數據X,如果X比堆頂元素Y小,則不需要改變原來的堆。如果X比堆頂元素大,那麼用X替換堆頂元素Y,在替換之後,X可能破壞了最小堆的結構,需要調整堆來維持堆的性質。調整過程時間複雜度為O(logK)。 全部的時間複雜度為O(N*logK)。
          這種方法當資料量比較大的時候,比較方便。因為對所有的資料只會遍歷一次,第一種方法則會多次遍歷陣列。 如果所查詢的K的數量比較大。可以考慮先求出k` ,然後再求出看k`+1 到 2 * k`之間的資料,然後一次求取。
  
  程式碼如下:

void heapifymin(int Array[],int i,int size)  
 {  
    if(i<size)  
 {  
  int left=2*i+1;  
  int right=2*i+2;  
  int smallest=i;//假設最小的節點為父結點  
  //確定三個結點中的最大結點  
  if(left<size)  
  {  
   if(Array[smallest]>Array[left])  
    smallest=left;  
  }  
  if(right<size)  
  {  
          if(Array[smallest]>Array[right])  
    smallest=right;  
  }  
  
  //開始交換父結點和最大的子結點  
       if(smallest!=i)  
    {  
     int temp=Array[smallest];  
     Array[smallest]=Array[i];  
     Array[i]=temp;  
     heapifymin(Array,smallest,size);//對調整的結點做同樣的交換  
    }  
 }  
 }  
  
//建堆過程,建立最小堆,從最後一個結點開始調整為最小堆  
void min_heapify(int Array[],int size)  
 {  
  int i;  
  for(i=size-1;i>=0;i--)  
   heapifymin(Array,i,size);  
    
 }  
 //k為需要查詢的最大元素個數,size為陣列大小,kMax儲存k個元素的最小堆  
void FindMax(int Array[],int k,int size,int kMax[])  
{  
  
  for(int i=0;i<k;i++)  
   kMax[i]=Array[i];  
 //對kMax中的元素建立最小堆  
   min_heapify(kMax,k);  
 printf("最小堆如下所示 : \n");  
for(i=0;i<k;i++)  
printf("%4d",kMax[i]);  
printf("\n");  
  
 for(int j=k;j<size;j++)  
  {  
     if(Array[j]>kMax[0]) //如果最小堆的堆頂元素,替換  
  {  
        int temp=kMax[0];  
  kMax[0]=Array[j];  
     Array[j]=temp;  
  //可能破壞堆結構,調整kMax堆  
  min_heapify(kMax,k);  
  }  
    
  
  }  
  
  
   
}  
  
int main()  
{  
      
 int a[]={10,23,8,2,52,35,7,1,12};  
 int length=sizeof(a)/sizeof(int);  
  
    //最大四個元素為23,52,35,12  
/***************查詢陣列中前K個最大的元素****************/  
 int k=4;  
 int * kMax=(int *)malloc(k*sizeof(int));  
 FindMax(a,k,length,kMax);  
  
   printf("最大的%d個元素如下所示 : \n",k);  
    for(int i=0;i<k;i++)  
        printf("%4d",kMax[i]);  
 printf("\n");  
 return 0;  
}  


         解法四:這也是尋找N個數中的第K大的數演算法。利用二分的方法求取TOP k問題。 首先查詢 max 和 min,然後計算出mid = (max + min) / 2該演算法的實質是尋找最大的K個數中最小的一個。該演算法在實際應用中效果不佳。

 const int N = 8 ;    
 const int K = 4 ;    
     
 /*  
 利用二分的方法求取TOP k問題。  
 首先查詢 max 和 min,然後計算出 mid = (max + min) / 2  
 該演算法的實質是尋找最大的K個數中最小的一個。   
   
   
 */    
     
 int find(int * a , int x) //查詢出大於或者等於x的元素個數      
 {    
     int sum = 0 ;    
         
     for(int i = 0 ; i < N ; i++ )    
     {     
        if(a[i] >= x)    
          sum++ ;                    
     }    
      return sum ;    
 }    
     
   
 int getK(int * a , int max , int min) //最終max min之間只會存在一個或者多個相同的數字      
 {    
     while(max - min > 1)             //max - min的值應該保證比兩個最小的元素之差要小      
      {    
        int mid = (max + min) / 2 ;    
        int num = find(a , mid) ;    //返回比mid大的數字個數      
        if(num >= K)                 //最大的k個數目都要比min值大      
           min = mid ;                   
        else    
           max = mid  ;    
      }    
      cout<<"end"<<endl;    
      return min ;    
 }    
     
 int main()    
 {    
   int a[N] = {54, 2 ,5 ,11 ,554 ,65 ,33 ,2} ;      
   int x = getK(a , 554 , 2) ;     
   cout<<x<<endl ;     
   getchar() ;     
   return 0 ;        
 }  



          解法五:如果N個數都是正數,取值範圍不太大,可以考慮用空間換時間。申請一個包括N中最大值的MAXN大小的陣列count[MAXN],count[i]表示整數i在所有整數中的個數。這樣只要掃描一遍陣列,就可以得到第K大的元素。
程式碼如下:

for(sumCount = 0, v = MAXN -1; v >=0; v--)    
{    
       cumCount += count[v];    
       if(sumCount >= k)    
            break;    
}    
return v;  


  擴充套件問題:

1.如果需要找出N個數中最大的K個不同的浮點數呢?比如,含有10個浮點數的陣列(1.5,1.5,2.5,3.5,3.5,5,0,- 1.5,3.5)中最大的3個不同的浮點數是(5,3.5,2.5)。
     解答:除了解法五不行,其他的都可以。因為最後一種需要是正數。
2. 如果是找第k到第m(0<k<=m<=n)大的數呢?
       解答:可以用小根堆來先求出m個最大的,然後從中輸出k到m個。
3. 在搜尋引擎中,網路上的每個網頁都有“權威性”權重,如page rank。如果我們需要尋找權重最大的K個網頁,而網頁的權重會不斷地更新,那麼演算法要如何變動以達到快速更新(incremental update)並及時返回權重最大的K個網頁?
   解答:(解法三)用堆排序當每一個網頁權重更新的時候,更新堆。

 舉一反三:查詢最小的K個元素

    解答:最直觀的方法是用快速排序或堆排序先排好,在取前K小的資料。最好的辦法是利用解法二解法三的原理進行查詢。