【问题标题】:Is it possible to group by and find top N in exactly one iteration是否可以在一次迭代中分组并找到前 N 个
【发布时间】:2020-07-14 11:39:12
【问题描述】:

它与 How to apply to sort and limiting after groupBy using Java streams 不同,因为我想在一次迭代中解决这个问题。想象一下我有以下实体:

@Getter
@Setter
@AllArgsConstructor
public static class Hospital {
    private AREA area;
    private int patients;
}

public enum AREA {
    AREA1, AREA2, AREA3
}

现在给定一个医院列表,我想找到其中患者最多的区域,这是我到目前为止所做的:

public static void main(String[] args) {
    List<Hospital> list = Arrays.asList(
            new Hospital(AREA.AREA1, 20),
            new Hospital(AREA.AREA2, 10),
            new Hospital(AREA.AREA1, 10),
            new Hospital(AREA.AREA3, 40),
            new Hospital(AREA.AREA2, 10));
    Map<AREA, Integer> map = findTopTen(list);
    for (AREA area : map.keySet())
        System.out.println(area);

}

public static Map<AREA, Integer> findTopTen(Iterable<Hospital> iterable) {
    Map<AREA, Integer> iterationOneResult = StreamSupport.stream(iterable.spliterator(), false)
            .collect(Collectors.groupingBy(Hospital::getArea,
                    Collectors.summingInt(Hospital::getPatients)));
    return iterationOneResult.entrySet().stream()
            .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
            .limit(10)
            .collect(Collectors.toMap(Map.Entry::getKey,
                    Map.Entry::getValue, (o, o2) -> o,
                    LinkedHashMap::new));

}

显然,我已经迭代了两次以找到患者最多的前十个区域(一次用于按区域对医院进行分组并计算该组的总和,再一次用于查找前十个区域)。

现在我想知道的是:

  1. 有没有更好的方法在一个流中解决这个问题,因此需要一个迭代?

  2. 在一次迭代中执行此操作是否有任何性能优势,解决此类问题的最佳实践是什么? (一方面我认为当我调用collect 这是一个终端操作,它第一次迭代我的可迭代对象并将中间结果保存在另一个对象中,在我的代码中我将该对象命名为iterationOneResult,因此使用一个流并调用collect one time 将省略中间结果,这是在 java 中使用流的主要好处,另一方面,在一次迭代中解决此问题使其更快)。

【问题讨论】:

  • (e1, e2) -&gt; e2.getValue() - e1.getValue() 分别替换为Map.Entry.comparingByValue()Map.Entry.comparingByValue(Comparator.reverseOrder())
  • 时间复杂度不是这样工作的。 “O(2n)”与 O(n) 没有什么不同。但无论如何,你的操作不是 O(n),因为排序是 O(n log n)。您可以为第二个操作创建一个自定义的基于PriorityQueue 的收集器,以仅保留前十个条目并避免对整个地图进行排序。但是,这并不意味着生成的操作比简单方法需要更少的时间。对了,你忘记limit(10)了吗?
  • 我不知道你想说什么“O(n log n) 是搜索而不是排序的复杂性”。首先,我不知道您指的是什么“搜索”,其次,仅仅因为某些操作具有O(n log n),并不意味着不能有任何其他操作具有O(n log n)。 Stream 的sorted 操作没有指定任何算法,但有证据表明,一般情况下不可能有比 O(n log n) 更好的排序算法。
  • 总和为 O(n)。而且,顺便说一句,第二个操作流过组,所以虽然最坏的情况仍然是 O(n log n),但实际性能可能要好得多,因为实际上组比原始元素少。同样,实际的排序算法具有更好的平均和最佳情况性能。这就是为什么假设迭代两次是不好的,是没有用的。理论上的时间复杂度不会改变,现实生活中的性能甚至可以比任何尝试在单次迭代中做到这一点更好。在这里不重要,因为单次迭代是不可能的
  • 因此,当您真正构建单通道解决方案时,例如使用PriorityQueue,您必须在遇到时删除并重新添加每个项目,因为排序标准的值会永久更改。因此,时间复杂度仍然是 O(n log n),但这里 n 真正意味着源元素的数量,即使在最好的情况下也是如此。因此,对于大多数实际用例,两次迭代的解决方案将明显更快。

标签: java java-8 java-stream


【解决方案1】:

让我试着回答你的问题,并提供一些背景说明为什么它们可能不是正确的:

  1. 有没有更好的方法在一个流中解决这个问题,因此需要一次迭代?

这里的基本问题是,您的目标是找到具有最大值的,从这些组的原始成员开始,未排序。因此,在您找到最大的任何东西之前,您需要将成员分配给组。问题是,组中的哪些成员决定了该组的价值 - 这导致您无法在将所有成员分组之前做出诸如“前十组是什么”之类的决定。

这是groupingByCollector 的原因之一 - 收集器执行终端操作,这是一种奇特的说法,它消耗整个流而不返回流但是一个已解决的某事 - 它“结束”了流。

它需要结束流的原因(即在返回其组之前等待最后一个元素)是因为它不能在看到最后一个元素之前给你组A,因为最后一个元素可能属于组@987654324 @。分组是一种在未排序的数据集上无法进行流水线化的操作。

这意味着,无论您做什么,都有一个严格的逻辑要求,即您首先必须以某种方式对您的项目进行分组,然后找到最大值。这个first, then 顺序意味着两次迭代:一次在项目上,第二次在组上。

  1. 在一次迭代中执行此操作是否有任何性能优势,解决此类问题的最佳实践是什么? (一方面,在我的观点中,当我第一次调用collect 这是一个终端操作时,它会迭代我的可迭代对象并将中间结果保存在另一个对象中,在我的代码中,我将该对象命名为iterationOneResult,因此使用一个流并调用collect one time 将忽略中间结果,这是在 java 中使用流的主要好处,另一方面,在一次迭代中解决这个问题会使其更快)。

重新阅读上述内容:“两次迭代:一次在项目上,第二次在组上”。这些将永远发生。但是,请注意,这是对两个不同事物的两次迭代。鉴于您的组可能比成员少,后一个迭代会更短。您的运行时将不是O(2n) = O(n),而是O(f(n, m)),其中f(n,m) 将是“将n 成员分类到m 组的成本,加上找到最大k 组的成本”。

在一次迭代中执行此操作是否有任何性能优势

嗯...不,因为如上所述你不能。

解决此类问题的最佳做法是什么?

这一点我怎么强调都不为过:干净的代码

99.9% 的情况下,如果自定义类可以为您带来任何好处,那么您将浪费更多的时间来优化自定义类而不是它们为您带来的性能。这里最容易获得的好处是最大限度地减少代码行数,并最大限度地提高它们对未来程序员的理解程度。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-12-05
    • 2011-12-17
    • 2021-12-01
    • 2016-05-23
    • 1970-01-01
    • 2015-07-30
    • 1970-01-01
    • 2018-02-16
    相关资源
    最近更新 更多