如何计算每个单词的出现次数?

时间:2014-10-09 15:14:13

标签: java count

如果我有一篇英文文章或一本英文小说,我想计算每个单词出现的次数,用Java编写的最快算法是什么?

有些人说你可以使用Map< String,Integer>()来完成这个,但我想知道我怎么知道关键词是什么?每篇文章都有不同的词汇,你怎么知道" key"单词然后在其计数上添加一个?

6 个答案:

答案 0 :(得分:7)

以下是使用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);
}

那它在做什么?

  1. 它将文本文件完全读入内存,更精确地读入字节数组:Files.readAllBytes(file)。这个方法在Java 7中出现并且允许以非常快的速度加载文件的方法,但是以文件完全在内存中的价格为代价,花费了大量内存。然而,对于速度来说,这是一个很好的评价。
  2. 将byte []转换为String:new String(Files.readAllBytes(file), StandardCharsets.UTF_8),同时假设文件是​​UTF8编码的。根据自己的需要改变。价格是内存中已经很大的数据的完整内存副本。 可能更快地使用内存映射文件。
  3. 该字符串在非Word charcaters中分割:...split("\\W+"),它会创建一个包含所有单词的字符串数组。
  4. 我们从该数组创建一个流:Arrays.stream(...)。这本身并没有做太多,但我们可以用流做很多有趣的事情
  5. 我们将所有单词组合在一起:Collectors.groupingBy(Function.<String>identity(), TreeMap::new, counting())。这意味着:
    • 我们希望通过单词本身(identity())对单词进行分组。我们也可以例如如果要将分组设置为不区分大小写,请首先在此处小写字符串。这最终将成为地图中的关键。
    • 作为存储分组值的结果,我们需要一个TreeMap(TreeMap::new)。 TreeMaps按其键排序,因此我们可以轻松地按字母顺序输出。如果你不需要排序,你也可以在这里使用HashMap。
    • 作为每个组的值,我们希望得到每个单词的出现次数(counting())。在背景中,这意味着对于我们添加到组中的每个单词,我们将计数器增加一个。
  6. 从第5步开始,我们留下了一张地图,用于将单词映射到计数中。现在我们只想打印它们。因此,我们访问此地图中所有键/值对的集合(.entrySet())。
  7. 最后实际打印。我们说应该将每个元素传递给println方法:.forEach(System.out::println)。现在你留下一个很好的清单。
  8. 这个答案有多好?好处是非常短暂,因此表现力很强。它也只是隐藏在Files.readAllBytes后面的一个系统调用(或者至少是一个固定的数字我不确定这是否真的适用于单个系统调用)并且系统调用可能是一个瓶颈。例如。如果您正在从流中读取文件,则每次读取调用都可能触发系统调用。通过使用名称为缓冲区的BufferedReader,可以显着减少这种情况。但是readAllBytes应该是最快的。这样做的代价是它消耗了大量的内存。然而,维基百科声称一本典型的英文书籍500 pages with 2,000 characters per page which mean roughly 1 Megabyte即使您使用智能手机,覆盆子或非常旧的计算机,也不应该在内存消耗方面存在问题。

    此解决方案确实涉及Java 8之前无法实现的一些优化。例如,成语map.put(word, map.get(word) + 1)需要&#34; word&#34;在地图上查找,这是一种不必要的浪费。

    但是,对于编译器来说,简单的循环可能更容易优化,并且可能会节省许多方法调用。所以我想知道并对此进行测试。我使用:

    生成了一个文件
    [ -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%
    

    请注意,我之前也使用TreeMaps进行了测试,但发现HashMaps更快,即使我之后对输出进行了排序。在Tagir Valeev在下面的评论中告诉我关于Pattern.splitAsStream()方法之后,我也改变了上述测试。由于我得到了很大的变化结果,我让测试运行了很长一段时间,因为你可以看到上面几秒钟的长度来获得有意义的结果。

    我如何判断结果:

    1. &#34;混合&#34;根本不使用流的方法,但使用&#34; merge&#34; Java 8中引入回调的方法确实提高了性能。这是我所期望的,因为经典的get / put appraoch需要在HashMap中查找两次密钥,并且不再需要使用&#34; merge&#34; -approach。

    2. 令我惊讶的是Pattern.splitAsStream() appraoch实际上比Arrays.asStream(....split())慢。我确实看了两个实现的源代码,我注意到split()调用将结果保存在一个ArrayList中,该ListList的大小为零,并根据需要放大。这需要许多复制操作,最后需要另一个复制操作将ArrayList复制到一个数组。但是&#34; splitAsStream&#34;实际上创建了一个迭代器,我认为可以根据需要进行查询,完全避免这些复制操作。我没有仔细查看将迭代器转换为流对象的所有源代码,但它似乎很慢,我不知道为什么。最后它理论上可能与CPU内存缓存有关:如果一遍又一遍地执行完全相同的代码,代码将更有可能在缓存中然后实际运行在大型函数链上,但这是一个非常疯狂的猜测我这边。它也可能是完全不同的东西。但是splitAsStream MIGHT 有更好的内存占用,也许没有,我没有对此进行分析。

    3. 流方法通常很慢。这并非完全出乎意料,因为发生了大量的方法调用,包括例如像Function.identity这样无意义的事情。但是我没想到这么大的差异。

    4. 作为一个有趣的旁注,我发现混合方法最快阅读和理解。呼叫&#34;合并&#34;对我没有最大的效果,但是如果你知道这个方法在做什么,那对我来说似乎最具可读性,同时groupingBy命令对我来说更难以理解。我想有人可能会说这个groupingBy非常特别且经过高度优化,因此将其用于性能是有意义的,但正如此处所示,情况并非如此。

答案 1 :(得分:5)

    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();

这个数字&#34;我&#34; m&#34;只有一个字

答案 2 :(得分:0)

步骤概述:

创建HashMap<String, Integer> 一次读一个单词的文件。如果它不存在于HashMap中,请添加它并更改分配给1的计数值。如果存在,请将值递增1.读取到文件末尾。

这将产生一组所有单词和每个单词的计数。

答案 3 :(得分:0)

如果我是你,我会使用map<String, int>的一个实现,就像一个hashmap。然后当你遍历每个单词时,如果它已经存在,只需将int递增1,否则将其添加到地图中。最后,您可以提取所有单词,或根据特定单词进行查询以获取计数。

如果订单对您很重要,您可以尝试SortedMap<String, int>按字母顺序排序。

希望有所帮助!

答案 4 :(得分:0)

它实际上是经典的字数统计算法。 这是解决方案:

public Map<String, Integer> wordCount(String[] strings) {

  Map<String, Integer> map = new HashMap<String, Integer>();
  int count = 0;

  for (String s:strings) {

    if (map.containsKey(s)) {
      count = map.get(s);
      map.put(s, count + 1);
    } else {
        map.put(s, 1);
    }

  }
  return map;
}

答案 5 :(得分:0)

这是我的解决方案:

Map<String, Integer> map= new HashMap();
 int count=0;
 for(int i =0;i<strings.length;i++){
   for(int j=0;j<strings.length;j++){
      if(strings[i]==strings[j])
      count++;
 }map.put(strings[i],count);
 count=0;
 }return map;