題目:
CVTE筆試題
https://www.1024do.com/?p=3949
搜索引擎會通過日志文件把用戶每次檢索使用的所有檢索串都記錄下來,每個查詢串的長度為1-255字節。
假設目前有一千萬個記錄(這些查詢串的重復度比較高,雖然總數是1千萬,但如果除去重復后,不超過3百萬個。一個查詢串的重復度越高,說明查詢它的用戶越多,也就是越熱門。),請你統計最熱門的10個查詢串,要求使用的內存不能超過1G。
思路:此題解題步驟可分為兩步:
1.統計每個“查詢串”(下稱為query)出現的次數 2.根據統計結果,找出top 10
1.統計query出現次數:
利用hash思想,維護一個Key為Query字串,Value為該Query出現次數的HashTable。每次讀取一個Query,如果該字串不在Table中,那么加入該字串,並且將Value值設為1;如果該字串在Table中,那么將該字串的計數加一即可。
因為hashtable中查詢速度非常快,幾乎達到O(1)的時間復雜度,所以統計N個記錄,時間復雜度能達到O(N),線性的時間復雜度
2.根據統計結果,找出topK
借助堆結構,我們可以在log量級的時間內查找和調整/移動。‘
具體做法:維護一個K(該題目中是10)大小的小根堆,然后遍歷300萬的Query,分別和根元素進行對比。(這道題目因為是找“最大”的10個,所以用小根堆,每次遍歷的元素只要和堆中最小的元素——“根”作比較,如果小於根,說明肯定進不了topK;如果大於根,說明它可以淘汰堆中的最小的一個元素,也就是根,然后再調整)
堆中最后剩下的K個元素就是top K
TOP K問題
Top k問題的討論(三種方法的java實現及適用范圍)
在很多的筆試和面試中,喜歡考察Top K.下面從自身的經驗給出三種實現方式及實用范圍。
- 合並法
這種方法適用於幾個數組有序的情況,來求Top k。時間復雜度為O(k*m)。(m:為數組的個數).具體實現如下:
/** * 已知幾個遞減有序的m個數組,求這幾個數據前k大的數 *適合采用Merge的方法,時間復雜度(O(k*m); */ import java.util.List; import java.util.Arrays; import java.util.ArrayList; public class TopKByMerge{ public int[] getTopK(List<List<Integer>>input,int k){ int index[]=new int[input.size()];//保存每個數組下標掃描的位置; int result[]=new int[k]; for(int i=0;i<k;i++){ int max=Integer.MIN_VALUE; int maxIndex=0; for(int j=0;j<input.size();j++){ if(index[j]<input.get(j).size()){ if(max<input.get(j).get(index[j])){ max=input.get(j).get(index[j]); maxIndex=j; } } } if(max==Integer.MIN_VALUE){ return result; } result[i]=max; index[maxIndex]+=1; } return result; }
- 快排過程法
快排過程法利用快速排序的過程來求Top k.平均時間復雜度為(O(n)).適用於無序單個數組。具體java實現如下:
Quick Select的目標是找出第k大元素,所以
選取一個基准元素pivot,將數組切分(partition)為兩個子數組,
- 若切分后的左子數組的長度 > k,則第k大元素必出現在左子數組中;
- 若切分后的左子數組的長度 = k-1,則第k大元素為pivot;
- 若上述兩個條件均不滿足,則第k大元素必出現在右子數組中。
/* *利用快速排序的過程來求最小的k個數 * */ public class TopK{ int partion(int a[],int first,int end){ int i=first; int main=a[end]; for(int j=first;j<end;j++){ if(a[j]<main){ int temp=a[j]; a[j]=a[i]; a[i]=temp; i++; } } a[end]=a[i]; a[i]=main; return i; } void getTopKMinBySort(int a[],int first,int end,int k){ if(first<end){ int partionIndex=partion(a,first,end); if(partionIndex==k-1)return; else if(partionIndex>k-1)getTopKMinBySort(a,first,partionIndex-1,k); else getTopKMinBySort(a,partionIndex+1,end,k); } } public static void main(String []args){ int a[]={2,20,3,7,9,1,17,18,0,4}; int k=6; new TopK().getTopKMinBySort(a,0,a.length-1,k); for(int i=0;i<k;i++){ System.out.print(a[i]+" "); } } }
- 采用小根堆或者大根堆
求最大K個采用小根堆,而求最小K個采用大根堆。
求最大K個的步奏:
- 根據數據前K個建立K個節點的小根堆。
- 在后面的N-K的數據的掃描中,
- 如果數據大於小根堆的根節點,則根節點的值覆為該數據,並調節節點至小根堆。
- 如果數據小於或等於小根堆的根節點,小根堆無變化。
求最小K個跟這求最大K個類似。時間復雜度O(nlogK)(n:數據的長度),特別適用於大數據的求Top K。
/** * 求前面的最大K個 解決方案:小根堆 (數據量比較大(特別是大到內存不可以容納)時,偏向於采用堆) * * */ public class TopK { /** * 創建k個節點的小根堆 * * @param a * @param k * @return */ int[] createHeap(int a[], int k) { int[] result = new int[k]; for (int i = 0; i < k; i++) { result[i] = a[i]; } for (int i = 1; i < k; i++) { int child = i; int parent = (i - 1) / 2; int temp = a[i]; while (parent >= 0 &&child!=0&& result[parent] >temp) { result[child] = result[parent]; child = parent; parent = (parent - 1) / 2; } result[child] = temp; } return result; } void insert(int a[], int value) { a[0]=value; int parent=0; while(parent<a.length){ int lchild=2*parent+1; int rchild=2*parent+2; int minIndex=parent; if(lchild<a.length&&a[parent]>a[lchild]){ minIndex=lchild; } if(rchild<a.length&&a[minIndex]>a[rchild]){ minIndex=rchild; } if(minIndex==parent){ break; }else{ int temp=a[parent]; a[parent]=a[minIndex]; a[minIndex]=temp; parent=minIndex; } } } int[] getTopKByHeap(int input[], int k) { int heap[] = this.createHeap(input, k); for(int i=k;i<input.length;i++){ if(input[i]>heap[0]){ this.insert(heap, input[i]); } } return heap; } public static void main(String[] args) { int a[] = { 4, 3, 5, 1, 2,8,9,10}; int result[] = new TopK().getTopKByHeap(a, 3); for (int temp : result) { System.out.println(temp); } } }