【问题标题】:How to use Java 8 streams to find all values preceding a larger value?如何使用 Java 8 流查找较大值之前的所有值?
【发布时间】:2024-01-29 04:20:02
【问题描述】:

用例

通过在工作中发布的一些编码 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


【解决方案1】:

使用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 package description

流的元素在流的生命周期内只被访问一次。与Iterator 一样,必须生成一个新流来重新访问源的相同元素。

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

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

  • 有条不紊(非常好)。
  • 使用流的iterator在技术上仍在使用流。

【讨论】:

  • 我认为这根本不是“meh”。我认为这是最好的方法。许多人认为流是按从左到右的顺序处理值。这导致了顺序的、状态突变的思考。它也容易出现边界错误。您可以在 OP 发布的传统循环方法中看到这一点。相比之下,您所做的基于列表索引的流遵循基于向量或数组的编程风格。对每个索引 i 进行计算,您可以轻松证明结果对所有 i 都是正确的。没有突变或顺序依赖性,因此它可以很好地并行化。 +1
  • @StuartMarks 从某种意义上说,我们没有在元素上流式传输,这很糟糕。但你是对的,这可能与我的思考方式有关。我的“理想”解决方案也是非捕获的。
  • @StuartMarks Lol,它们被称为“流”,因为它们应该体现“以从左到右的顺序处理值”的概念。关键是您只使用少量内存,并且不会一次将所有数据保存在内存中。 (例如,当您“流式传输”一首歌曲时。)当然,Java 8 完全改变了定义,使其成为“一个易于并行化的无状态进程”,这就是为什么您发现“使用流”来索引并没有什么讽刺意味的原因一个很大的List
【解决方案2】:

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

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]

【讨论】:

  • 现在您刚刚将流转换为顺序流(实际上并没有改变任何东西,因为Collection.stream() 已经返回了顺序流)。我的意思是你不能利用这种方法来利用并行计算。
  • 即使使用顺序流,流库仍然可以进行树缩减,而不是您在此处依赖的线性缩减。
  • @JeffreyBosboom 我很想集成一些在顺序情况下进行树归约的代码,只是为了打破每个人的非关联归约器功能。 非常诱惑。
  • @JeffreyBosboom,据我所知目前树缩减在 JDK 中不用于顺序流(包括 Java 9 主干代码)。但你是对的,你不能依赖这个。我的 StreamEx 库有一个 foldLeft 方法,即使对于并行流,它也严格从左到右工作。
  • @Bohemian 即使对于并行流,它也是连续的吗?我将您的解决方案添加到了 parallel() 中,结果因为关联性被破坏总是不同的......
【解决方案3】:

这不是纯 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);

pairMap 操作在内部使用自定义 spliterator 实现。结果,您的代码非常干净,它不依赖于源是List 还是其他任何东西。当然,它也适用于并行流。

为此任务提交了testcase

【讨论】:

【解决方案4】:

如果流是顺序的或并行的,则接受的答案可以正常工作,但如果底层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 并行,然后决定返回哪个收集器。

【讨论】:

  • 当我查看您的个人资料时,我的个人资料浏览量正好是 6666 次......这一定意味着什么......我不知道这个答案(代码)是否正确,我不能理解/遵循您的代码,但根据您的解释,我确实理解,我知道这是最好的。我同样尝试了两天,但我的头脑爆炸了。 +1
  • 其实你的并行版本没有任何好处。最后一行中指定的实际算法只有在其他所有操作完成后才会按顺序执行。即使你在那里用parallelStream() 替换stream(),我相信这个解决方案也会很慢。
  • 刚刚使用 4 个 CPU 盒在随机创建的 10k、100k 和 1M 数字输入上分析了所有提供的版本。您的并行版本是所有版本中最慢的(比顺序版本慢 2-3 倍,具体取决于输入大小)。这是带有基准核心和结果的gist(StreamEx 测试需要 JMH 和 StreamEx 库)
  • @TagirValeev 这并不奇怪,因为您需要一次重新过滤所有输入。您的库可能性能更高,因为您实现了自己的拆分器(这也需要比我的 15 行解决方案更多的代码),所以我不会称这是一个公平的比较 :-),尽管如果您需要性能,这可能是走。由于collectingAndThen 操作,并行版本较慢,我并不感到惊讶。我现在真的没有时间测试,但与实际答案相比,我很想知道输入是否为LinkedList 的结果。
  • 对于不支持随机访问的列表,接受的答案肯定会表现不佳,并且对于任何其他流源(例如,BufferedReader、流的串联等)根本不起作用。使用我的库你有两个优点:它可以很好地与任何流源一起工作,并且并行化确实提高了速度。缺点是:您必须使用更多代码。如果最快和灵活的解决方案也是简短的,我根本不会写这个库:-)
【解决方案5】:

如果您愿意使用第三方库并且不需要并行性,那么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λ 背后的公司工作

【讨论】:

  • 你可以用Window::value代替w -&gt; w.value()
【解决方案6】:

您可以通过使用有界队列来存储流经流的元素来实现这一点(这是基于我在这里详细描述的想法:Is it possible to get next element in the Stream?

下面的示例首先定义了 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);
  }
}

【讨论】:

    最近更新 更多