搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为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)步中可以增加文件数。