如果我有英文文章或英文小说文章,并且想要计算每个单词出现的次数,用Java编写的最快算法是什么?如何统计每个单词的出现次数?
有人说你可以用Map < String,Integer>()来完成这个,但我想知道怎么知道关键词是什么?每篇文章都有不同的单词,你如何知道“关键”单词,然后在其数量上添加一个单词?
如果我有英文文章或英文小说文章,并且想要计算每个单词出现的次数,用Java编写的最快算法是什么?如何统计每个单词的出现次数?
有人说你可以用Map < String,Integer>()来完成这个,但我想知道怎么知道关键词是什么?每篇文章都有不同的单词,你如何知道“关键”单词,然后在其数量上添加一个单词?
Map<String, Integer> countByWords = new HashMap<String, Integer>();
Scanner s = new Scanner(new File("your_file_path"));
while (s.hasNext()) {
String next = s.next();
Integer count = countByWords.get(next);
if (count != null) {
countByWords.put(next, count + 1);
} else {
countByWords.put(next, 1);
}
}
s.close();
这个数“我”因为只有一个字
如果您使用'entrySet()'来更改您已经放入集合的单词的计数,它会(稍微)更快吗?我预计地图在已经包含它的情况下会看起来是'next'的三倍(1:'contains()',2:'get()',3:'put()') – tgmath 2014-10-09 15:42:07
的步骤总体概述:
创建HashMap<String, Integer>
阅读文件一个字的时间。如果它不存在于您的HashMap
中,请添加它并将计数值更改为1.如果存在,请将该值增加1.读取直至文件结束。
这将导致一组所有单词和每个单词的计数。
如果我是你,我会使用map<String, int>
的一个实现,如散列图。然后,如果已经存在每个单词,则循环遍历整个单词,否则将其添加到地图中。最后,您可以提取所有单词,或者根据特定单词查询该单词以获得计数。
如果订单对您很重要,您可以尝试SortedMap<String, int>
以便按字母顺序排列。
希望有帮助!
这里是另一种方式与出现在Java中8要做的事情是:
private void countWords(final Path file) throws IOException {
Arrays.stream(new String(Files.readAllBytes(file), StandardCharsets.UTF_8).split("\\W+"))
.collect(Collectors.groupingBy(Function.<String>identity(), TreeMap::new, counting())).entrySet()
.forEach(System.out::println);
}
那么是什么做?
Files.readAllBytes(file)
。这种方法在Java 7中出现,并且允许非常快速地加载文件的方法,但是对于文件将完全存储在内存中的代价,会花费大量内存。对于速度而言,这是一个很好的评价。new String(Files.readAllBytes(file), StandardCharsets.UTF_8)
,同时假定该文件是UTF8编码的。根据自己的需要进行更改。价格是内存中已有巨大数据的完整内存拷贝。它可能改为使用内存映射文件。...split("\\W+")
,它会创建一个包含所有单词的字符串数组。Arrays.stream(...)
。这本身并不是很多,但我们可以用流Collectors.groupingBy(Function.<String>identity(), TreeMap::new, counting())
。这意味着:
identity()
)。我们也可以例如如果您希望分组不区分大小写,请首先在此处输入字符串。这将最终成为地图中的关键。TreeMap::new
)。TreeMaps按键排序,因此我们可以按字母顺序轻松输出。如果你不需要排序,你也可以在这里使用HashMap。counting()
)。在背景中,这意味着对于我们添加到组中的每个单词,我们增加一个计数器。.entrySet()
)。.forEach(System.out::println)
。现在你留下了一个不错的名单。那么这个答案有多好?好处是非常短暂,因此具有很强的表现力。它也只与一个隐藏在Files.readAllBytes
之后的系统调用相关(或者至少是一个固定的数字,我不确定这是否真的可以在单个系统调用中工作),系统调用可能是一个瓶颈。例如。如果您正在从流中读取文件,则每个要读取的呼叫都可能触发系统调用。这通过使用名称为缓冲区的BufferedReader显着减少。但stilly readAllBytes
应该是最快的。这样做的代价是它消耗了大量的内存。然而,维基百科宣称,一本典型的英文书籍有500 pages with 2,000 characters per page which mean roughly 1 Megabyte,即使您使用的是智能手机,树莓派或真正旧的计算机,在内存消耗方面也不应该存在问题。
这个解决方案确实涉及到一些在Java 8之前不可能的优化。例如,成语map.put(word, map.get(word) + 1)
要求在地图中查找“单词”,这是一种不必要的浪费。
但是,一个简单的循环可能更容易优化编译器,并可能节省大量的方法调用。所以我想知道,并把这个测试。我生成使用文件:
[ -f /tmp/random.txt ] && rm /tmp/random.txt; for i in {1..15}; do head -n 10000 /usr/share/dict/american-english >> /tmp/random.txt; done; perl -MList::Util -e 'print List::Util::shuffle <>' /tmp/random.txt > /tmp/random.tmp; mv /tmp/random.tmp /tmp/random.txt
这给了我大约1,3MB文件,所以不就是非典型的一本书大部分的话被重复15次,但在随机为了规避这个目的达到成为分支预测测试。然后,我跑到了以下测试:
public class WordCountTest {
@Test(dataProvider = "provide_description_testMethod")
public void test(String description, TestMethod testMethod) throws Exception {
long start = System.currentTimeMillis();
for (int i = 0; i < 100_000; i++) {
testMethod.run();
}
System.out.println(description + " took " + (System.currentTimeMillis() - start)/1000d + "s");
}
@DataProvider
public Object[][] provide_description_testMethod() {
Path path = Paths.get("/tmp/random.txt");
return new Object[][]{
{"classic", (TestMethod)() -> countWordsClassic(path)},
{"mixed", (TestMethod)() -> countWordsMixed(path)},
{"mixed2", (TestMethod)() -> countWordsMixed2(path)},
{"stream", (TestMethod)() -> countWordsStream(path)},
{"stream2", (TestMethod)() -> countWordsStream2(path)},
};
}
private void countWordsClassic(final Path path) throws IOException {
final Map<String, Integer> wordCounts = new HashMap<>();
for (String word : new String(readAllBytes(path), StandardCharsets.UTF_8).split("\\W+")) {
Integer oldCount = wordCounts.get(word);
if (oldCount == null) {
wordCounts.put(word, 1);
} else {
wordCounts.put(word, oldCount + 1);
}
}
}
private void countWordsMixed(final Path path) throws IOException {
final Map<String, Integer> wordCounts = new HashMap<>();
for (String word : new String(readAllBytes(path), StandardCharsets.UTF_8).split("\\W+")) {
wordCounts.merge(word, 1, (key, oldCount) -> oldCount + 1);
}
}
private void countWordsMixed2(final Path path) throws IOException {
final Map<String, Integer> wordCounts = new HashMap<>();
Pattern.compile("\\W+")
.splitAsStream(new String(readAllBytes(path), StandardCharsets.UTF_8))
.forEach(word -> wordCounts.merge(word, 1, (key, oldCount) -> oldCount + 1));
}
private void countWordsStream2(final Path tmpFile) throws IOException {
Pattern.compile("\\W+").splitAsStream(new String(readAllBytes(tmpFile), StandardCharsets.UTF_8))
.collect(Collectors.groupingBy(Function.<String>identity(), HashMap::new, counting()));
}
private void countWordsStream(final Path tmpFile) throws IOException {
Arrays.stream(new String(readAllBytes(tmpFile), StandardCharsets.UTF_8).split("\\W+"))
.collect(Collectors.groupingBy(Function.<String>identity(), HashMap::new, counting()));
}
interface TestMethod {
void run() throws Exception;
}
}
结果是:
type length diff
classic 4665s +9%
mixed 4273s +0%
mixed2 4833s +13%
stream 4868s +14%
stream2 5070s +19%
注意,我以前也有树状图进行测试,却发现包含HashMap是要快得多,即使我排序的输出之后。在Tagir Valeev在下面关于Pattern.splitAsStream()
方法的评论中告诉我时,我还更改了上述测试。由于我得到的结果差异很大,因此我将测试运行了很长一段时间,正如您可以看到上面的秒数,以获得有意义的结果。
我怎么判断的结果:
“混合”的做法,完全不使用流,但使用在Java 8中引入并改进性能回调“合并”的方法。这是我期望的,因为经典的get/put appraoch需要在HashMap中查找两次密钥,并且这对于“合并”方法不再需要。
令我惊讶的是Pattern.splitAsStream()
appraoch实际上比Arrays.asStream(....split())
慢。我看过两种实现的源代码,我注意到调用split()
将结果保存在一个ArrayList中,该ArrayList的大小为零,并根据需要进行放大。这需要许多复制操作,最后需要另一个复制操作来将ArrayList复制到数组中。但是“splitAsStream”实际上创建了一个我认为可以根据需要查询的迭代器,完全避免了这些复制操作。我没有仔细查看将迭代器转换为流对象的所有源代码,但它看起来很慢,我不知道为什么。最后,它理论上可能与CPU内存缓存有关:如果完全相同的代码一遍又一遍地执行,代码将更可能在缓存中,然后实际上在大型函数链上运行,但这是一个非常疯狂的猜测我这边。它也可能是完全不同的东西。然而splitAsStream
可能有更好的内存占用,也许它没有,我没有分析。
一般流的方法是相当缓慢的。这并不完全出乎意料,因为相当多的方法调用发生,包括例如像Function.identity
那样毫无意义的事情。不过,我并没有预料到在这么大的差别。
作为一个有趣的方面说明,我发现最快的混合方法阅读和理解。对“合并”的调用对我来说并不是最具影响力的,但如果你知道这个方法正在做什么,它似乎对我来说是最易读的,同时groupingBy
命令对我来说更难以理解。我想有人可能会说,这个groupingBy
是如此特别和高度优化,它是有道理的使用它的性能,但如这里演示,情况并非如此。
使用'Pattern.compile \\ W +“)。splitAsStream(new String(...))'您将保存一个数组分配,这可能会提高解决方案的性能和/或内存占用。 – 2015-11-27 03:52:41
@TagirValeev:我不知道这一点,并深入研究了这种可能性。我在很大程度上改变了我的答案,深入了解更多内容。 – yankee 2015-11-29 15:14:19
你是什么意思与“关键”单词 – 2014-10-09 15:19:02
您的文本中的单词可能是包含键+计数的HashMap的关键。例如:'HashMap()' –
mreiterer
2014-10-09 15:19:16
也许你可以使用专门的文本搜索引擎,如[Lucene](http://lucene.apache.org/core/)来构建索引并获取例如[高频术语](https://lucene.apache.org/core/4_0_0/misc/org/apache/lucene/misc/HighFreqTerms.html)。 – 2014-10-09 15:19:53