Java 8, strumienie, aby znaleźć zduplikowane elementy

87

Próbuję wymienić zduplikowane elementy na liście liczb całkowitych, powiedzmy np.

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

używając strumieni jdk 8. Czy ktoś próbował. Aby usunąć duplikaty, możemy użyć wyraźnego () api. Ale co ze znalezieniem zduplikowanych elementów? Czy ktoś może mi pomóc?

Siva
źródło
Jeśli nie chcesz zbierać strumienia, sprowadza się to zasadniczo do „jak mogę spojrzeć na więcej niż jeden element naraz w strumieniu”?
Thorbjørn Ravn Andersen
Set <Integer> items = new HashSet (); numbers.stream (). filter (n -> i! tems.add (n)). collect (Collectors.toSet ());
Saroj Kumar Sahoo

Odpowiedzi:

127

Możesz użyć Collections.frequency:

numbers.stream().filter(i -> Collections.frequency(numbers, i) >1)
                .collect(Collectors.toSet()).forEach(System.out::println);
Bao Dinh
źródło
11
Ta sama wydajność O (n ^ 2) jak w odpowiedzi @OussamaZoghlami , choć prawdopodobnie prostsza. Niemniej jednak głos za. Witamy w StackOverflow!
Tagir Valeev,
6
Jak wspomniano, jest to rozwiązanie ^ 2, w którym istnieje trywialne rozwiązanie liniowe. Nie zaakceptowałbym tego w CR.
jwilner
3
Może być wolniejsza niż opcja @Dave, ale jest ładniejsza, więc wezmę wydajność.
jDub9
@jwilner jest twoim zdaniem odnośnie rozwiązania n ^ 2 odnoszącego się do użycia Collections.frequency w filtrze?
mancocapac
5
@mancocapac tak, jest to kwadratowe, ponieważ wywołanie częstotliwości musi odwiedzać każdy element w liczbach i jest wywoływane w każdym elemencie. Dlatego dla każdego elementu odwiedzamy każdy element - n ^ 2 i niepotrzebnie nieefektywny.
jwilner
71

Podstawowy przykład. Pierwsza połowa tworzy mapę częstotliwości, druga połowa redukuje ją do przefiltrowanej listy. Prawdopodobnie nie tak wydajna jak odpowiedź Dave'a, ale bardziej wszechstronna (np. Jeśli chcesz wykryć dokładnie dwa itp.)

     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() );
RobAu
źródło
12
Ta odpowiedź jest poprawna imo, ponieważ jest liniowa i nie narusza reguły „predykatu bezstanowego”.
jwilner
54

Potrzebujesz zestawu ( allItemsponiżej) do przechowywania całej zawartości tablicy, ale to jest 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]
Dave
źródło
18
filter()wymaga predykatu bezpaństwowego. Twoje „rozwiązanie” jest uderzająco podobne do przykładu predykatu stanowego podanego w javadoc: docs.oracle.com/javase/8/docs/api/java/util/stream/ ...
Matt McHenry,
1
@MattMcHenry: czy to oznacza, że ​​to rozwiązanie może powodować nieoczekiwane zachowanie, czy jest to po prostu zła praktyka?
IcedDante
7
@IcedDante W zlokalizowanym przypadku, takim jak tam, gdzie wiesz na pewno, że strumień jest sequential(), prawdopodobnie jest bezpieczny. W bardziej ogólnym przypadku, gdy strumień może być parallel(), prawie na pewno pęknie w dziwny sposób.
Matt McHenry,
5
Oprócz wywoływania nieoczekiwanego zachowania w niektórych sytuacjach, powoduje to mieszanie paradygmatów, jak twierdzi Bloch, nie powinno się to robić w trzeciej edycji Efektywnej Javy. Jeśli zauważysz, że to piszesz, po prostu użyj pętli for.
jwilner
6
Znalazłem to na wolności, używane przez ograniczenie Hibernate Validator UniqueElements .
Dave,
14

Sposób O (n) wyglądałby jak poniżej:

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

W tym podejściu złożoność przestrzeni podwoiłaby się, ale przestrzeń ta nie jest marnotrawstwem; w rzeczywistości mamy teraz tylko duplikat tylko jako Zestaw, a także jako inny Zestaw z usuniętymi wszystkimi duplikatami.

Thomas Mathew
źródło
13

Biblioteka My StreamEx, która ulepsza strumienie Java 8, zapewnia specjalną operację, distinct(atLeast)która może zachować tylko elementy pojawiające się co najmniej określoną liczbę razy. Więc twój problem można rozwiązać w następujący sposób:

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

Wewnętrznie jest podobny do rozwiązania @Dave, zlicza obiekty, obsługuje inne pożądane ilości i jest przyjazny ConcurrentHashMapdla równoległości (używa do równoległego strumienia, ale HashMapdla sekwencyjnego). W przypadku dużych ilości danych można przyspieszyć za pomocą .parallel().distinct(2).

Tagir Valeev
źródło
26
Pytanie dotyczy strumieni Java, a nie bibliotek innych firm.
ᄂ ᄀ
9

Możesz uzyskać duplikat w ten sposób:

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());
Oussama Zoghlami
źródło
11
Czy to nie jest operacja O (n ^ 2)?
Trejkaz
4
Spróbuj użyćnumbers = Arrays.asList(400, 400, 500, 500);
Tagir Valeev
1
Czy jest to podobne do tworzenia pętli o 2 głębokościach? for (..) {for (..)} Tylko ciekawostki, jak to działa wewnętrznie
redigaffi
Chociaż to miłe podejście, to jednak posiadanie streamwnętrza streamjest kosztowne.
Vishwa Ratna
4

Myślę, że podstawowe rozwiązania tego pytania powinny wyglądać następująco:

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

cóż, nie jest zalecane wykonywanie operacji filtrowania, ale dla lepszego zrozumienia użyłem go, ponadto w przyszłych wersjach powinno być trochę niestandardowego filtrowania.

Prashant
źródło
3

Zestaw wielozbiorowy to struktura utrzymująca liczbę wystąpień dla każdego elementu. Korzystanie z implementacji Guava:

Set<Integer> duplicated =
        ImmutableMultiset.copyOf(numbers).entrySet().stream()
                .filter(entry -> entry.getCount() > 1)
                .map(Multiset.Entry::getElement)
                .collect(Collectors.toSet());
numéro6
źródło
2

tworzenie dodatkowej mapy lub strumienia jest czasochłonne i przestrzenne…

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]


… I dla którego kwestia jest uważana za [duplikat]

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() );
}
Kaplan
źródło
1

Jeśli chcesz tylko wykryć obecność duplikatów (zamiast wymieniać je, czego chciał OP), po prostu przekonwertuj je na listę i zestaw, a następnie porównaj rozmiary:

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

Podoba mi się to podejście, ponieważ jest mniej miejsc na błędy.

Patrick
źródło
0

Myślę, że mam dobre rozwiązanie, jak rozwiązać taki problem - List => Lista z grupowaniem według Something.a & Something.b. Istnieje rozszerzona definicja:

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

}

klasa A, lista1 to tylko dane przychodzące - magia jest w Objects.hash (...) :)

Zhurov Konstantin
źródło
1
Ostrzeżenie: jeśli Objects.hashdaje tę samą wartość dla (v.a_1, v.b_1, v.c_1, v.d_1)i (v.a_2, v.b_2, v.c_2, v.d_2), to zostaną one uznane za równe i zostaną usunięte jako duplikaty, bez faktycznego sprawdzania, czy a, b, c i d są takie same. Może to być akceptowalne ryzyko lub możesz chcieć użyć funkcji innej niż ta, Objects.hashktóra gwarantuje unikalny wynik w całej domenie.
Marty Neal
0

Czy musisz używać idiomów Java 8 (steams)? Perphaps prostym rozwiązaniem byłoby przeniesienie złożoności do struktury danych podobnej do mapy, która zawiera liczby jako klucz (bez powtarzania) i czas ich występowania jako wartość. Możesz powtórzyć tę mapę i zrobić coś tylko z tymi liczbami, które pojawiają się> 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());
    }
  }
}
Zwycięzca
źródło
0

Wypróbuj to rozwiązanie:

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

}
Ilia Galperin
źródło
0

A co ze sprawdzaniem indeksów?

        numbers.stream()
            .filter(integer -> numbers.indexOf(integer) != numbers.lastIndexOf(integer))
            .collect(Collectors.toSet())
            .forEach(System.out::println);
bagom
źródło
1
Powinno działać dobrze, ale także wydajność O (n ^ 2), jak niektóre inne rozwiązania tutaj.
Florian Albrecht