【问题标题】:Java 8 Stream, How to get Top N count? [closed]Java 8 Stream,如何获得前 N 个计数? [关闭]
【发布时间】:2019-02-19 22:29:25
【问题描述】:

我需要您的建议来简化下面的代码。我有一个玩家列表,其中包含获胜游戏的 ID。我想从这个列表中提取 2 个最好的球员(比赛 ID 数量较多的 2 个球员) 提取后,我必须返回初始列表以执行其他操作。 我认为可以在优化或阅读方面改进此代码。如果你能帮助我。

public class PlayerStatistics {
    int id
    String name;
    int idMatchWon; // key from Match

    // getter , setter
}

public static void main(String[] args) throws Exception {

    List<PlayerStatistics> _players = new ArrayList<PlayerStatistics>();

    _players.add(initialize(1,'John',4));
    _players.add(initialize(2,'Teddy',2));
    _players.add(initialize(3,'Kevin',3));

    // How to get Top 2
    List<PlayerStatistics> _top2Players = extractTop2Players(_players);
}

private List<PlayerStatistics> extractTop2Players (List<PlayerStatistics> _list) {

    List<PlayerStatistics> _topPlayers = new ArrayList<PlayerStatistics>();

    // 1. Group and count 
    Map<String, Long> _players = _list
            .stream()
            .filter(x -> (!"".equals(x.getName()) && x.getName()!= null) )
            .collect(
                    Collectors.groupingBy(
                            PlayerStatistics::getName, Collectors.counting()
                    )
            );
    ;

    // 2 Best Palyers
    Set<String> _sortedPlayers = _players.entrySet().stream()
            .sorted(Map.Entry.comparingByValue(Collections.reverseOrder()))
            .limit(2)
            .map(Entry::getKey)
            .collect(Collectors.toSet())
    ;

    // 3. Rebuild list 
    _topPlayers = _list
            .stream()
            .filter(x -> _sortedPlayers.contains(x.getName()))
            .collect(Collectors.toList())
    ;

    return _topPlayers;
}


private PlayerStatistics initialize (int id, String name, int year, int month, int won, int lost) {
    return 
        new PlayerStatistics()
            .withId(id)
            .withName(name)
            .withIdMatchWon(won)
        );
}

【问题讨论】:

  • 我会像你一样使用.sorted().limit(2)。不过,我还没有研究过你所有的代码。
  • 我会这样做的。
  • 'John' 等不是有效的 Java 语法。此外,您的任务还不清楚。您说,您想获得“玩家”,但实际上您正在收集PlayerStatistics,每个玩家可能不止一个。您似乎假设每个玩家都有一个唯一的名称,但您应该明确说明。特别是,因为名称是“玩家”的唯一实际概念。如果这就是您对某个玩家的全部了解,那么您的_sortedPlayers 集已经是最终结果,即前 2 名“玩家”,所以不清楚为什么要执行第三步收集所有相关的PlayerStatistics

标签: java sorting java-8 java-stream collect


【解决方案1】:

首先,让我们声明您的代码绝对正确。它做了需要做的事情,甚至通过使用集合进行了优化。不过,它可以通过两种方式进一步改进:

  1. 时间复杂度:您正在对整个数据集进行排序,其时间复杂度为 O(mlogm)m 是您初始玩家列表的大小。立即,您将使用N &lt;&lt; m 获取列表中最重要的N 元素。

    下面我展示了一种将算法的时间复杂度提高到O(mlogN) 的方法,这意味着在您的特定情况下它将变为O(m)(这是因为N=2,所以logN=log2=1)。

  2. 您正在遍历数据集 3 次:首先您正在迭代玩家列表以创建计数地图,然后您正在迭代此地图以获得顶级 N 玩家的集合,最后您是再次迭代玩家列表以检查每个玩家是否属于顶级N玩家集合。

    这可以改进为仅对数据集执行 2 次遍历:第一次创建计数图(类似于您已经完成的),另一次创建仅保留顶部的结构 @ 987654334@ 个元素,按计数降序排序,遍历完成后即可返回结果。

重要提示:以下解决方案要求您的 PlayerStatistics 类始终如一地实现 hashCodeequals 方法。

首先,我们有一个通用方法topN,它(毫不奇怪)从任何给定的地图中提取顶部的N 元素。它通过按值比较其条目,降序(在此版本中,值 V 必须为 Comparable&lt;V&gt;,但此算法可以通过提供自定义 @987654343 轻松扩展以支持不实现 Comparable&lt;V&gt; 的值@):

public static 
<K, V extends Comparable<? super V>, T extends Comparable<? super T>>
Collection<K> 
topN(
        Map<K, V> map, 
        int N,
        Function<? super K, ? extends T> tieBreaker) {

    TreeMap<Map.Entry<K, V>, K> topN = new TreeMap<>(
        Map.Entry.<K, V>comparingByValue()      // by value descending, then by key
            .reversed()                         // to allow entries with duplicate values
            .thenComparing(e -> tieBreaker.apply(e.getKey())));

    map.entrySet().forEach(e -> {
      topN.put(e, e.getKey());
      if (topN.size() > N) topN.pollLastEntry();
    });

    return topN.values();
}

这里的topN TreeMap 表现为priority queue 大小为N(尽管我们添加了N+1 元素)。首先我们将条目放入topN映射中,然后,如果映射有多个N条目,我们立即在其上调用pollLastEntry方法,删除优先级最低的条目(按照顺序TreeMap 的键)。这保证在遍历时,topN 映射将只包含顶部的 N 条目,已排序。

请注意,我使用的比较器首先按值VTreeMap&lt;Map.Entry&lt;K, V&gt;, K&gt; 进行降序排序,然后按键K。这是在Function&lt;? super K, ? extends T&gt; tieBreaker 函数的帮助下实现的,该函数将每个键K 转换为必须为Comparable&lt;T&gt; 的值T。所有这些都允许映射包含具有重复值 V 的条目,而不需要键 K 也为 Comparable&lt;K&gt;

最后,您将使用上述方法如下:

Map<PlayerStatistics, Long> counts = yourInitialListOfPlayers.stream()
    .filter(x -> !"".equals(x.getName()) && x.getName() != null)
    .collect(Collectors.groupingBy(x -> x, Collectors.counting()));

Collection<PlayerStatistics> top2 = topN(counts, 2, PlayerStatistics::getName);

【讨论】:

  • 非常感谢费德里科。首先,不要停留在报价问题上,而是花时间分析我的问题。最重要的是,要有抽象解决问题的能力:)。我只是将您的功能集成到我的代码中,一切正常。
  • @anthony44 我很高兴知道我的回答对您有所帮助,写起来很有趣。至于 cmets 和观察,它们是有效的,至少对新读者来说是这样。老实说,我在回答你的问题时猜到了一点......我的猜测是PlayerStatistics 只是一个例子,你拥有的真实代码通过一些属性来计算元素和组。 Holger 的 cmets 帮助访问者更好地了解问题的背景,以便他们更好地理解答案。
  • 不错的答案。 &lt;T extends Comparable&lt;? super T&gt;&gt; 在这里会更好吗?
  • @user7 我不确定,但似乎更好
  • 您的解决方案将 每个 地图条目插入到您的 TreeSet 中,并在每次插入后踢出最小的元素。但是所有这些 log(n) 插入都是不必要的 - 您可以在插入之前检查传入条目是否大于 TreeSet 中的最小条目。
猜你喜欢
  • 1970-01-01
  • 2015-03-10
  • 1970-01-01
  • 2016-07-06
  • 1970-01-01
  • 1970-01-01
  • 2016-02-29
  • 1970-01-01
相关资源
最近更新 更多