Java 8, Streams для поиска повторяющихся элементов


87

Я пытаюсь перечислить повторяющиеся элементы в целочисленном списке, например,

List<Integer> numbers = Arrays.asList(new Integer[]{1,2,1,3,4,4});    

using Streams of jdk 8. Кто-нибудь пробовал. Чтобы удалить дубликаты, мы можем использовать отдельный () api. Но как насчет поиска повторяющихся элементов? Кто-нибудь может мне помочь?



Если вы не хотите собирать поток, это, по сути, сводится к следующему: «как я могу просматривать более одного элемента в потоке одновременно»?
Торбьёрн Равн Андерсен

Set <Integer> items = new HashSet (); numbers.stream (). filter (n -> я! tems.add (n)). collect (Collectors.toSet ());
Сародж Кумар Саху

Ответы:


127

Вы можете использовать Collections.frequency:

numbers.stream().filter(i -> Collections.frequency(numbers, i) >1)
                .collect(Collectors.toSet()).forEach(System.out::println);

11
Та же производительность O (n ^ 2), что и в ответе @OussamaZoghlami , хотя, вероятно, проще. Тем не менее, вот голосование. Добро пожаловать в StackOverflow!
Тагир Валеев

6
Как уже упоминалось, это решение ^ 2, в котором существует тривиальное линейное решение. Я бы не принял это в ЧР.
jwilner

3
Он может быть медленнее, чем опция @Dave, но он красивее, поэтому я понесу удар по производительности.
jDub9

@jwilner - это ваша точка зрения относительно решения n ^ 2, относящегося к использованию Collections.frequency в фильтре?
mancocapac

5
@mancocapac да, это квадратично, потому что частотный вызов должен посещать каждый элемент в числах, и он вызывается для каждого элемента. Таким образом, для каждого элемента мы посещаем каждый элемент - n ^ 2 и неэффективно.
jwilner

71

Базовый пример. Первая половина строит частотную карту, вторая половина сокращает ее до отфильтрованного списка. Вероятно, не так эффективно, как ответ Дэйва, но более универсален (например, если вы хотите обнаружить ровно два и т. Д.)

     List<Integer> duplicates = IntStream.of( 1, 2, 3, 2, 1, 2, 3, 4, 2, 2, 2 )
       .boxed()
       .collect( Collectors.groupingBy( Function.identity(), Collectors.counting() ) )
       .entrySet()
       .stream()
       .filter( p -> p.getValue() > 1 )
       .map( Map.Entry::getKey )
       .collect( Collectors.toList() );

12
Этот ответ правильный, imo, потому что он линейный и не нарушает правило «предиката без состояния».
jwilner

54

Вам нужен набор ( allItemsниже) для хранения всего содержимого массива, но это O (n):

Integer[] numbers = new Integer[] { 1, 2, 1, 3, 4, 4 };
Set<Integer> allItems = new HashSet<>();
Set<Integer> duplicates = Arrays.stream(numbers)
        .filter(n -> !allItems.add(n)) //Set.add() returns false if the item was already in the set.
        .collect(Collectors.toSet());
System.out.println(duplicates); // [1, 4]

18
filter()требует предиката без состояния. Ваше «решение» поразительно похоже на пример предиката с отслеживанием
Мэтт МакГенри

1
@MattMcHenry: означает ли это, что это решение может привести к неожиданному поведению, или это просто плохая практика?
IcedDante

7
@IcedDante В локализованном случае, например, там, где вы точно знаете, что поток есть sequential(), это, вероятно, безопасно. В более общем случае, когда может быть поток parallel(), он почти гарантированно прерывается странными способами.
Мэтт МакГенри,

5
Это не только приводит к неожиданному поведению в некоторых ситуациях, но и смешивает парадигмы, как утверждает Блох, в третьем издании Effective Java этого делать не следует. Если вы поймете, что пишете это, просто используйте цикл for.
jwilner

6
Обнаружил, что это в дикой природе используется ограничением Hibernate Validator UniqueElements .
Дэйв

14

Способ O (n) будет следующим:

List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 4, 4);
Set<Integer> duplicatedNumbersRemovedSet = new HashSet<>();
Set<Integer> duplicatedNumbersSet = numbers.stream().filter(n -> !duplicatedNumbersRemovedSet.add(n)).collect(Collectors.toSet());

При таком подходе сложность пространства увеличилась бы вдвое, но это пространство не пустая трата; Фактически, теперь у нас есть только дубликат только как Набор, а также еще один Набор с удалением всех дубликатов.


13

Моя библиотека StreamEx, которая расширяет потоки Java 8, предоставляет специальную операцию, distinct(atLeast)которая может сохранять только элементы, появляющиеся по крайней мере указанное количество раз. Итак, вашу проблему можно решить так:

List<Integer> repeatingNumbers = StreamEx.of(numbers).distinct(2).toList();

Внутренне оно похоже на решение @Dave, оно подсчитывает объекты для поддержки других требуемых количеств и поддерживает параллелизм (используется ConcurrentHashMapдля параллельного потока, но HashMapдля последовательного). Для больших объемов данных вы можете получить ускорение с помощью .parallel().distinct(2).


26
Речь идет о Java Streams, а не о сторонних библиотеках.
ᄂ ᄀ

9

Вы можете получить дубликат следующим образом:

List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 4, 4);
Set<Integer> duplicated = numbers
  .stream()
  .filter(n -> numbers
        .stream()
        .filter(x -> x == n)
        .count() > 1)
   .collect(Collectors.toSet());

11
Разве это не операция O (n ^ 2)?
Trejkaz

4
Попробуйте использоватьnumbers = Arrays.asList(400, 400, 500, 500);
Тагир Валеев

1
Это похоже на создание петли с двумя глубинами? for (..) {for (..)} Просто любопытно, как это работает
изнутри

Хотя это хороший подход, но иметь streamвнутреннюю часть streamстоит дорого.
Вишва Ратна

4

Я думаю, что основные решения вопроса должны быть следующими:

Supplier supplier=HashSet::new; 
HashSet has=ls.stream().collect(Collectors.toCollection(supplier));

List lst = (List) ls.stream().filter(e->Collections.frequency(ls,e)>1).distinct().collect(Collectors.toList());

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


3

Мультимножество - это структура, поддерживающая количество вхождений каждого элемента. Использование реализации Guava:

Set<Integer> duplicated =
        ImmutableMultiset.copyOf(numbers).entrySet().stream()
                .filter(entry -> entry.getCount() > 1)
                .map(Multiset.Entry::getElement)
                .collect(Collectors.toSet());

2

создание дополнительной карты или потока занимает много времени и места…

Set<Integer> duplicates = numbers.stream().collect( Collectors.collectingAndThen(
  Collectors.groupingBy( Function.identity(), Collectors.counting() ),
  map -> {
    map.values().removeIf( cnt -> cnt < 2 );
    return( map.keySet() );
  } ) );  // [1, 4]


… И по вопросу о том, что является [дубликатом]

public static int[] getDuplicatesStreamsToArray( int[] input ) {
  return( IntStream.of( input ).boxed().collect( Collectors.collectingAndThen(
      Collectors.groupingBy( Function.identity(), Collectors.counting() ),
      map -> {
        map.values().removeIf( cnt -> cnt < 2 );
        return( map.keySet() );
      } ) ).stream().mapToInt( i -> i ).toArray() );
}

1

Если вам нужно только обнаружить наличие дубликатов (вместо того, чтобы перечислять их, чего хотел OP), просто преобразуйте их как в список, так и в набор, а затем сравните размеры:

    List<Integer> list = ...;
    Set<Integer> set = new HashSet<>(list);
    if (list.size() != set.size()) {
      // duplicates detected
    }

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


0

Думаю, у меня есть хорошее решение, как исправить такую ​​проблему - Список => Список с группировкой по Something.a и Something.b. Есть расширенное определение:

public class Test {

    public static void test() {

        class A {
            private int a;
            private int b;
            private float c;
            private float d;

            public A(int a, int b, float c, float d) {
                this.a = a;
                this.b = b;
                this.c = c;
                this.d = d;
            }
        }


        List<A> list1 = new ArrayList<A>();

        list1.addAll(Arrays.asList(new A(1, 2, 3, 4),
                new A(2, 3, 4, 5),
                new A(1, 2, 3, 4),
                new A(2, 3, 4, 5),
                new A(1, 2, 3, 4)));

        Map<Integer, A> map = list1.stream()
                .collect(HashMap::new, (m, v) -> m.put(
                        Objects.hash(v.a, v.b, v.c, v.d), v),
                        HashMap::putAll);

        list1.clear();
        list1.addAll(map.values());

        System.out.println(list1);
    }

}

class A, list1, это просто входящие данные - магия находится в Objects.hash (...) :)


1
Предупреждение: если Objects.hashвыдает одно (v.a_1, v.b_1, v.c_1, v.d_1)и (v.a_2, v.b_2, v.c_2, v.d_2)то же значение для и , то они будут считаться равными и удаляться как дубликаты, без фактической проверки того, что a, b, c и d совпадают. Это может быть приемлемым риском, или вы можете захотеть использовать функцию, отличную от той, Objects.hashкоторая гарантированно даст уникальный результат в вашем домене.
Марти Нил

0

Вам нужно использовать идиомы java 8 (steam)? Возможно, простое решение - перенести сложность в структуру данных, подобную карте, которая содержит числа в качестве ключа (без повторения) и время, когда оно встречается в качестве значения. Вы можете перебирать эту карту и делать что-то только с теми числами, которые ocurrs> 1.

import java.lang.Math;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;

public class RemoveDuplicates
{
  public static void main(String[] args)
  {
   List<Integer> numbers = Arrays.asList(new Integer[]{1,2,1,3,4,4});
   Map<Integer,Integer> countByNumber = new HashMap<Integer,Integer>();
   for(Integer n:numbers)
   {
     Integer count = countByNumber.get(n);
     if (count != null) {
       countByNumber.put(n,count + 1);
     } else {
       countByNumber.put(n,1);
     }
   }
   System.out.println(countByNumber);
   Iterator it = countByNumber.entrySet().iterator();
    while (it.hasNext()) {
        Map.Entry pair = (Map.Entry)it.next();
        System.out.println(pair.getKey() + " = " + pair.getValue());
    }
  }
}

0

Попробуйте это решение:

public class Anagramm {

public static boolean isAnagramLetters(String word, String anagramm) {
    if (anagramm.isEmpty()) {
        return false;
    }

    Map<Character, Integer> mapExistString = CharCountMap(word);
    Map<Character, Integer> mapCheckString = CharCountMap(anagramm);
    return enoughLetters(mapExistString, mapCheckString);
}

private static Map<Character, Integer> CharCountMap(String chars) {
    HashMap<Character, Integer> charCountMap = new HashMap<Character, Integer>();
    for (char c : chars.toCharArray()) {
        if (charCountMap.containsKey(c)) {
            charCountMap.put(c, charCountMap.get(c) + 1);
        } else {
            charCountMap.put(c, 1);
        }
    }
    return charCountMap;
}

static boolean enoughLetters(Map<Character, Integer> mapExistString, Map<Character,Integer> mapCheckString) {
    for( Entry<Character, Integer> e : mapCheckString.entrySet() ) {
        Character letter = e.getKey();
        Integer available = mapExistString.get(letter);
        if (available == null || e.getValue() > available) return false;
    }
    return true;
}

}

0

А как насчет проверки индексов?

        numbers.stream()
            .filter(integer -> numbers.indexOf(integer) != numbers.lastIndexOf(integer))
            .collect(Collectors.toSet())
            .forEach(System.out::println);

1
Должно работать нормально, но также производительность O (n ^ 2), как и некоторые другие решения здесь.
Флориан Альбрехт
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.