【问题标题】:Elegant way of counting occurrences in a java collectionjava 集合中计数出现次数的优雅方法
【发布时间】:2012-12-24 23:18:11
【问题描述】:

给定一个可能重复的对象集合,我希望得到每个对象的出现次数。我通过初始化一个空的Map,然后遍历Collection 并将对象映射到它的计数来做到这一点(每次映射已经包含对象时增加计数)。

public Map<Object, Integer> countOccurrences(Collection<Object> list) {
    Map<Object, Integer> occurrenceMap = new HashMap<Object, Integer>();
    for (Object obj : list) {
        Integer numOccurrence = occurrenceMap.get(obj);
        if (numOccurrence == null) {
            //first count
            occurrenceMap.put(obj, 1);
        } else {
            occurrenceMap.put(obj, numOccurrence++);
        }
    }
    return occurrenceMap;
}

对于计算出现次数的简单逻辑来说,这看起来太冗长了。有没有更优雅/更短的方法来做到这一点?我对完全不同的算法或允许更短代码的 Java 语言特定功能持开放态度。

【问题讨论】:

  • 计算出现次数毕竟不是那么简单,你的代码似乎是你能做的最好的。
  • 为了获得所有元素出现的完整列表,您必须遍历整个集合,我认为您的实现是一个不错的实现。
  • 是什么让你觉得这很冗长?对我来说看起来很清楚。这就是 Java 的样子。
  • @DariuszWawer 无论如何都是一次迭代,排序不会;在这里没有区别。
  • 这段代码有错误。在else 语句中应该是++numOccurrence,否则我们会用1 覆盖出现次数。

标签: java dictionary collections count


【解决方案1】:

查看Guava's Multiset。几乎正是您想要的。

不幸的是,它没有 addAll(Iterable iterable) 函数,但是在您的集合上调用 add(E e) 的简单循环就足够简单了。

编辑

我的错误,它确实有一个 addAll 方法 - 必须的,因为它实现了 Collection。

【讨论】:

  • 或者,Iterables.addAll(Collection, Iterable)
【解决方案2】:

现在让我们尝试一些 Java 8 代码:

static public Map<String, Integer> toMap(List<String> lst) {
    return lst.stream()
            .collect(HashMap<String, Integer>::new,
                    (map, str) -> {
                        if (!map.containsKey(str)) {
                            map.put(str, 1);
                        } else {
                            map.put(str, map.get(str) + 1);
                        }
                    },
                    HashMap<String, Integer>::putAll);
}
static public Map<String, Integer> toMap(List<String> lst) {
    return lst.stream().collect(Collectors.groupingBy(s -> s,
                                  Collectors.counting()));
}

我觉得这段代码更优雅。

【讨论】:

  • 也许我在搜索错误的术语,但是这个 Java 8 示例在其他地方真的很难找到,感谢发布!
  • 只是好奇您为什么提供详细方法和非详细方法?另请注意,Collectors.* 的静态导入也使这不那么冗长,并且在许多情况下不需要静态方法,因为它通常可以从现有流中删除。
  • 使用这样的累加器更加优雅:(map, str) -&gt; map.merge(str, 1, (old, one) -&gt; old+one)
  • Collectors.counting 产生一个 Long,所以返回类型应该是 Map
  • 添加到@Witbrock 的评论:如果你想要Map&lt;String, Integer&gt;,你可以使用Collectors.reducing(0, e -&gt; 1, Integer::sum) 而不是Collectors.counting()
【解决方案3】:

我知道这是一个老问题,但我在 Java 8 中找到了一种更优雅的方式来计算这些选票,希望你喜欢。

Map<String, Long> map = a.getSomeStringList()
            .stream()
            .collect(Collectors.groupingBy(
                    Function.identity(),
                    Collectors.counting())
            );

如有错误,请发表评论。

【讨论】:

    【解决方案4】:

    查看这篇文章How to count the number of occurrences of an element in a List。要计算出现次数,您可以使用int occurrences = Collections.frequency(list, obj);

    【讨论】:

    • Collections.frequency 很适合统计一些特定的对象。但是 OP 需要的是统计所有对象的频率,这使得这种方法效率很低。
    • 啊,我不知道这种方法。然而,就我而言,我想一次性计算集合中所有唯一对象的出现次数。
    • 我同意,它效率低下。这只是对本机 Java 库中用于此任务的方法的建议。我不知道有其他人能完全解决这个问题,那就用外部的吧。
    【解决方案5】:

    这里有一篇关于 Java 计数器的好文章:http://www.programcreek.com/2013/10/efficient-counter-in-java/ 虽然它更注重效率而不是优雅。

    获胜者是这样的:

    HashMap<String, int[]> intCounter = new HashMap<String, int[]>();
    for (int i = 0; i < NUM_ITERATIONS; i++) {
        for (String a : sArr) {
            int[] valueWrapper = intCounter.get(a);
    
            if (valueWrapper == null) {
                intCounter.put(a, new int[] { 1 });
            } else {
                valueWrapper[0]++;
            }
        }
    }
    

    【讨论】:

      【解决方案6】:

      对于 Java 来说并没有那么冗长。你可以使用TObjectIntHashMap:

      public <T> TObjectIntHashMap<T> countOccurrences(Iterable<T> list) {
          TObjectIntHashMap<T> counts = new TObjectIntHashMap<T>();
          for (T obj : list) counts.adjustOrPut(obj, 1, 1);
          return counts;
      }
      

      【讨论】:

      • 这比番石榴选项好吗?
      • @NimChimpsky 如果更好是指更快,是的。 MultiSet 可能更清晰。
      【解决方案7】:

      请参考以下解决方案来计算集合中的每个元素。

      对于整数值:

      List<Integer> list = new ArrayList<Integer>();
      list.add(3);
      list.add(2);
      list.add(5);
      list.add(1);
      list.add(8);
      list.add(0);
      list.add(2);
      list.add(32);
      list.add(72);
      list.add(0);
      list.add(13);
      list.add(32);
      list.add(73);
      list.add(22);
      list.add(73);
      list.add(73);
      list.add(21);
      list.add(73);
      
      HashSet<Integer> set = new HashSet<>();
      
      for (int j = 0; j < list.size(); j++) {
          set.add(list.get(j));
      }
      
      Iterator<Integer> itr = set.iterator();
      while (itr.hasNext()) {
          int a = itr.next();
          System.out.println(a + " : " + Collections.frequency(list, a));
      }
      

      输出:

      0 : 2
      32 : 2
      1 : 1
      2 : 2
      3 : 1
      5 : 1
      21 : 1
      22 : 1
      8 : 1
      72 : 1
      73 : 4
      13 : 1
      

      对于字符串值:

      List<String> stringList = new ArrayList<>();
      stringList.add("ABC");
      stringList.add("GHI");
      stringList.add("ABC");
      stringList.add("DEF");
      stringList.add("ABC");
      stringList.add("GHI");
      
      HashSet<String> setString = new HashSet<>();
      
      for (int j = 0; j < stringList.size(); j++) {
          setString.add(stringList.get(j));
      }
      
      Iterator<String> itrString = setString.iterator();
      while (itrString.hasNext()) {
          String a = itrString.next();
          System.out.println(a + " :::  " + Collections.frequency(stringList, a));
      }
      

      输出:

      ABC :::  3
      DEF :::  1
      GHI :::  2
      

      【讨论】:

        【解决方案8】:

        作为对与@NimChimpsky 讨论的回应,这里有另一种更快的方法——我试图证明这一点——使用排序集合的计数方法。根据元素的数量和“sortFactor”(参见代码),速度差异会有所不同,但对于运行环境(非调试)中的大量对象,我的方法相对于默认方法的速度提高了 20-30%。 这是两个方法的简单测试类。

        public class EltCountTest {
        
            final static int N_ELTS = 10000;
        
            static final class SampleCountedObject implements Comparable<SampleCountedObject>
            {
                int value = 0;
        
                public SampleCountedObject(int value) {
                    super();
                    this.value = value;
                }
        
                @Override
                public int compareTo(SampleCountedObject o) {
                    return (value == o.value)? 0:(value > o.value)?1:-1; // just *a* sort
                }
        
                @Override
                public int hashCode() {
                    return value;
                }
        
                @Override
                public boolean equals(Object obj) {
                    if (obj instanceof SampleCountedObject) {
                        return value == ((SampleCountedObject)obj).value;
                    }
                    return false;
                }
        
                @Override
                public String toString() {
                    return "SampleCountedObject("+value+")";
                }
            }
        
            /**
             * * @param args
             */
            public static void main(String[] args) {
                int tries = 10000;
                int sortFactor = 10;
                Map<SampleCountedObject, Integer> map1 = null;
                Map<SampleCountedObject, Integer> map2 = null;
        
                ArrayList<SampleCountedObject> objList = new ArrayList<EltCountTest.SampleCountedObject>(N_ELTS);
        
                for (int i =0, max=N_ELTS/sortFactor; i<max; i++){
                    for (int j = 0; j<sortFactor; j++) {
                        objList.add(new SampleCountedObject(i));
                    }
                }
        
                long timestart = System.nanoTime();
                for (int a=0; a< tries; a++) {
                    map1 = method1(objList);
                }
                System.out.println();
                long timeend1 = System.nanoTime();
                System.out.println();
        
                for (int a=0; a< tries; a++) {
                    map2 = metod2(objList);
                }
                long timeend2 = System.nanoTime();
                System.out.println();
        
        
                long t1 = timeend1-timestart;
                long t2 = timeend2-timeend1;
                System.out.println("\n        org count method=["+t1+"]\nsorted collection method=["+t2+"]"+
                         "\ndiff=["+Math.abs(t1-t2)+"] percent=["+(100d*t2/t1)+"]");
        
                for (SampleCountedObject obj: objList) {
                    int val1 = map1.get(obj);
                    int val2 = map2.get(obj);
                    if (val1 != val2) {
                        throw new RuntimeException("val1 != val2 for obj "+obj);
                    }
                }
                System.out.println("veryfy OK");
        
            }
        
            private static Map<SampleCountedObject, Integer> method1(ArrayList<SampleCountedObject> objList) {
                Map<SampleCountedObject, Integer> occurenceMap = new HashMap<SampleCountedObject, Integer>();
        
                for(SampleCountedObject obj: objList){
                     Integer numOccurrence = occurenceMap.get(obj);
                     if(numOccurrence == null){
                         occurenceMap.put(obj, 1);
                     } else {
                         occurenceMap.put(obj, ++numOccurrence);
                     }
                }
                return occurenceMap;
            }
        
            private static Map<SampleCountedObject, Integer> metod2(ArrayList<SampleCountedObject> objList) {
                Map<SampleCountedObject, Integer> occurenceMap = new HashMap<SampleCountedObject, Integer>();
                int count = 0;
                Collections.sort(objList);
                SampleCountedObject prevObj = objList.get(0);
        
                for(SampleCountedObject obj: objList){
                    if (!obj.equals(prevObj)) {
                        occurenceMap.put(prevObj, count);
                        count = 1;
                    } else {
                        count ++;
                    }
                    prevObj = obj;
                }
                occurenceMap.put(prevObj, count);
                return occurenceMap;
            }
        }
        

        请注意,我还验证了结果是否相同,并且是在打印测试结果后进行的。

        我发现有趣的是,在 Debug 运行中我的方法比原来的方法慢很多(10-20%,再次 - 取决于集合中元素的数量)。

        【讨论】:

        • 您正在生成一个已经排序的列表,因此您甚至看不到进行排序的真正性能影响。对于第一种方法,时间复杂度为 O(n)。你的是 O(n) 加上排序方法,可能是 O(n log n),或者像 O(n^2) 一样糟糕。基本上你所做的一切(除了排序)就是用equals()替换hashmap get()。我真的不认为这可以节省很多(它们都是 O(1) ),除非您有专门针对您的方法优化的数据(所以在现实世界中可能永远不会)
        【解决方案9】:

        commons-collections:CollectionUtils.getCardinalityMap 中有一个方法正是这样做的。

        【讨论】:

          【解决方案10】:

          我很惊讶没有人提供这种简单易读的解决方案。你可以使用Map#getOrDefault()

           public Map<Object, Integer> countOccurrences(Collection<Object> list){
                Map<Object, Integer> occurrenceMap = new HashMap<Object, Integer>();
                for(Object obj: list){
                    occurrenceMap.put(obj, occurrenceMap.getOrDefault(obj, 0) + 1);
                }
                return occurrenceMap;
           }
          

          它完全解决了您遇到的问题并消除了笨拙的if..else

          【讨论】:

            【解决方案11】:

            您可以使用来自Eclipse CollectionsBag

            Iterable<Object> iterable = Arrays.asList("1", "2", "2", "3", "3", "3");
            MutableBag<Object> counts = Bags.mutable.withAll(iterable);
            
            Assertions.assertEquals(1, counts.occurrencesOf("1"));
            Assertions.assertEquals(2, counts.occurrencesOf("2"));
            Assertions.assertEquals(3, counts.occurrencesOf("3"));
            Assertions.assertEquals(0, counts.occurrencesOf("4"));
            

            MutableBagFactory 接口上的withAll 方法接受Iterable 作为参数,并返回MutableBagMutableBag 上的方法 occurrencesOf 返回一个 int,这是一个元素的出现次数。与Map 不同,如果Bag 不包含元素,则不会返回null。相反,occurrencesOf 方法将返回 0

            MutableBag 是一个Collection,因此它有一个addAll 方法,该方法将Collection 作为参数。

            counts.addAll(Arrays.asList("4", "4", "4", "4"));
            Assertions.assertEquals(4, counts.occurrencesOf("4"));
            

            MutableBag 也有一个addAllIterable 方法,它采用Iterable

            Stream<Object> stream = Stream.of("1", "2", "3", "4");
            counts.addAllIterable(stream::iterator);
            
            Assertions.assertEquals(2, counts.occurrencesOf("1"));
            Assertions.assertEquals(3, counts.occurrencesOf("2"));
            Assertions.assertEquals(4, counts.occurrencesOf("3"));
            Assertions.assertEquals(5, counts.occurrencesOf("4"));
            

            Eclipse Collections 中的HashBag 实现由ObjectIntHashMap 支持,因此int 计数没有被装箱。在 Eclipse Collections 中有一个博客,其中包含有关 Bag 类型的更多信息,位于 here

            注意:我是 Eclipse Collections 的提交者

            【讨论】:

              【解决方案12】:

              Java 是一种冗长的语言,我认为没有更简单的方法可以实现这一点,除非使用 3rd-party 库或等待 Java 8 的 Lambda 表达式。

              【讨论】:

              • 与 ruby​​、python、perl、Scala、C# 等相比很多。
              • 谁能给我一个拒绝这个答案的理由?使用 3rd-party 库是否接受答案?仅仅因为我说Java很冗长?多有趣啊。
              • 我认为它至少是有争议的,因此会吸引这样的回应。
              猜你喜欢
              • 2019-09-21
              • 2017-03-01
              • 1970-01-01
              • 2017-03-07
              • 2017-03-08
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2017-03-24
              相关资源
              最近更新 更多