【程式設計之美】讀書筆記:尋找最大的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小的資料。最好的辦法是利用解法二和解法三的原理進行查詢。