【问题标题】:Cumulative Sum using Java 8 stream API使用 Java 8 流 API 的累积和
【发布时间】:2019-08-11 10:39:33
【问题描述】:

我有一个整数列表,比如 list1,我想获得另一个列表 list2,其中包含从开始到当前索引的累积总和。如何使用 Stream API java 8 做到这一点?

List<Integer> list1 = new ArrayList<>();
list1.addAll(Arrays.asList(1, 2, 3, 4));
List<Integer> list2 = new ArrayList<>();
// initialization
list2.add(list1.get(0));
for(int i=1;i<list1.size();i++) {
// increment step
    list2.add(list2.get(i-1) + list1.get(i));
}

如何将上述命令式代码更改为声明式

list2 should be [1, 3, 6, 10]

【问题讨论】:

  • 您会从答案中注意到,任何使用流的解决方案都会效率低下。流并不真正适用于这个用例。 (一般来说,流并不打算替换所有命令式代码。)
  • @LouisWasserman 我同意。但在这种情况下,我不关心效率(可能应该在问题中提到它)。我想用上面提到的命令式风格以外的东西写同样的东西。我自己做不到,因为我被卡住了,因为解决方案是 map 和 reduce 操作的混合
  • 伙计,这里缺少元组真的很痛苦。
  • 昨天问了一个极其相似的问题:stackoverflow.com/questions/55230261/…

标签: java java-8 java-stream


【解决方案1】:

流不适合此类任务,因为涉及到状态(累积部分和)。相反,您可以使用Arrays.parallelPrefix:

Integer[] arr = list1.toArray(Integer[]::new);

Arrays.parallelPrefix(arr, Integer::sum);

List<Integer> list2 = Arrays.asList(arr);

这首先使用Collection.toArraylist1 复制到一个数组中,该功能从JDK 11 开始提供。如果您还没有使用Java 11,可以将第一行替换为传统的toArray 调用:

Integer[] arr = list1.toArray(new Integer[0]);

此解决方案不使用流,但它是声明性,因为Arrays.parallelPrefix 接收累积运算作为参数(在本例中为Integer::sum)。

时间复杂度为O(N),尽管可能会涉及一些与设置并行处理所需的基础架构相关的非次要固定成本。但是,根据文档:

并行前缀计算通常比大型数组的顺序循环更有效

所以看来这种方法值得一试。

此外,值得一提的是,这种方法有效,因为Integer::sum 是一个关联操作。这是一个要求。

【讨论】:

【解决方案2】:

O(n)(仅按顺序工作)解决方案如下,但我觉得它不是很优雅。我想这是一个品味问题

AtomicInteger ai = new AtomicInteger();
List<Integer> collect = list1.stream()
                             .map(ai::addAndGet)
                             .collect(Collectors.toList());
System.out.println(collect); // [1, 3, 6, 10]

【讨论】:

  • 此解决方案仅在流是连续的情况下才有效。并行化它会完全打破这一点。
  • @ben 在并行流中,你不能有累积和吗?
  • Ruslan 的回答允许并行流。唯一的有状态操作是对原始列表本身,我们可以相对安全地假设它是只读的。他的解决方案中列表中的每个项目都可以独立于其他流元素查询列表。
  • @VenkataraghavanYanamandram 和其他解决方案是的,但它们是 O(n^2)
【解决方案3】:

您可以为此使用Stream.collect()

List<Integer> list1 = Arrays.asList(1, 2, 3, 4);
List<Integer> list2 = list1.stream()
        .collect(ArrayList::new, (sums, number) -> {
            if (sums.isEmpty()) {
                sums.add(number);
            } else {
                sums.add(sums.get(sums.size() - 1) + number);
            }
        }, (sums1, sums2) -> {
            if (!sums1.isEmpty()) {
                int sum = sums1.get(sums1.size() - 1);
                sums2.replaceAll(num -> sum + num);
            }
            sums1.addAll(sums2);
        });

此解决方案也适用于并行流。使用list1.parallelStream()list1.stream().parallel() 而不是list1.stream()

两种情况的结果都是:[1, 3, 6, 10]

【讨论】:

  • 这应该是公认的答案,因为无论流是否并行以及在任何情况下都是 O(n),它都是正确的。
  • 这是我对无国籍状态的看法:stackoverflow.com/a/70969797/402428
【解决方案4】:

对于每个索引:从零迭代到该索引,获取每个元素,然后获取总和
将整数装箱到Integers
收集到一个列表

IntStream.range(0, list1.size())
    .map(i -> IntStream.rangeClosed(0, i).map(list1::get).sum())
    .boxed()
    .collect(Collectors.toList());

您每次都将每个数字相加,而不是重复使用之前的累积结果,但是流不适合查看之前迭代的结果。

您可以编写自己的收集器,但在这一点上,老实说,您为什么还要为流而烦恼?

list1.stream()
    .collect(
        Collector.of(
            ArrayList::new,
            (a, b) -> a.add(a.isEmpty() ? b : b + a.get(a.size() - 1)),
            (a, b) -> { throw new UnsupportedOperationException(); }
        )
    );

【讨论】:

  • 那是 O(n^2) ! (相对于 OP 的 O(n))
  • @DodgyCodeException 是的。
  • @Michael 感谢您的精心回复。我了解到我可以编写自己的收藏家。我知道这是矫枉过正。但很高兴知道还有其他方法。
【解决方案5】:

你可以使用sublist从开始累加到当前索引:

List<Integer> list = IntStream.range(0, list1.size())
        .mapToObj(i -> list1.subList(0, i + 1).stream().mapToInt(Integer::intValue).sum())
        .collect(Collectors.toList());

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-12-14
    • 1970-01-01
    • 2016-02-07
    • 2018-06-10
    相关资源
    最近更新 更多