Как использовать потоки Java 8 для поиска всех значений, предшествующих большему значению?

Пример использования

Через некоторое кодирование Katas, опубликованное на работе, я наткнулся на эту проблему, что не уверен, как ее решить.

Используя потоки Java 8, учитывая список положительных целых чисел, создайте список целых чисел, где целое число предшествует большему значению.

[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;
}

Что я пробовал

Проблема у меня есть, я не могу понять, как получить доступ к следующей в лямбда.

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

Вопрос

  • Можно ли получить следующее значение в потоке?
  • Должен ли я использовать map и сопоставлять с Pair для доступа к следующей?

Ответ 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:

Элементы потока посещаются только один раз в течение жизни потока. Как и Iterator, новый поток должен быть сгенерирован для повторного просмотра тех же элементов источника.

(Извлечение элементов, кроме текущего элемента, который будет работать, будет означать, что их можно было бы посещать более одного раза.)

Мы также могли бы технически сделать это еще несколькими способами:

  • Устно (очень meh).
  • Использование потока Iterator технически все еще использует поток.

Ответ 2

Это не чистый 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.

Ответ 3

Это не однострочный (это двухстрочный), но это работает:

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

Вместо того, чтобы решать это "глядя на следующий элемент", это решает его "глядя на предыдущий элемент, который 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]

Ответ 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.

Идея состоит в том, чтобы сначала создать список пар (представленный массивом int[] размером 2), который содержит значения в потоке, нарезанные окном размера два с пробелом в один. Когда нам нужно объединить два списка, мы проверяем пустоту и объединяем пробел последнего элемента первого списка с первым элементом второго списка. Затем мы применяем окончательное преобразование для фильтрации только желаемых значений и отображения их для получения желаемого результата.

Это может быть не так просто, как принятый ответ, но это может быть альтернативное решение.

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, а затем решает, какой сборщик должен вернуться.

Ответ 5

Если вы хотите использовать стороннюю библиотеку и не нуждаетесь в parallelism, то 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λ

Ответ 6

Вы можете добиться этого, используя ограниченную очередь для хранения элементов, которые текут через поток (который основывается на идее, которую я подробно описал здесь: Можно ли получить следующий в потоке?

В нижеследующем примере сначала определяется экземпляр класса 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);
  }
}