Qual è il modo più veloce per confrontare due set in Java?


102

Sto cercando di ottimizzare un pezzo di codice che confronta gli elementi della lista.

Per esempio.

public void compare(Set<Record> firstSet, Set<Record> secondSet){
    for(Record firstRecord : firstSet){
        for(Record secondRecord : secondSet){
            // comparing logic
        }
    }
}

Tieni presente che il numero di record nei set sarà elevato.

Grazie

Shekhar


7
Non è possibile ottimizzare i loop senza conoscere (e modificare) la logica di confronto. Potresti mostrare più codice?
josefx

Risposte:


161
firstSet.equals(secondSet)

Dipende molto da cosa vuoi fare nella logica di confronto ... cioè cosa succede se trovi un elemento in un insieme non nell'altro? Il tuo metodo ha un voidtipo di ritorno quindi presumo che farai il lavoro necessario con questo metodo.

Controllo più preciso se necessario:

if (!firstSet.containsAll(secondSet)) {
  // do something if needs be
}
if (!secondSet.containsAll(firstSet)) {
  // do something if needs be
}

Se hai bisogno di ottenere gli elementi che sono in un set e non nell'altro.
EDIT: set.removeAll(otherSet)restituisce un booleano, non un set. Per usare removeAll (), dovrai copiare il set e usarlo.

Set one = new HashSet<>(firstSet);
Set two = new HashSet<>(secondSet);
one.removeAll(secondSet);
two.removeAll(firstSet);

Se i contenuti di onee twosono entrambi vuoti, allora sai che i due insiemi erano uguali. In caso contrario, hai gli elementi che hanno reso gli insiemi disuguali.

Hai detto che il numero di record potrebbe essere elevato. Se l'implementazione sottostante è una, HashSetil recupero di ogni record viene eseguito in O(1)tempo, quindi non puoi davvero ottenere molto meglio di così. TreeSetè O(log n).


3
L'implementazione di equals () e hashcode () per la classe Record è altrettanto importante quando si richiama equals () sul set.
Vineet Reynolds,

1
Non sono sicuro che gli esempi removeAll () siano corretti. removeAll () restituisce un valore booleano, non un altro Set. Gli elementi in secondSet vengono effettivamente rimossi da firstSet e viene restituito true se è stata apportata una modifica.
Richard Corfield

4
L'esempio removeAll non è ancora corretto perché non hai fatto copie (Set one = firstSet; Set two = secondSet). Userei il costruttore di copie.
Michael Rusch

1
In realtà, l'implementazione predefinita di equalsè più veloce di due chiamate a containsAllnel caso peggiore; vedere la mia risposta.
Stephen C

6
Devi fare Set one = new HashSet (firstSet), altrimenti gli elementi di firstSet e secondSet verranno rimossi.
Bonton255

61

Se vuoi semplicemente sapere se i set sono uguali, il equalsmetodo su AbstractSetè implementato più o meno come di seguito:

    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Set))
            return false;
        Collection c = (Collection) o;
        if (c.size() != size())
            return false;
        return containsAll(c);
    }

Nota come ottimizza i casi comuni in cui:

  • i due oggetti sono gli stessi
  • l'altro oggetto non è affatto un insieme, e
  • le dimensioni dei due set sono diverse.

Dopodiché, containsAll(...)tornerà falsenon appena troverà un elemento nell'altro set che non è anche in questo set. Ma se tutti gli elementi sono presenti in entrambi i set, sarà necessario testarli tutti.

La performance nel caso peggiore si verifica quindi quando i due set sono uguali ma non gli stessi oggetti. Tale costo è tipicamente O(N)o O(NlogN)dipende dall'implementazione di this.containsAll(c).

E ottieni prestazioni quasi peggiori se i set sono grandi e differiscono solo in una piccola percentuale degli elementi.


AGGIORNARE

Se sei disposto a investire tempo in un'implementazione di set personalizzato, esiste un approccio che può migliorare il caso "quasi lo stesso".

L'idea è che devi pre-calcolare e memorizzare nella cache un hash per l'intero set in modo da poter ottenere il valore hashcode corrente del set in O(1). Quindi puoi confrontare il codice hash per i due set come un'accelerazione.

Come potresti implementare un codice hash del genere? Ebbene, se il codice hash impostato fosse:

  • zero per un insieme vuoto e
  • lo XOR di tutti i codici hash degli elementi per un insieme non vuoto,

quindi potresti aggiornare a buon mercato l'hashcode memorizzato nella cache del set ogni volta che aggiungi o rimuovi un elemento. In entrambi i casi, devi semplicemente XOR il codice hash dell'elemento con il codice hash impostato corrente.

Naturalmente, ciò presuppone che i codici hash degli elementi siano stabili mentre gli elementi sono membri di insiemi. Presume inoltre che la funzione hashcode delle classi di elementi dia una buona diffusione. Questo perché quando i due codici hash impostati sono gli stessi devi ancora ricorrere al O(N)confronto di tutti gli elementi.


Potresti portare questa idea un po 'oltre ... almeno in teoria.

ATTENZIONE - Questo è altamente speculativo. Un "esperimento mentale", se vuoi.

Supponiamo che la tua classe di elementi set abbia un metodo per restituire un checksum crittografico per l'elemento. Ora implementa i checksum del set XORing dei checksum restituiti per gli elementi.

Cosa ci fa guadagnare?

Bene, se assumiamo che non stia succedendo nulla di subdolo, la probabilità che due elementi di insieme disuguali abbiano lo stesso checksum di N bit è 2 -N . E anche la probabilità che 2 insiemi disuguali abbiano gli stessi checksum a N bit è 2 -N . Quindi la mia idea è che puoi implementare equalscome:

    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Set))
            return false;
        Collection c = (Collection) o;
        if (c.size() != size())
            return false;
        return checksums.equals(c.checksums);
    }

In base alle ipotesi di cui sopra, questo ti darà la risposta sbagliata solo una volta ogni 2 -N . Se si imposta N abbastanza grande (ad esempio 512 bit) la probabilità di una risposta sbagliata diventa trascurabile (ad esempio circa 10 -150 ).

Lo svantaggio è che il calcolo dei checksum crittografici per gli elementi è molto costoso, soprattutto all'aumentare del numero di bit. Quindi hai davvero bisogno di un meccanismo efficace per memorizzare i checksum. E questo potrebbe essere problematico.

E l'altro aspetto negativo è che una probabilità di errore diversa da zero può essere inaccettabile, non importa quanto piccola sia la probabilità. (Ma se questo è il caso ... come gestisci il caso in cui un raggio cosmico capovolge un bit critico? O se capovolge simultaneamente lo stesso bit in due istanze di un sistema ridondante?)


Dovrebbe essere if (checksumsDoNotMatch (0)) return false; altrimenti return doHeavyComparisonToMakeSureTheSetsReallyMatch (o);
Esko Piirainen

Non necessariamente. Se la probabilità di due checksum corrispondenti per insiemi non uguali, è abbastanza piccola, suppongo che tu possa saltare il confronto. Fai i conti.
Stephen C

17

C'è un metodo in Guava Setsche può aiutare qui:

public static <E>  boolean equals(Set<? extends E> set1, Set<? extends E> set2){
return Sets.symmetricDifference(set1,set2).isEmpty();
}

5

Hai la seguente soluzione da https://www.mkyong.com/java/java-how-to-compare-two-sets/

public static boolean equals(Set<?> set1, Set<?> set2){

    if(set1 == null || set2 ==null){
        return false;
    }

    if(set1.size() != set2.size()){
        return false;
    }

    return set1.containsAll(set2);
}

O se preferisci utilizzare una singola dichiarazione di reso:

public static boolean equals(Set<?> set1, Set<?> set2){

  return set1 != null 
    && set2 != null 
    && set1.size() == set2.size() 
    && set1.containsAll(set2);
}

O forse semplicemente usa il equals()metodo da AbstractSet(fornito con JDK) che è quasi la stessa della soluzione qui ad eccezione dei controlli null aggiuntivi . Java-11 Set Interface
Chaithu Narayana

4

Esiste una soluzione O (N) per casi molto specifici in cui:

  • i set vengono entrambi ordinati
  • entrambi ordinati nello stesso ordine

Il codice seguente presuppone che entrambi i set siano basati su record confrontabili. Un metodo simile potrebbe essere basato su un comparatore.

    public class SortedSetComparitor <Foo extends Comparable<Foo>> 
            implements Comparator<SortedSet<Foo>> {

        @Override
        public int compare( SortedSet<Foo> arg0, SortedSet<Foo> arg1 ) {
            Iterator<Foo> otherRecords = arg1.iterator();
            for (Foo thisRecord : arg0) {
                // Shorter sets sort first.
                if (!otherRecords.hasNext()) return 1;
                int comparison = thisRecord.compareTo(otherRecords.next());
                if (comparison != 0) return comparison;
            }
            // Shorter sets sort first
            if (otherRecords.hasNext()) return -1;
            else return 0;
        }
    }

3

Se stai usando la Guavalibreria è possibile fare:

        SetView<Record> added = Sets.difference(secondSet, firstSet);
        SetView<Record> removed = Sets.difference(firstSet, secondSet);

E poi trarre una conclusione basata su questi.


2

Metterei il secondSet in una HashMap prima del confronto. In questo modo ridurrai il tempo di ricerca della seconda lista a n (1). Come questo:

HashMap<Integer,Record> hm = new HashMap<Integer,Record>(secondSet.size());
int i = 0;
for(Record secondRecord : secondSet){
    hm.put(i,secondRecord);
    i++;
}
for(Record firstRecord : firstSet){
    for(int i=0; i<secondSet.size(); i++){
    //use hm for comparison
    }
}

Oppure puoi usare un array invece di una hashmap per il secondo elenco.
Sahin Habesoglu

E questa soluzione presuppone che gli insiemi non siano ordinati.
Sahin Habesoglu

1
public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Set))
            return false;

        Set<String> a = this;
        Set<String> b = o;
        Set<String> thedifference_a_b = new HashSet<String>(a);


        thedifference_a_b.removeAll(b);
        if(thedifference_a_b.isEmpty() == false) return false;

        Set<String> thedifference_b_a = new HashSet<String>(b);
        thedifference_b_a.removeAll(a);

        if(thedifference_b_a.isEmpty() == false) return false;

        return true;
    }

-1

Penso che il riferimento al metodo con il metodo uguale possa essere utilizzato. Partiamo dal presupposto che il tipo di oggetto abbia senza ombra di dubbio il proprio metodo di confronto. Un esempio chiaro e semplice è qui,

Set<String> set = new HashSet<>();
set.addAll(Arrays.asList("leo","bale","hanks"));

Set<String> set2 = new HashSet<>();
set2.addAll(Arrays.asList("hanks","leo","bale"));

Predicate<Set> pred = set::equals;
boolean result = pred.test(set2);
System.out.println(result);   // true

1
questo è un modo complicato per direset.equals(set2)
Alex
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.