如何使用 Java 8 流查找较大值之前的所有值?

问题描述 投票:0回答:7

用例

通过 Katas 在工作中发布的一些编码,我偶然发现了这个问题,但我不知道如何解决。

使用 Java 8 Streams,给定一个正整数列表,生成一个 整数列表,其中整数位于较大值之前。

[10, 1, 15, 30, 2, 6]

上述输入将产生:

[1, 15, 2]

因为 1 在 15 之前,15 在 30 之前,2 在 6 之前。

非流解决方案

public List<Integer> findSmallPrecedingValues(final List<Integer> values) {

    List<Integer> result = new ArrayList<Integer>();
    for (int i = 0; i < values.size(); i++) {
        Integer next = (i + 1 < values.size() ? values.get(i + 1) : -1);
        Integer current = values.get(i);
        if (current < next) {
            result.push(current);
        }
    }
    return result;
}

我尝试过的事情

我遇到的问题是我不知道如何访问 lambda 中的 next。

return values.stream().filter(v -> v < next).collect(Collectors.toList());

问题

  • 是否可以检索流中的下一个值?
  • 我应该使用
    map
    并映射到
    Pair
    才能访问下一个吗?
java java-8 java-stream
7个回答
35
投票

使用

IntStream.range

static List<Integer> findSmallPrecedingValues(List<Integer> values) {
    return IntStream.range(0, values.size() - 1)
        .filter(i -> values.get(i) < values.get(i + 1))
        .mapToObj(values::get)
        .collect(Collectors.toList());
}

这当然比带有大循环的命令式解决方案更好,但就以惯用方式“使用流”的目标而言,仍然有点乏味。

是否可以检索流中的下一个值?

不,不是真的。我所知道的最好的引用是在

java.util.stream
包描述

流的元素在流的生命周期中仅被访问一次。与

Iterator
一样,必须生成新流才能重新访问源中的相同元素。

(检索当前正在操作的元素之外的元素意味着它们可以被多次访问。)

从技术上来说,我们还可以通过其他几种方式来做到这一点:

  • 庄严地(非常无聊)。
  • 使用流的
    iterator
    技术上仍然使用流。

13
投票

这不是一个纯粹的 Java8,但最近我发布了一个名为 StreamEx 的小型库,它有一个完全适合此任务的方法:

// Find all numbers where the integer preceded a larger value.
Collection<Integer> numbers = Arrays.asList(10, 1, 15, 30, 2, 6);
List<Integer> res = StreamEx.of(numbers).pairMap((a, b) -> a < b ? a : null)
    .nonNull().toList();
assertEquals(Arrays.asList(1, 15, 2), res);

使用自定义 spliterator 在内部实现 pairMap 操作。因此,您的代码非常干净,不依赖于源是

List
还是其他任何内容。当然,它也适用于并行流。

为此任务提交了一个测试用例


10
投票

这不是单行(它是两行),但这是有效的:

List<Integer> result = new ArrayList<>();
values.stream().reduce((a,b) -> {if (a < b) result.add(a); return b;});

不是通过“查看下一个元素”来解决它,而是通过“查看 previous 元素”来解决它,它

reduce()
免费为您提供。我通过注入填充的代码片段来改变其预期用法。基于先前和当前元素的比较的列表,然后返回当前元素,以便下一次迭代会将其视为其先前元素。


一些测试代码:

List<Integer> result = new ArrayList<>();
IntStream.of(10, 1, 15, 30, 2, 6).reduce((a,b) -> {if (a < b) result.add(a); return b;});
System.out.println(result);

输出:

[1, 15, 2]

6
投票

如果流是顺序的或并行的,则接受的答案工作正常,但如果底层

List
不是随机访问,则可能会受到影响,因为对
get
的多次调用。

如果您的流是连续的,您可以滚动此收集器:

public static Collector<Integer, ?, List<Integer>> collectPrecedingValues() {
    int[] holder = {Integer.MAX_VALUE};
    return Collector.of(ArrayList::new,
            (l, elem) -> {
                if (holder[0] < elem) l.add(holder[0]);
                holder[0] = elem;
            },
            (l1, l2) -> {
                throw new UnsupportedOperationException("Don't run in parallel");
            });
}

和用法:

List<Integer> precedingValues = list.stream().collect(collectPrecedingValues());

尽管如此,您也可以实现一个收集器,以便适用于顺序流和并行流。唯一的问题是您需要应用最终转换,但在这里您可以控制

List
实现,因此您不会受到
get
性能的影响。

这个想法是首先生成一个对列表(由大小为 2 的

int[]
数组表示),其中包含由大小为 2、间隙为 1 的窗口分割的流中的值。当我们需要合并两个列表时,我们检查是否为空并将第一个列表的最后一个元素与第二个列表的第一个元素的间隙合并。然后我们应用最终的转换来仅过滤所需的值并将它们映射以获得所需的输出。

它可能不像公认的答案那么简单,但它可以是一个替代解决方案。

public static Collector<Integer, ?, List<Integer>> collectPrecedingValues() {
    return Collectors.collectingAndThen(
            Collector.of(() -> new ArrayList<int[]>(),
                    (l, elem) -> {
                        if (l.isEmpty()) l.add(new int[]{Integer.MAX_VALUE, elem});
                        else l.add(new int[]{l.get(l.size() - 1)[1], elem});
                    },
                    (l1, l2) -> {
                        if (l1.isEmpty()) return l2;
                        if (l2.isEmpty()) return l1;
                        l2.get(0)[0] = l1.get(l1.size() - 1)[1];
                        l1.addAll(l2);
                        return l1;
                    }), l -> l.stream().filter(arr -> arr[0] < arr[1]).map(arr -> arr[0]).collect(Collectors.toList()));
}

然后,您可以将这两个收集器包装在实用收集器方法中,检查流是否与

isParallel
并行,然后决定返回哪个收集器。


4
投票

如果你愿意使用第三方库并且不需要并行性,那么jOOλ提供了如下的SQL风格的窗口函数

System.out.println(
Seq.of(10, 1, 15, 30, 2, 6)
   .window()
   .filter(w -> w.lead().isPresent() && w.value() < w.lead().get())
   .map(w -> w.value())
   .toList()
);

屈服

[1, 15, 2]

lead()
函数从窗口中按遍历顺序访问下一个值。

免责声明:我在jOOλ背后的公司工作


2
投票

您可以通过使用有界队列来存储流经流的元素来实现这一点(这是基于我在这里详细描述的想法:是否有可能获取流中的下一个元素?

下面的示例首先定义了 BoundedQueue 类的实例,它将存储通过流的元素(如果您不喜欢扩展 LinkedList 的想法,请参阅上面提到的链接以获取替代和更通用的方法)。稍后您只需检查两个后续元素 - 感谢辅助类:

public class Kata {
  public static void main(String[] args) {
    List<Integer> input = new ArrayList<Integer>(asList(10, 1, 15, 30, 2, 6));

    class BoundedQueue<T> extends LinkedList<T> {
      public BoundedQueue<T> save(T curElem) {
        if (size() == 2) { // we need to know only two subsequent elements
          pollLast(); // remove last to keep only requested number of elements
        }

        offerFirst(curElem);
        return this;
      }

      public T getPrevious() {
        return (size() < 2) ? null : getLast();
      }

      public T getCurrent() {
        return (size() == 0) ? null : getFirst();
      }
    }

    BoundedQueue<Integer> streamHistory = new BoundedQueue<Integer>();

    final List<Integer> answer = input.stream()
      .map(i -> streamHistory.save(i))
      .filter(e -> e.getPrevious() != null)
      .filter(e -> e.getCurrent() > e.getPrevious())
      .map(e -> e.getPrevious())
      .collect(Collectors.toList());

    answer.forEach(System.out::println);
  }
}

0
投票

列表 listOne= List.of(12,13,67,56,90,62);

listOne.stream().filter(val->val.intValue()>50).collect(Collectors.toList()).forEach(System.out::println);

© www.soinside.com 2019 - 2024. All rights reserved.