如何计算列表中元素出现的次数

234

我有一个Java的集合类ArrayList,具体如下:

ArrayList<String> animals = new ArrayList<String>();
animals.add("bat");
animals.add("owl");
animals.add("bat");
animals.add("bat");

你可以看到,animals ArrayList 包含了3个 bat 元素和一个 owl 元素。我想知道在 Collection 框架中是否有返回 bat 出现次数的 API,或者是否有其他方法来确定出现次数。

我发现谷歌的 Collection Multiset 确实有一个 API 可以返回一个元素的总出现次数。但这只适用于 JDK 1.5。我们的产品目前使用的是 JDK 1.6,所以我不能使用它。


这就是为什么你应该编程到接口而不是实现的原因之一。如果你碰巧找到了正确的集合,你需要改变类型来使用那个集合。我会在这里发布一个答案。 - OscarRyz
25个回答

424

我非常确定Collections中的静态frequency方法在这里会很有用:

int occurrences = Collections.frequency(animals, "bat");

这就是我会做的方式。我非常确定这是纯粹的jdk 1.6。


1
始终优先选择JRE中的API,这样可以避免向项目添加其他依赖。不要重复造轮子!! - Fernando.
5
它是在JDK 5中引入的(尽管没有人使用该版本,所以这并不重要)。https://docs.oracle.com/javase/8/docs/technotes/guides/collections/changes5.html - Minion Jim

161

在Java 8中:

Map<String, Long> counts =
    list.stream().collect(Collectors.groupingBy(e -> e, Collectors.counting()));

7
使用静态导入的 Function.identity() 代替 e -> e,能使代码更易读。 - Kuchi
13
为何这比Collections.frequency()更好?看起来可读性更差。 - rozina
这不是被要求的内容。它做了比必要更多的工作。 - Alex Worden
15
这可能超出了要求,但它恰好做到了我想要的(获取一个列表中不同元素到它们的计数的映射)。此外,当我搜索时,这个问题在谷歌上是排名第一的结果。 - KJP
2
@rozina 你可以一次性获取所有计数。 - atoMerz
@rozina 如果你已经有一个 Stream,就不需要先收集再使用 Collections.frequency,而是可以一次性获取所有内容。 - Andrea Bergonzo

28

使用流(Streams)的另一种Java 8解决方案:

long count = animals.stream().filter(animal -> "bat".equals(animal)).count();

Lambda可以被替换为"bat"::equals - Unmitigated

24

这表明了为什么要像Effective Java书中所描述的那样,"通过接口引用对象"是很重要的。

如果你按照实现方式来编码,在代码中使用ArrayList,当你找到一个好的“List”实现,它会计算项目数时,你将不得不更改这50个地方,并且可能会导致你的代码出错(如果只被你使用没有太大关系,但如果其他人在使用,你还会破坏他们的代码)

通过编程接口方法,你可以让这些50个地方保持不变,并将ArrayList的实现替换为"CountItemsList"(例如)或其他类。

下面是一个非常基本的示例,说明如何编写此代码。这只是一个示例,生产就绪的列表会复杂得多

import java.util.*;

public class CountItemsList<E> extends ArrayList<E> { 

    // This is private. It is not visible from outside.
    private Map<E,Integer> count = new HashMap<E,Integer>();

    // There are several entry points to this class
    // this is just to show one of them.
    public boolean add( E element  ) { 
        if( !count.containsKey( element ) ){
            count.put( element, 1 );
        } else { 
            count.put( element, count.get( element ) + 1 );
        }
        return super.add( element );
    }

    // This method belongs to CountItemList interface ( or class ) 
    // to used you have to cast.
    public int getCount( E element ) { 
        if( ! count.containsKey( element ) ) {
            return 0;
        }
        return count.get( element );
    }

    public static void main( String [] args ) { 
        List<String> animals = new CountItemsList<String>();
        animals.add("bat");
        animals.add("owl");
        animals.add("bat");
        animals.add("bat");

        System.out.println( (( CountItemsList<String> )animals).getCount( "bat" ));
    }
}

应用了面向对象编程原则:继承、多态、抽象、封装。


13
最好尝试组合而不是继承。你的实现现在被限制为ArrayList,但有时你可能需要LinkedList或其他类型的列表。你的示例应该在构造函数/工厂中接受另一个列表,并返回一个包装器。 - mP.
我完全同意你的观点。我在示例中使用继承的原因是,使用继承比组合更容易展示一个运行示例(需要实现List接口)。继承会创建最高的耦合度。 - OscarRyz
3
通过将其命名为CountItemsList,你暗示它有两个功能,即计数和列表。我认为这个类只需要一个单一的职责,即计算出现次数,这样会更简单,也不需要实现列表接口。 - flob

14

很抱歉,没有简单的方法可以完成它。不过你需要做的是创建一个映射表并使用它来计算频率。

HashMap<String,int> frequencymap = new HashMap<String,int>();
foreach(String a in animals) {
  if(frequencymap.containsKey(a)) {
    frequencymap.put(a, frequencymap.get(a)+1);
  }
  else{ frequencymap.put(a, 1); }
}

2
这真的不是一个可扩展的解决方案 - 想象一下如果 MM 的数据集有数百或数千个条目,并且 MM 想要了解每个条目的频率。这可能是一项非常昂贵的任务 - 特别是当存在更好的解决方法时。 - mP.
3
@dehmann,我认为他并不真正想知道一个4个元素的集合中蝙蝠出现的次数,我认为那只是示例数据,让我们更好地理解 :-). - paxdiablo
2
编程是关于现在正确地做事情,这样我们就不会给将来的用户或其他程序员带来头痛或不良体验。附注:你写的代码越多,出错的可能性就越大。 - mP.
2
@mP:请解释一下为什么这不是可扩展的解决方案。Ray Hidayat正在为每个标记构建频率计数,以便可以查找每个标记。有更好的解决方案吗? - stackoverflowuser2010
1
这看起来像是C#,但问题标记为 [tag:java]。 - MC Emperor
显示剩余4条评论

11

为了实现这一目标,可以采用以下几种方法:

返回单个元素出现次数的方法:

Collection Frequency

Collections.frequency(animals, "bat");

Java Stream:

过滤器

animals.stream().filter("bat"::equals).count();

只需迭代列表

public static long manually(Collection<?> c, Object o){
    int count = 0;
    for(Object e : c)
        if(e.equals(o))
            count++;
    return count;
}

创建频率图的方法:

Collectors.groupingBy

Map<String, Long> counts = 
       animals.stream()
              .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));

合并

Map<String, Long> map = new HashMap<>();
c.forEach(e -> map.merge(e, 1L, Long::sum));

手动地

Map<String, Integer> mp = new HashMap<>();
        animals.forEach(animal -> mp.compute(animal, (k, v) -> (v == null) ? 1 : v + 1));

所有方法的运行示例:

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

public class Frequency {

    public static int frequency(Collection<?> c, Object o){
        return Collections.frequency(c, o);
    }

    public static long filter(Collection<?> c, Object o){
        return c.stream().filter(o::equals).count();
    }

    public static long manually(Collection<?> c, Object o){
        int count = 0;
        for(Object e : c)
            if(e.equals(o))
                count++;
        return count;
    }

    public static Map<?, Long> mapGroupBy(Collection<?> c){
        return c.stream()
                .collect(Collectors.groupingBy(Function.identity() , Collectors.counting()));
    }

    public static Map<Object, Long> mapMerge(Collection<?> c){
        Map<Object, Long> map = new HashMap<>();
        c.forEach(e -> map.merge(e, 1L, Long::sum));
        return map;
    }

    public static Map<Object, Long> manualMap(Collection<?> c){
        Map<Object, Long> map = new HashMap<>();
        c.forEach(e -> map.compute(e, (k, v) -> (v == null) ? 1 : v + 1));
        return map;
    }


    public static void main(String[] args){
        List<String> animals = new ArrayList<>();
        animals.add("bat");
        animals.add("owl");
        animals.add("bat");
        animals.add("bat");

        System.out.println(frequency(animals, "bat"));
        System.out.println(filter(animals,"bat"));
        System.out.println(manually(animals,"bat"));
        mapGroupBy(animals).forEach((k, v) -> System.out.println(k + " -> "+v));
        mapMerge(animals).forEach((k, v) -> System.out.println(k + " -> "+v));
        manualMap(animals).forEach((k, v) -> System.out.println(k + " -> "+v));
    }
}

方法的名称应该反映出这些方法正在做什么,但是我使用名称来反映所使用的方法(鉴于当前环境可以接受)。


11

Java中没有原生方法来为您执行此操作。 但是,您可以使用Apache Commons-Collections中的IterableUtils#countMatches()来为您执行此操作。


1
请参考下面的答案-正确的方法是使用一个结构来支持从一开始就进行计数的想法,而不是每次查询时都从头到尾计算条目数。 - mP.
-1 是因为你输不起 :-) 我认为 mP 给你的解决方案投了反对票,因为每次想要结果时都需要花费时间。插入一个袋子只需要一点时间。就像数据库一样,这些类型的结构往往是“读多写少”,因此使用低成本选项是有意义的。 - paxdiablo
而且看起来你的答案也需要非本地的东西,所以你的评论似乎有点奇怪。 - paxdiablo
谢谢你们两个。我相信这两种方法中的一种或者两种都可能会起作用。我明天会试一下。 - MM.
@Pax 我可以想象这样一种情况,他不是 Collection 的所有者/创建者,而只是一个用户。在这种情况下,他将无法使用 Bag。无论如何,正确的答案是:使用 Commons Collections :) - Kevin
显示剩余8条评论

10

使用Java 8功能查找数组中字符串值的出现方式的简单方法。

public void checkDuplicateOccurance() {
        List<String> duplicateList = new ArrayList<String>();
        duplicateList.add("Cat");
        duplicateList.add("Dog");
        duplicateList.add("Cat");
        duplicateList.add("cow");
        duplicateList.add("Cow");
        duplicateList.add("Goat");          
        Map<String, Long> couterMap = duplicateList.stream().collect(Collectors.groupingBy(e -> e.toString(),Collectors.counting()));
        System.out.println(couterMap);
    }

输出:{Cat=2, Goat=1, Cow=1, cow=1, Dog=1}

您可以注意到"Cow"和"cow"被认为是不同的字符串,如果需要将其视为相同计数,请使用.toLowerCase()。请在下面找到相应的代码段。

Map<String, Long> couterMap = duplicateList.stream().collect(Collectors.groupingBy(e -> e.toString().toLowerCase(),Collectors.counting()));

输出:{猫=2,牛=2,山羊=1,狗=1}


注意:由于列表是字符串列表,因此toString()是不必要的。您可以这样做:duplicateList.stream().collect(Collectors.groupingBy(e -> e,Collectors.counting())); - Tad

8

实际上,Collections类有一个名为frequency的静态方法(Collection c, Object o),它返回你要搜索的元素出现的次数,顺便说一下,这对你来说非常完美:

ArrayList<String> animals = new ArrayList<String>();
animals.add("bat");
animals.add("owl");
animals.add("bat");
animals.add("bat");
System.out.println("Freq of bat: "+Collections.frequency(animals, "bat"));

38
Lars Andren在你之前5年发布了同样的答案。 - Fabian Barney

8

我想知道为什么你不能在JDK 1.6中使用Google的Collection API。它有这样的说明吗?我认为可以使用,因为它是为更低版本构建的,所以不应存在任何兼容性问题。如果它是为1.6构建的,而你却在运行1.5,那情况就会不同。

我哪里错了吗?


他们明确表示,他们正在升级他们的API到JDK 1.6。 - MM.
1
这并不会使老旧版本不兼容,对吗? - Adeel Ansari
不应该。但是他们放置免责声明的方式让我感到不舒服,因此我不想在他们的0.9版本中使用它。 - MM.
我们使用它与1.6版本。哪里说它只兼容1.5版本? - Patrick
3
他们所说的“升级到1.6”可能是指“升级以利用1.6中的新功能”,而不是“修复与1.6的兼容性问题”。 - Adam Jaskiewicz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接