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?



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

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

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]

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.


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).


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

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.


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

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

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.


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 (...) :)


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

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

}

0

A co ze sprawdzaniem indeksów?

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

1
Powinno działać dobrze, ale także wydajność O (n ^ 2), jak niektóre inne rozwiązania tutaj.
Florian Albrecht
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.