使用Java 8流API实现累加和

20

我有一个整数列表list1,想要得到另一个列表list2,其中包含从开头累积求和至当前索引的结果。如何使用Java 8 Stream API实现?

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]

11
从答案中可以注意到,任何使用流的解决方案都不太高效。 流并不真正用于这种情况。(通常来说,流并不意味着要取代所有命令式代码。) - Louis Wasserman
@LouisWasserman 我同意。但在这种情况下,我不关心效率(可能应该在问题中提到)。我想用除了上述命令式风格之外的东西来编写相同的内容。我自己无法做到这一点,因为解决方案有点类似于映射和归约操作的混合体。 - run_time_error
1
兄弟,这里缺少元组真的很痛苦。 - Alexander
2
昨天已经有一个非常相似的问题被问到了:https://dev59.com/frLma4cB1Zd3GeqPdp-b - Jacob G.
5个回答

21

流并不适合处理这种任务,因为涉及到状态 (累积部分和)。相反,您可以使用 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
从未听说过并行前缀。非常感谢您教给我一个新概念。:)我一定会尝试的。 - run_time_error
2
@run_time_error 这里有更多关于这个主题的阅读材料:https://zh.wikipedia.org/wiki/%E5%89%8D%E7%BC%80%E5%92%8C。 - fps
3
不要让生活变得不必要的困难。使用list1.toArray(new Integer[0])。正如这篇优秀的文章所阐述的那样,在常用的Hotspot JVM中,使用零大小甚至更有效。这是JDK 11新方法之所以做同样的事情的原因之一:“*默认实现使用零调用生成器函数,然后将结果数组传递给 toArray(T[]).*” - Holger

6

对于每个索引:从零迭代到该索引,获取每个元素并求和
将int类型的数据装箱为Integer
收集到一个列表中

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(); }
        )
    );

6
那是O(n^2)!!(与原帖中的O(n)相比)。 - DodgyCodeException
1
@DodgyCodeException 是的。 - Michael
@ Michael 谢谢你详细的回答。我已经学会了自己编写收集器。我知道这有点过头了,但很高兴知道还有其他方法。 - run_time_error

6
一个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]

7
只有在流是顺序的情况下,这个解决方案才能起作用。如果并行化,将会完全破坏它。 - Ben R.
@ben 在并行流中,你不能得到累加和,对吗? - Venkataraghavan Yanamandram
Ruslan的答案允许并行流处理。唯一有状态的操作是在原始列表本身上进行的,我们可以相对安全地假设它是只读的。在他的解决方案中,列表中的每个项目都可以独立于其他流元素查询列表。 - Ben R.
@VenkataraghavanYanamandram 有其他解决方案,但它们的时间复杂度为 O(n^2) - Yassin Hajaj

3

你可以使用 sublist 方法从开头对当前索引之前的元素进行求和:

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

2
你可以直接使用 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]
"Original Answer"翻译成"最初的回答"

这应该是被接受的答案,因为它是正确的,无论流是否并行,并且在任何情况下都是O(n)。 - michid
这是我对使其无状态的看法:https://dev59.com/K8Pra4cB1Zd3GeqPio7j#70969797 - michid

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