搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为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,遍历hashmap,如果MaxHeap未满,那么往MaxHeap中插入这个键值对,如果MinHeap满了,则比较遍历到的元素的count值堆顶的count,如果遍历到元素的count大于堆顶count值,删除堆顶元素,插入当前遍历到的元素。
遍历完整个hashmap以后,在MaxHeap中存储的就是最热门10个查询串。
百度面试题:将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)步中可以增加文件数。
统计外站的搜索关键词的词频
通过外站的链接主要是百度,谷歌,soso等,每天都有通过记录在日志文件中,每天会运行程序进行统计。
每天产生有10多个文件,每个文件1G左右, 每个文件的每一行都存放的是用户的query,每个文件的query都可能重复。要按照解析query中的关键词,并对统计其频度,取出搜索次数最多的前1000个关键词。
第一次直接遍历所有文件并按照Map<String,Integer>方式来统计,统计差不多共有四千万条记录,词也有一百万多个,最后排序实现。方法简单,但也有很大的缺点,占用的内存太大,可能会将服务器弄垮掉。
/**
* 初始写文件流
*/
public void init() {
if (numFiles < 1 && numFiles > 1000) {
throw new RuntimeException("中间保存搜索关键词的文件数目不能小于1或大于1000");
}
outs = new BufferedWriter[numFiles];
outFiles = new File[numFiles];
File tempDir = new File("temp");
if (!tempDir.exists()) {
tempDir.mkdir();
}
for (int i = 0; i < numFiles; i++) {
try {
outFiles[i] = new File("temp/" + String.valueOf(i));
outFiles[i].createNewFile();
outs[i] = new BufferedWriter(new FileWriter(outFiles[i]));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
先不统计,利用外存处理,将分析出来的每一个关键词,取出第一个字符的哈希值%500,分别放进500个临时文件中,然后才对每个文件进行统计。
/**
* 记录关键词到相应的文件
*
* @param keyword
*/
public void addKeyword(String keyword) {
// 写入不同的外存(文件)
if (keyword.length() < 1)
return;
String firstStr = keyword.substring(0, 1);
int index = firstStr.hashCode() % numFiles;
index = Math.abs(index);
try {
outs[index].write(keyword + lineSep);
} catch (IOException e) {
logger.error("", e);
}
}
由于只取前1000个关键词,所以每统计完一个文件后,采用堆排序,将前1000名的关键词保存在一个堆里,这样内存中只维护一份堆,可以减少许多内存的消耗,只占用到之前的五分之一以下。速度较之前也提高不少。
public List<Keyword> sortKeywordMap(int size) {
final KeywordQueue queue = new KeywordQueue(size);
try {
TObjectIntHashMap<String> key2numMap = new TObjectIntHashMap<String>();
// 统计所有文件
for (int i = 0; i < numFiles; i++) {
// 对单个文件统计
LineIterator lines = FileUtils.lineIterator(outFiles[i]);
while (lines.hasNext()) {
String line = lines.nextLine();
int num = key2numMap.get(line);
num++;
key2numMap.put(line, num);
}
lines.close();
// 将统计完的map中的所有关键词放入最小堆中
key2numMap.forEachEntry(new TObjectIntProcedure<String>() {
@Override
public boolean execute(String key, int num) {
Keyword k = new Keyword(key, num);
queue.insertWithOverflow(k);
return true;
}
});
// 进行清除
key2numMap.clear();
}
} catch (Exception e) {
logger.error("", e);
}
List<Keyword> list = new ArrayList<Keyword>();
for (int i = queue.size() - 1; i >= 0; i--) {
list.add(queue.pop());
}
Collections.reverse(list);
return list;
}