搜尋引擎會通過日誌檔案把使用者每次檢索使用的所有檢索串都記錄下來,每個查詢串的長度為1-255位元組。假設目前一個日誌檔案中有一千萬個記錄(這些查詢串的重複度比較高,雖然總數是1千萬,但如果除去重複後,不超過3百萬個。一個查詢串的重複度越高,說明查詢它的使用者越多,也就是越熱門),請你統計最熱門的10個查詢串,要求使用的記憶體不能超過1G。
1000萬條記錄,每條記錄最大為255Byte,那麼日誌檔案最大有2.5G左右,大於1G記憶體。但是題目中又提到這樣的1000萬條記錄中有許多是重複的,出去重複的話只有300萬條記錄,儲存這樣的300萬條記錄需要0.75G左右的記憶體,小於1G記憶體。那麼我們可以考慮將這些無重複的記錄裝入記憶體,這是我們需要一種資料結構,這種資料結構即能夠儲存查詢串,又能儲存查詢串的出現次數,我們可以通過hashmap<query,count>來儲存。讀取檔案,建立一個hashmap,如果hashmap中儲存了遍歷到的query,則修改該query所對應的count值,使其+1;如果hashmap中沒有這個query,那麼往haspmap中插入<query,1>。這樣我們就建立好了一個包含所有query和次數的hashmap。
然後我們建立一個長度為10最大堆MaxHeap(這裡應該是最小堆MinHeap,求最多的要用最小堆,求最小的要用最大堆,ps:2012-10-8),最小堆的堆頂元素最小,如果堆頂這個最小的元素都大於其他非堆元素了,那麼堆中的其他元素必定大於其他非堆中元素。遍歷hashmap,如果MaxHeap未滿,那麼往MaxHeapMinHeap中插入這個鍵值對,如果MinHeap滿了,則比較遍歷到的元素的count值堆頂的count,如果遍歷到元素的count大於堆頂count值,刪除堆頂元素,插入當前遍歷到的元素。遍歷完整個hashmap以後,在MaxHeapMinHeap中儲存的就是最熱門10個查詢串。
程式碼實現:
花了一天時間才寫出這道題目的具體程式碼實現。具體思路前面已經說過了,主要分為以下幾步:
首先我們遍歷words.txt這個檔案,並且建立一個hashmap,其中key就是words.txt中的查詢串,而value則是這個查詢穿出現的次數。這裡通過判斷key是否存在,如果不存在則put(key,1);如果存在的話,則先求value=get(key),然後put(key,value+1),這裡的put相當於是修改value的值。
在構建好hashmap以後,我們需要建立一個最小堆(我們這裡有LinkedList實現),假如堆沒有滿,我們將hashmpa中的元素放入到最小堆中,如果滿的話,則比較hashmap中元素的value值與堆頂元素的value值,如果大於堆頂元素的value值,則刪除堆頂元素,然後將這個hashmap元素插入到堆中,在調整堆結構,使其滿足最小堆結構。
在遍歷完hashmap以後,我們的最小堆中儲存的元素就是最熱門查詢。
import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.FileReader; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.Map; import java.util.Map.Entry; public class GetPopularQuery { public static void main(String args[]) { GetPopularQuery gpq = new GetPopularQuery(); Map<String, Integer> content = new HashMap<String, Integer>(); gpq.buildHashMap(content); // gpq.printHashMap(content); int k = 10; gpq.findPopularQuery(content, k); } // 第一步:建立hashmap public void buildHashMap(Map<String, Integer> content) { try { FileReader reader = new FileReader("words.txt"); BufferedReader br = new BufferedReader(reader); String s = null; while ((s = br.readLine()) != null) { int count; if (!content.containsKey(s))// 如果不存在這個key那麼就插入這個key,其值為1 { content.put(s, 1); } else// 修改hashmpa中的值,直接使用put覆蓋,不需要remove後再put { count = content.get(s); content.put(s, count + 1); } } br.close(); reader.close(); } catch (Exception e) { e.printStackTrace(); } } // 列印hashmap中的值 public void printHashMap(Map<String, Integer> content) { Iterator it = content.entrySet().iterator(); while (it.hasNext()) { Entry entry = (Entry) it.next(); // entry.getKey() 返回與此項對應的鍵 // entry.getValue() 返回與此項對應的值 System.out.println(entry.getKey() + " " + entry.getValue()); } } // 查詢最熱門查詢 public void findPopularQuery(Map<String, Integer> content, int k) { LinkedList<Entry<String, Integer>> list = new LinkedList<Entry<String, Integer>>();// 使用ListedList來建立最大堆 int count = 0; Iterator it = content.entrySet().iterator(); while (it.hasNext() && count < k) {// 首先將hashmap中前10個元素放入ListedList當中。 Entry entry = (Entry) it.next(); // entry.getKey(); //返回與此項對應的鍵 // entry.getValue();// 返回與此項對應的值 // System.out.println(entry.getKey()+" "+entry.getValue()); list.add(entry); count++; } //輸出list中元素 for (int i = 0; i < count; i++) { System.out.println(list.get(i).getKey() + " " + list.get(i).getValue()); } System.out.println("----------------------"); buildHeap(list, k);//構建最大堆,裡面儲存有LinkedList中的前k個元素。 int len = content.size() - 1;//hashmap中總共的元素個數。 while(it.hasNext()) { Entry<String, Integer> entry = (Entry<String, Integer>) it.next(); if(entry.getValue()>list.get(0).getValue()) { list.set(0, entry); adjustHeap(list, 0, k); } } //輸出最熱門查詢 for (int i = 0; i < count; i++) { System.out.println(list.get(i).getKey() + " " + list.get(i).getValue()); } } //構建最小堆 public void buildHeap(LinkedList<Entry<String, Integer>> list, int k) { int nonleaf = k / 2 - 1;// for (int i = nonleaf; i >= 0; i--) { adjustHeap(list, i, k); } } //調整最小堆 public void adjustHeap(LinkedList<Entry<String, Integer>> list, int parent,int k) { int left = parent * 2 + 1;// 左節點 while (left < k) { if (left + 1 < k && list.get(left).getValue() > list.get(left + 1) .getValue()) left++;// 此時left代表右節點 if (list.get(parent).getValue() <= list.get(left).getValue())//最小堆 break; else { swap(list, parent, left); parent = left; left = parent * 2 + 1; } } } // 交換LinkedList中元素值 public void swap(LinkedList<Entry<String, Integer>> list, int i, int j) { Entry temp = list.get(i); list.set(i, list.get(j)); list.set(j, temp); } }
百度面試題:將query按照出現的頻度排序(10個1G大小的檔案)。有10個檔案,每個檔案1G,每個檔案的每一行都存放的是使用者的query,每個檔案的query都可能重複。如何按照query的頻度排序?
網上給出的答案:
1)讀取10個檔案,按照hash(query)%10的結果將query寫到對應的10個檔案(file0,file1....file9)中,這樣的10個檔案不同於原先的10個檔案。這樣我們就有了10個大小約為1G的檔案。任意一個query只會出現在某個檔案中。
2)對於1)中獲得的10個檔案,分別進行如下操作
- 利用hash_map(query,query_count)來統計每個query出現的次數。
- 利用堆排序演算法對query按照出現次數進行排序。
- 將排序好的query輸出的檔案中。
這樣我們就獲得了10個檔案,每個檔案中都是按頻率排序好的query。
3)對2)中獲得的10個檔案進行歸併排序,並將最終結果輸出到檔案中。
注:如果記憶體比較小,在第1)步中可以增加檔案數。
我的答案
1)讀取10個檔案,按照hash(query)%10的結果將query寫到對應的10個檔案(file0,file1....file9)中,這樣的10個檔案不同於原先的10個檔案。這樣我們就有了10個大小約為1G的檔案。任意一個query只會出現在某個檔案中。
2)對於1)中獲得的10個檔案,分別進行如下操作
- 利用hash_map(query,query_count)來統計每個query出現的次數。
- 建立一個長度為10的堆來儲存一個檔案中出現次數最多的hash_map(query,query_count),最後將這10個鍵值對輸出到result檔案中。
3)通過2)獲得的result檔案儲存著每個檔案出現次數最多的10條記錄,對其中的100條記錄按照query_count進行排序,最後輸出query_count最大的10條query。
注:如果記憶體比較小,在第1)步中可以增加檔案數。