题目
给定一个很大的文件(1T?10T),里面每一行存储着一个用户的ID(IP?IQ?),你的电脑只有2G内存,请找出其中出现频率最高的十个ID
介绍
TopK问题是近年来实战考的最多最多最多的问题了
其实答案也比较简单,对于
- 单机
- 内存有限
- 文件过大
这样的环境,使用以下的思路进行解决就行了
- 按行读取大文件
- Hash分文件
- 读取单个小文件
- 使用map计数
- 维护小顶堆
创建用例
首先自己搞一些用例用来测试
这儿是创建了十万条字符串,因为这样数据量不多不少,再多运行时间有点长
生成的文件大小如图
生成的数据如图
按行读取和分文件
这一段的代码应该没什么问题?
逻辑为
- 按行读取
- 每1000行打印一下进程
- 根据Hashcode值投桶
- 以追加模式写入到指定的文件
这样,理论情况下,我们就能得到1000个包含着相同hashcode取模值的字符串
也就是说,所有相同的字符串也在一个文件中
计数
单个文件是系统可以存下的,那么久将这些字符串计数,将键值放入到一个hashmap中即可
维护小顶堆
一次遍历完成之后,就可以将计数放入小顶堆中,这样只用维护一个k大小的小顶堆,就能完成排序
输出
最后输出小顶堆中的内容即可
更优解
如果可以多机就可以使用hadoop,将文件拆分成多个,map单独计数,reduce时统一计数,然后还是使用小顶堆也可以求出topk
但是面试官一般会不允许这样操作XD
附录
TopK用例生成
import java.io.*;
import java.util.*;
class TopKUseCase {
public static void main(String[] args) throws IOException {
try {
final int divNum = 1000;
int fileSize =100000;
File outputfile = new File("d:\\bigdata.txt");
StringBuilder stringBuilder = new StringBuilder();
PrintWriter output = new PrintWriter(outputfile);
for (int i = 0; i < fileSize; i++) {
stringBuilder.append("user" + (int) (Math.random() * 10000) + "who" + System.lineSeparator());
}
if (!outputfile.exists())
outputfile.createNewFile();
output = new PrintWriter(outputfile);
output.println(stringBuilder);
} catch (Exception e) {
e.printStackTrace();
}
}
}
TopK代码
import java.awt.*;
import java.io.*;
import java.util.*;
class TopK {
public static void main(String[] args) throws IOException {
final int divNum = 1000; // 分成多少个文件提前按照给定的条件确定
// 按行读取大文件
File inputFile = new File("d:\\bigdata.txt");
FileInputStream inputStream = new FileInputStream(inputFile);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String str = null;
// 输出的小文件
File outputfile;
BufferedWriter output;
// 计数用来打印日志
int times = 0;
System.out.println("分文件开始");
// 创建目录
File menu = new File("D:\\div");
if (!menu.exists())
menu.mkdirs();
// 循环读取大文件
while ((str = bufferedReader.readLine()) != null) {
times++;
if (times % 1000 == 0) {
System.out.println(times + "次了" + str);
}
// 根据Hashcode值看到底投到哪个桶
int order = str.hashCode() & divNum;
outputfile = new File("D:\\div\\d" + order + "file.txt");
if (!outputfile.exists())
outputfile.createNewFile();
// 注意此处第二个参数要设置为true,表示添加模式
output = new BufferedWriter(new FileWriter(outputfile, true));
output.write(str);
output.newLine();
output.flush();
}
System.out.println("分文件完毕");
// 创建好小顶堆和hash表
String[] strArray;
Map<String, Integer> map;
Queue<Map.Entry<String, Integer>> queue = new PriorityQueue<>(10, new Comparator<Map.Entry<String, Integer>>() {
@Override
public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
return o1.getValue() - o2.getValue();
}
});
// 对每一个存在的小文件进行遍历
for (int i = 0; i <= divNum; i++) {
System.out.println("第" + i + "个");
inputFile = new File("D:\\div\\d" + i + "file.txt");
if (!inputFile.exists())
continue;
// 一次读取整个文件 这儿是为了方便 如果实在内存不足就按行读取
str = readToString(inputFile);
strArray = str.split(System.lineSeparator());
map = new HashMap<>();
// 使用hash表计数
for (String string : strArray) {
try {
map.put(string, map.get(string) + 1);
} catch (Exception e) {
map.put(string, 1);
}
}
// 维护小顶堆
for (Map.Entry<String, Integer> entry : map.entrySet()) {
if (queue.size() < 10) {
queue.add(entry);
continue;
}
if (entry.getValue() > queue.peek().getValue()) {
queue.poll();
queue.add(entry);
}
}
}
// 输出小顶堆
Iterator<Map.Entry<String, Integer>> iterator = queue.iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
System.out.println(entry.getKey() + " " + entry.getValue());
}
}
// 一次读取全部文件
public static String readToString(File file) {
Long filelength = file.length(); //获取文件长度
byte[] filecontent = new byte[filelength.intValue()];
try {
FileInputStream in = new FileInputStream(file);
in.read(filecontent);
in.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return new String(filecontent);//返回文件内容,默认编码
}
}