Java 8, Streams per trovare gli elementi duplicati


87

Sto cercando di elencare elementi duplicati nell'elenco di numeri interi, ad esempio,

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

utilizzando Streams di jdk 8. Qualcuno ha provato. Per rimuovere i duplicati possiamo usare l'api distinte (). Ma per quanto riguarda la ricerca degli elementi duplicati? Qualcuno mi può aiutare?



Se non vuoi raccogliere il flusso, questo essenzialmente si riduce a "come posso guardare più di un elemento contemporaneamente in un flusso"?
Thorbjørn Ravn Andersen

Imposta elementi <Integer> = new HashSet (); numbers.stream (). filter (n -> i! tems.add (n)). collect (Collectors.toSet ());
Saroj Kumar Sahoo

Risposte:


127

Puoi usare Collections.frequency:

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

11
La stessa performance O (n ^ 2) della risposta di @OussamaZoghlami , anche se probabilmente più semplice. Tuttavia, ecco un voto positivo. Benvenuto in StackOverflow!
Tagir Valeev,

6
Come accennato, questa è una soluzione ^ 2 in cui esiste una soluzione lineare banale. Non lo accetterei in CR.
jwilner

3
Potrebbe essere più lento dell'opzione @Dave, ma è più carino quindi prenderò il colpo di prestazioni.
jDub9

@jwilner è il tuo punto riguardo alla soluzione n ^ 2 riferita all'uso di Collections.frequency in un filtro?
mancocapac

5
@mancocapac sì, è quadratico perché la chiamata di frequenza deve visitare ogni elemento in numeri e viene chiamata su ogni elemento. Quindi, per ogni elemento, visitiamo ogni elemento - n ^ 2 e inutilmente inefficiente.
jwilner

72

Esempio di base. La prima metà costruisce la mappa di frequenza, la seconda la riduce a un elenco filtrato. Probabilmente non efficiente come la risposta di Dave, ma più versatile (come se volessi rilevarne esattamente due ecc.)

     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
Questa risposta è quella corretta imo perché è lineare e non viola la regola del "predicato senza stato".
jwilner

55

Hai bisogno di un set ( allItemssotto) per contenere l'intero contenuto dell'array, ma questo è 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()richiede un predicato senza stato. La tua "soluzione" è sorprendentemente simile all'esempio di un predicato stateful fornito nel javadoc: docs.oracle.com/javase/8/docs/api/java/util/stream/…
Matt McHenry

1
@ MattMcHenry: significa che questa soluzione ha il potenziale per produrre un comportamento inaspettato o è solo una cattiva pratica?
IcedDante

7
@IcedDante In un caso localizzato come quello in cui sai per certo che lo streaming è sequential(), probabilmente è sicuro. Nel caso più generale in cui potrebbe essere il flusso parallel(), è praticamente garantito che si interrompa in modi strani.
Matt McHenry

5
Oltre a produrre comportamenti inaspettati in alcune situazioni, questo mescola paradigmi come sostiene Bloch che non dovresti nella terza edizione di Effective Java. Se ti ritrovi a scrivere questo, usa un ciclo for.
jwilner

6
Trovato questo in the wild utilizzato dal vincolo UniqueElements di Hibernate Validator .
Dave

14

Un modo O (n) sarebbe il seguente:

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

La complessità dello spazio raddoppierebbe in questo approccio, ma quello spazio non è uno spreco; infatti, ora abbiamo solo i duplicati solo come Set così come un altro Set con tutti i duplicati rimossi.


13

La mia libreria StreamEx che migliora i flussi Java 8 fornisce un'operazione speciale distinct(atLeast)che può conservare solo gli elementi che compaiono almeno il numero di volte specificato. Quindi il tuo problema può essere risolto in questo modo:

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

Internamente è simile alla soluzione @Dave, conta gli oggetti, per supportare altre quantità desiderate ed è parallel-friendly (usa ConcurrentHashMapper flussi parallelizzati, ma HashMapper sequenziali). Per grandi quantità di dati è possibile aumentare la velocità utilizzando .parallel().distinct(2).


26
La domanda riguarda Java Streams, non le librerie di terze parti.
ᄂ ᄀ

9

Puoi ottenere il duplicato in questo modo:

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
Non è un'operazione O (n ^ 2)?
Trejkaz

4
Prova a usarenumbers = Arrays.asList(400, 400, 500, 500);
Tagir Valeev

1
È simile alla creazione di un loop a 2 profondità? per (..) {per (..)} Solo curiosità come funziona internamente
redigaffi

Sebbene sia un buon approccio, avere streamdentro streamè costoso.
Vishwa Ratna

4

Penso che le soluzioni di base alla domanda dovrebbero essere le seguenti:

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

beh, non è consigliabile eseguire un'operazione di filtro, ma per una migliore comprensione l'ho usato, inoltre, ci dovrebbero essere dei filtri personalizzati nelle versioni future.


3

Un multiset è una struttura che mantiene il numero di occorrenze per ogni elemento. Utilizzando l'implementazione Guava:

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

2

la creazione di una mappa o di un flusso aggiuntivo richiede tempo e spazio ...

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]


... e per la cui domanda si afferma che sia un [duplicato]

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

Se hai solo bisogno di rilevare la presenza di duplicati (invece di elencarli, che è ciò che voleva l'OP), basta convertirli sia in un elenco che in un insieme, quindi confrontare le dimensioni:

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

Mi piace questo approccio perché ha meno posti per gli errori.


0

Penso di avere una buona soluzione su come risolvere un problema come questo - List => List with grouping by Something.a & Something.b. C'è una definizione estesa:

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

}

classe A, list1 sono solo dati in arrivo - la magia è in Objects.hash (...) :)


1
Avvertenza: se Objects.hashproduce lo stesso valore per (v.a_1, v.b_1, v.c_1, v.d_1)e (v.a_2, v.b_2, v.c_2, v.d_2), verranno considerati uguali e verranno rimossi come duplicati, senza verificare che a, b, c e d siano uguali. Questo potrebbe essere un rischio accettabile oppure potresti voler utilizzare una funzione diversa da quella Objects.hashgarantita per produrre un risultato univoco nel tuo dominio.
Marty Neal

0

Devi usare gli idiomi java 8 (steams)? Forse una soluzione semplice sarebbe spostare la complessità su una struttura di dati simile a una mappa che contiene i numeri come chiave (senza ripetizione) e le volte che si presenta come un valore. Potresti iterare quella mappa e fare qualcosa solo con quei numeri che sono 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

Prova questa soluzione:

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

E il controllo degli indici?

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

1
Dovrebbe funzionare bene, ma anche O (n ^ 2) prestazioni come alcune altre soluzioni qui.
Florian Albrecht
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.