问题
问题:有一个 1G 大小的文件,里面每一行是一个词,每个词的大小不超过 16 字节,内存限制大小是 1M。返回出现频率最高的 100 个单词
解法一:
第一步:将 1 G 拆分成小文件,每个小文件的大小不超过 1 MB ,其实应该留空间用于统计一个小文件中单词出现的频率,所以为了保险起见,每个文件的大小不超过 512 kb,那么得到 1 GB / 512 KB = 2048 个小文件
这里不用 hash 的方法来切分小文件,直接顺序读取然后写到小文件,小文件大小多到了 512kb 就关闭,写下一个小文件
第二步:分别对每个文件做下面的事情: ① 将小文件中的单词加载进内存 ② 使用 HashMap 进行单词统计 ③ 将 HashMap 中词频数据写到另一个新的小文件中,我们称为词频小文件 这里再写的时候,对单词进行 hash (word) % 2048 写到对应的文件中 这样做的目的是为了相同的单词放到同一个文件中。
第三步:初始化一个 100 个节点的小顶堆,用于保存 100 个出现频率最多的单词 分别对每个词频小文件做下面的事情: ① 将小文件中的单词及其频率加载进内存 ② 使用 HashMap 进行单词统计 ③ 遍历 HashMap 中单词词频,如果词频大于小顶堆中堆顶词频的话,则放入小顶堆,否则不放
最终,小顶堆中的 100 个单词就是出现频率最多的单词了
该方案在第二步的第 ③ 点的时候有点问题:就是当词频小文件大于 1 M 了,该怎么处理呢? 或者说极端情况下,每个单词都只出现一次,并且每个单词的 hash (word) % 2048 值都是相同的话,那词频小文件的大小都会超过 1 G 了
解法二:
第一步:使用多路归并排序对大文件进行排序,这样相同的单词肯定是挨着的
第二步: ① 初始化一个 100 个节点的小顶堆,用于保存 100 个出现频率最多的单词 ② 遍历整个文件,一个单词一个单词的从文件中取出来,并计数 ③ 等到遍历的单词和上一个单词不同的话,那么上一个单词及其频率如果大于堆顶的词的频率,那么放在堆中,否则不放
最终,小顶堆中就是出现频率前 100 的单词了
多路归并排序对大文件进行排序的步骤如下: ① 将文件按照顺序切分成大小不超过 512KB 的小文件,总共 2048 个小文件 ② 使用 1MB 内存分别对 2048 个小文件中的单词进行排序 ③ 使用一个大小为 2048 大小的堆,对 2048 个小文件进行多路排序,结果写到一个大文件中
代码实现
一:数据准备和工具类
package com.douma.practical.mult_line_merging;
import java.io.*;
public class FileIOUtils {
public static BufferedReader getReader(String name) {
try {
FileInputStream inputStream = new FileInputStream(name);
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
return br;
} catch (IOException e) {
throw new RuntimeException("IOException", e);
}
}
public static BufferedWriter getWriter(String name) {
try {
FileOutputStream outputStream = new FileOutputStream(name);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(outputStream));
return bw;
} catch (IOException e) {
throw new RuntimeException("IOException", e);
}
}
public static void closeReader(Reader reader) {
try {
if (reader != null) {
reader.close();
}
} catch (IOException e) {
throw new RuntimeException("IOException", e);
}
}
public static void closeWriter(Writer writer) {
try {
if (writer != null) {
writer.close();
}
} catch (IOException e) {
throw new RuntimeException("IOException", e);
}
}
}
package com.douma.practical.mult_line_merging;
import java.io.*;
import java.util.Random;
public class _0_WordsGenerator {
private static Random r = new Random();
public static void main(String[] args) throws IOException {
BufferedWriter writer = FileIOUtils.getWriter("data\\top100\\words.txt");
char[] chars = {'a', 'b', 'c', 'd', 'e', 'f', 'g'};
int m = chars.length;
for (int i = 0; i < 10000; i++) {
StringBuilder line = new StringBuilder();
for (int j = 0; j < r.nextInt(16); j++) {
line.append(chars[r.nextInt(m)]);
}
if (line.length() == 0) continue;
writer.write(line.toString());
writer.newLine();
}
FileIOUtils.closeWriter(writer);
}
}
二:将一个大文件切割成多个小文件
package com.douma.practical.mult_line_merging;
import java.io.*;
public class _1_FileSplit {
public void splitFile(String fileName) throws IOException {
int fileNum = 0;
String fileSuffix = "data\\top100\\raw_data\\";
String littleFileName = fileSuffix + fileNum;
long totalSize = 0;
BufferedWriter bw = FileIOUtils.getWriter(littleFileName);
BufferedReader br = FileIOUtils.getReader(fileName);
String line = null;
while ((line = br.readLine()) != null) {
if (totalSize >= 512 * 1024) {
FileIOUtils.closeWriter(bw);
fileNum++;
littleFileName = fileSuffix + fileNum;
bw = FileIOUtils.getWriter(littleFileName);
totalSize = 0;
}
totalSize += line.length();
bw.write(line);
bw.newLine();
}
FileIOUtils.closeReader(br);
}
public static void main(String[] args) throws IOException {
String fileName = "data\\top100\\words.txt";
new _1_FileSplit().splitFile(fileName);
}
}
三:将每个小文件中的单词进行排序
package com.douma.practical.mult_line_merging;
import java.io.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class _2_LittleFileSorter {
public void sortEachFile(String dirName) throws IOException {
File dir = new File(dirName);
File[] littleFiles = dir.listFiles();
for (int i = 0; i < littleFiles.length; i++) {
BufferedReader br = FileIOUtils.getReader(littleFiles[i].getName());
List<String> words = new ArrayList<>();
String line = null;
while ((line = br.readLine()) != null) {
words.add(line);
}
FileIOUtils.closeReader(br);
Collections.sort(words);
BufferedWriter bw = FileIOUtils.getWriter("data\\top100\\sorted_data\\" + i);
for (String word : words) {
bw.write(word);
bw.newLine();
}
FileIOUtils.closeWriter(bw);
}
}
public static void main(String[] args) throws IOException {
String dir = "data\\top100\\raw_data";
new _2_LittleFileSorter().sortEachFile(dir);
}
}
四:对有序的小文件进行外部排序
我们先写一个 BufferedIterator 类,如下:
package com.douma.practical.mult_line_merging;
import java.io.BufferedReader;
import java.io.IOException;
public class BufferedIterator {
private BufferedReader reader;
private String head;
BufferedIterator(BufferedReader reader) {
this.reader = reader;
}
public boolean hasNext() {
try {
head = this.reader.readLine();
} catch (IOException e) {
e.printStackTrace();
head = null;
}
return head != null;
}
public String next() {
return head;
}
public void close() throws Exception {
this.reader.close();
}
}
然后使用多路归并排序来实现文件的外部排序,如下代码:
package com.douma.practical.mult_line_merging;
import java.io.*;
import java.util.Comparator;
import java.util.PriorityQueue;
public class _3_ExternalSorter {
public void mergeSort(String dirName) throws Exception {
File dir = new File(dirName);
File[] children = dir.listFiles();
PriorityQueue<BufferedIterator> minHeap = new PriorityQueue<>(children.length, new Comparator<BufferedIterator>() {
@Override
public int compare(BufferedIterator o1, BufferedIterator o2) {
return o1.next().compareTo(o2.next());
}
});
for (File file : children) {
BufferedReader br = FileIOUtils.getReader(file.getName());
BufferedIterator buf = new BufferedIterator(br);
if (buf.hasNext()) {
minHeap.add(buf);
} else {
buf.close();
}
}
BufferedWriter bw = FileIOUtils.getWriter("data\\top100\\sorted_words.txt");
while (!minHeap.isEmpty()) {
BufferedIterator firstBuf = minHeap.poll();
bw.write(firstBuf.next());
bw.newLine();
if (firstBuf.hasNext()) {
minHeap.add(firstBuf);
} else {
firstBuf.close();
}
}
FileIOUtils.closeWriter(bw);
}
public static void main(String[] args) throws Exception {
String dirName = "data\\top100\\sorted_data\\";
new _3_ExternalSorter().mergeSort(dirName);
}
}
五:利用 100 大小的小顶堆得到出现频率 top 100 的单词
package com.douma.practical.mult_line_merging;
import java.io.BufferedReader;
import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;
public class _4_Top_100_Words {
class Pair {
String word;
int cnt;
Pair(String word, int cnt) {
this.word = word;
this.cnt = cnt;
}
}
public String[] top_100 (String fileName) throws Exception {
PriorityQueue<Pair> minHeap = new PriorityQueue<>(100, new Comparator<Pair>() {
@Override
public int compare(Pair o1, Pair o2) {
return o1.cnt - o2.cnt;
}
});
String prevWord = null;
int prevCnt = 0;
BufferedReader br = FileIOUtils.getReader(fileName);
String currWord = null;
while ((currWord = br.readLine()) != null) {
if (!currWord.equals(prevWord)) {
if (minHeap.size() < 100) {
minHeap.add(new Pair(prevWord, prevCnt));
} else if (prevCnt > minHeap.peek().cnt) {
minHeap.remove();
minHeap.add(new Pair(prevWord, prevCnt));
}
prevWord = currWord;
prevCnt = 0;
}
prevCnt++;
}
String[] res = new String[100];
int index = 0;
while (!minHeap.isEmpty()) {
res[index++] = minHeap.poll().word;
}
return res;
}
public static void main(String[] args) throws Exception {
String fileName = "data\\top100\\sorted_words.txt";
String[] res = new _4_Top_100_Words().top_100(fileName);
System.out.println(Arrays.toString(res));
}
}
|