【问题标题】:How to count the number of occurrences of each word?如何计算每个单词的出现次数?
【发布时间】:2014-10-09 15:14:13
【问题描述】:

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

有人说你可以使用 Map () 来完成这个,但我想知道我怎么知道关键词是什么?每篇文章都有不同的词,你怎么知道“关键”词然后加一个?

【问题讨论】:

  • “关键词”是什么意思
  • 文本中的单词可能是包含键 + 计数的 HashMap 的键。例如:HashMap<String, Integer>()
  • 也许您可以使用专门的文本搜索引擎(例如 Lucene)来构建索引并获取例如 High Frequency Terms

标签: java count


【解决方案1】:

这是处理 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[] 被转换为字符串:new String(Files.readAllBytes(file), StandardCharsets.UTF_8),同时假设文件是​​ UTF8 编码的。根据自己的需要进行更改。价格是内存中已经巨大的数据的完整内存副本。使用内存映射文件可能会更快。
  3. 字符串在非单词字符处拆分:...split("\\W+"),它会创建一个包含所有单词的字符串数组。
  4. 我们从该数组创建一个流:Arrays.stream(...)。这本身并不能做很多事情,但我们可以用流做很多有趣的事情
  5. 我们将所有单词组合在一起:Collectors.groupingBy(Function.&lt;String&gt;identity(), TreeMap::new, counting())。这表示:
    • 我们希望按单词本身对单词进行分组 (identity())。我们也可以例如如果您希望分组不区分大小写,请先将此处的字符串小写。这最终将成为地图中的关键。
    • 作为存储分组值的结果,我们需要一个 TreeMap (TreeMap::new)。 TreeMaps 是按它们的键排序的,所以我们可以很容易地在最后按字母顺序输出。如果您不需要排序,也可以在此处使用 HashMap。
    • 作为每个组的值,我们希望获得每个单词的出现次数 (counting())。在后台,这意味着对于我们添加到组中的每个单词,我们都会将计数器加一。
  6. 从第 5 步开始,我们留下了一个映射,将单词映射到它们的计数。现在我们只想打印它们。因此,我们使用此映射 (.entrySet()) 中的所有键/值对访问一个集合。
  7. 最后是实际打印。我们说每个元素都应该传递给 println 方法:.forEach(System.out::println)。现在,您得到了一份不错的清单。

那么这个答案有多好?好处是它很短,因此很有表现力。它也仅与隐藏在Files.readAllBytes 后面的单个系统调用(或至少一个固定的数字,我不确定这是否真的适用于单个系统调用)一起使用,并且系统调用可能是一个瓶颈。例如。如果您正在从流中读取文件,则每次调用 read 都可能触发系统调用。这通过使用顾名思义缓冲区的 BufferedReader 显着减少。但静止的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%

请注意,我之前也使用 TreeMaps 进行了测试,但发现 HashMaps 更快,即使我之后对输出进行了排序。此外,在 Tagir Valeev 在下面的 cmets 中告诉我关于 Pattern.splitAsStream() 方法之后,我更改了上面的测试。由于我得到了差异很大的结果,我让测试运行了很长一段时间,正如您从上面以秒为单位的长度看到的那样,以获得有意义的结果。

我如何判断结果:

  1. 完全不使用流的“混合”方法,而是使用 Java 8 中引入的带有回调的“合并”方法确实提高了性能。这是我所期望的,因为经典的 get/put 方法需要在 HashMap 中查找两次密钥,而“合并”方法不再需要。

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

  3. 一般来说,流式方法很慢。这并不完全出乎意料,因为发生了相当多的方法调用,例如像Function.identity 这样毫无意义的东西。但是我没想到会有这么大的差异。

作为一个有趣的旁注,我发现混合方法是最快的阅读和理解。对“合并”的调用对我来说没有最明显的效果,但如果你知道这个方法在做什么,它对我来说似乎最易读,同时groupingBy 命令对我来说更难理解。我想有人可能会说这个groupingBy 是如此特殊且高度优化,因此使用它来提高性能是有意义的,但正如这里所展示的那样,情况并非如此。

【讨论】:

  • 使用Pattern.compile("\\W+").splitAsStream(new String(...)) 可以节省数组分配,这可能会提高解决方案的性能和/或内存占用。
  • @TagirValeev:我不知道这一点,并深入研究了这种可能性。我在很大程度上改变了我的答案以更深入地等等。
【解决方案2】:
    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()
【解决方案3】:

步骤概述:

创建一个HashMap&lt;String, Integer&gt; 一次读一个字的文件。如果它在您的HashMap 中不存在,则添加它并将分配的计数值更改为 1。如果存在,则将该值增加 1。读到文件末尾。

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

【讨论】:

    【解决方案4】:

    如果我是你,我会使用 map&lt;String, int&gt; 的一种实现方式,比如哈希图。然后,当您遍历每个单词时,如果它已经存在,只需将 int 加一,否则将其添加到地图中。最后,您可以提取所有单词,或根据特定单词查询它以获取计数。

    如果顺序对您很重要,您可以尝试SortedMap&lt;String, int&gt; 以便能够按字母顺序将它们打印出来。

    希望有帮助!

    【讨论】:

      【解决方案5】:

      其实是经典的字数算法。 这是解决方案:

      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;
      }
      

      【讨论】:

        【解决方案6】:

        这是我的解决方案:

        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;
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-04-19
          • 1970-01-01
          • 2016-06-28
          • 2018-08-25
          • 1970-01-01
          相关资源
          最近更新 更多