Hashset vs Treeset


497

Ho sempre amato gli alberi, così belli O(n*log(n))e ordinati. Tuttavia, ogni ingegnere del software che abbia mai conosciuto mi ha chiesto chiaramente perché avrei usato un TreeSet. Da un background CS, non penso che importi molto di quello che usi, e non mi interessa fare confusione con le funzioni hash e i bucket (nel caso di Java).

In quali casi dovrei usare un HashSetover a TreeSet?

Risposte:


861

HashSet è molto più veloce di TreeSet (tempo costante contro log-time per la maggior parte delle operazioni come aggiungi, rimuovi e contiene) ma non offre garanzie di ordinazione come TreeSet.

HashSet

  • la classe offre prestazioni costanti nel tempo per le operazioni di base (aggiungi, rimuovi, contiene e dimensioni).
  • non garantisce che l'ordine degli elementi rimarrà costante nel tempo
  • le prestazioni dell'iterazione dipendono dalla capacità iniziale e dal fattore di carico di HashSet.
    • È abbastanza sicuro accettare il fattore di carico predefinito ma potresti voler specificare una capacità iniziale che è circa il doppio della dimensione a cui prevedi che il set cresca.

TreeSet

  • garantisce il log (n) tempo costo per le operazioni di base (aggiungi, rimuovi e contiene)
  • garantisce che gli elementi dell'insieme saranno ordinati (crescente, naturale o quello specificato da te tramite il suo costruttore) (implementa SortedSet )
  • non offre alcun parametro di ottimizzazione per le prestazioni di iterazione
  • offre alcuni metodi pratici per affrontare l'insieme ordinato come first(), last(), headSet(), e tailSet()ecc

Punti importanti:

  • Entrambi garantiscono una raccolta di elementi senza duplicati
  • In genere è più veloce aggiungere elementi a HashSet e quindi convertire la raccolta in TreeSet per un attraversamento ordinato privo di duplicati.
  • Nessuna di queste implementazioni è sincronizzata. Cioè se più thread accedono a un set contemporaneamente e almeno uno dei thread modifica il set, deve essere sincronizzato esternamente.
  • LinkedHashSet è in qualche modo intermedio tra HashSete TreeSet. Implementato come una tabella hash con un elenco collegato che lo attraversa, tuttavia, fornisce un'iterazione ordinata per inserzione che non è la stessa della traversata ordinata garantita da TreeSet .

Quindi una scelta di utilizzo dipende interamente dalle tue esigenze ma ritengo che anche se hai bisogno di una raccolta ordinata, dovresti comunque preferire HashSet per creare il set e poi convertirlo in TreeSet.

  • per esempio SortedSet<String> s = new TreeSet<String>(hashSet);

38
Sono solo io a trovare l'affermazione "HashSet è molto più veloce di TreeSet (tempo costante contro tempo di registro ...)" chiaramente sbagliato? Prima di tutto si tratta della complessità temporale, non del tempo assoluto, e O (1) può essere in troppi casi più lento di O (f (N)). Secondo che O (logN) è "quasi" O (1). Non sarei sorpreso se per molti casi comuni un TreeSet superasse un HashSet.
lvella,

22
Voglio solo secondare il commento di Ivella. la complessità temporale NON è la stessa cosa del tempo di esecuzione e O (1) non è sempre migliore di O (2 ^ n). Un esempio perverso illustra il punto: considera un set di hash usando un algoritmo di hash che ha eseguito 1 trilione di istruzioni macchina per eseguire (O (1)) rispetto a qualsiasi implementazione comune di bubble sort (O (N ^ 2) avg / worst) per 10 elementi . L'ordinamento a bolle vincerà ogni volta. Il punto è che le classi di algoritmi insegnano a tutti a pensare alle approssimazioni usando la complessità del tempo, ma nel mondo reale i fattori costanti SONO frequenti.
Peter Oehlert,

17
Forse sono solo io, ma non è il consiglio di aggiungere prima tutto a un hashset, e poi convertirlo in un albero è orribile? 1) L'inserimento in un hashset è rapido solo se si conosce in anticipo la dimensione del proprio set di dati, altrimenti si paga un O (n) ri-hashing, possibilmente più volte. e 2) Si paga comunque per l'inserimento di TreeSet quando si converte il set. (con una vendetta, perché l'iterazione attraverso un hashset non è terribilmente efficiente)
TinkerTank

5
Questo consiglio si basa sul fatto che per un set è necessario verificare se un articolo è un duplicato prima di aggiungerlo; pertanto risparmierai tempo eliminando i duplicati se stai usando un hashset su un set di alberi. Tuttavia, considerando il prezzo da pagare per la creazione di un secondo set per i non duplicati, la percentuale di duplicati dovrebbe essere davvero eccezionale per superare questo prezzo e renderlo un risparmio di tempo. E, naturalmente, questo è per set medi e grandi perché per un set piccolo, il set di alberi è probabilmente più veloce di un hashset.
SylvainL

5
@PeterOehlert: fornisci un punto di riferimento per questo. Capisco il tuo punto, ma la differenza tra entrambi i set ha poca importanza con le piccole dimensioni della raccolta. E non appena il set cresce fino a un punto in cui l'implementazione è importante, log (n) sta diventando un problema. In generale, le funzioni di hash (anche complesse) sono molto più rapide rispetto a diverse mancate cache (che hai su alberi enormi per quasi tutti i livelli di accesso) per trovare / accedere / aggiungere / modificare la foglia. Almeno questa è la mia esperienza con questi due set in Java.
Bouncner,

38

Un vantaggio non ancora menzionato di a TreeSetè che ha una maggiore "località", che è una scorciatoia per dire (1) se due voci sono vicine nell'ordine, una le TreeSetcolloca una accanto all'altra nella struttura dei dati, e quindi nella memoria; e (2) questo posizionamento sfrutta il principio della località, secondo il quale l'accesso a dati simili è spesso accessibile da un'applicazione con frequenza simile.

Questo è in contrasto con a HashSet, che diffonde le voci su tutta la memoria, indipendentemente dalle loro chiavi.

Quando il costo di latenza della lettura da un disco rigido è migliaia di volte il costo della lettura dalla cache o dalla RAM e quando si accede realmente ai dati con la località, TreeSetpuò essere una scelta molto migliore.


3
Puoi dimostrare che se due voci sono vicine nell'ordine, un TreeSet le posiziona una accanto all'altra nella struttura dei dati, e quindi nella memoria ?
David Soroko,

6
Abbastanza irrilevante per Java. Gli elementi dell'insieme sono comunque Oggetti e puntano altrove, quindi non stai risparmiando granché.
Andrew Gallasch,

Oltre agli altri commenti fatti sulla mancanza di località in Java in generale, l'implementazione di OpenJDK di TreeSet/ TreeMapnon è ottimizzata per località. Mentre è possibile utilizzare un b-tree di ordine 4 per rappresentare un albero rosso-nero e quindi migliorare le prestazioni di localizzazione e cache, non è così che funziona l'implementazione. Al contrario, ogni nodo memorizza un puntatore alla propria chiave, al proprio valore, al suo genitore e ai suoi nodi figlio sinistro e destro, evidente nel codice sorgente JDK 8 per TreeMap.Entry .
kbolino,

25

HashSetè O (1) per accedere agli elementi, quindi sicuramente importa. Ma mantenere l'ordine degli oggetti nel set non è possibile.

TreeSetè utile se il mantenimento di un ordine (in termini di valori e non di inserimento) è importante per te. Ma, come hai notato, stai scambiando l'ordine per un tempo più lento per accedere a un elemento: O (log n) per le operazioni di base.

Dai javadocs perTreeSet :

Questa implementazione fornisce costo log (n) garantito per le operazioni di base ( add, removee contains).


22

1.HashSet consente l'oggetto null.

2.TreeSet non consentirà l'oggetto null. Se si tenta di aggiungere un valore null, verrà generata una NullPointerException.

3.HashSet è molto più veloce di TreeSet.

per esempio

 TreeSet<String> ts = new TreeSet<String>();
 ts.add(null); // throws NullPointerException

 HashSet<String> hs = new HashSet<String>();
 hs.add(null); // runs fine

3
ts.add (null) funzionerà bene in caso di TreeSet se null viene aggiunto come primo oggetto in TreeSet. E qualsiasi oggetto aggiunto successivamente fornirà NullPointerException nel metodo compareTo di Comparator.
Shoaib Chikate,

2
Non dovresti davvero aggiungere il nulltuo set in entrambi i modi.
soffice

TreeSet<String> badassTreeSet = new TreeSet<String>(new Comparator<String>() { public int compare(String string1, String string2) { if (string1 == null) { return (string2 == null) ? 0 : -1; } else if (string2 == null) { return 1; } else { return string1.compareTo(string2); } } }); badassTreeSet.add("tree"); badassTreeSet.add("asdf"); badassTreeSet.add(null); badassTreeSet.add(null); badassTreeSet.add("set"); badassTreeSet.add("tree"); System.out.println(badassTreeSet);
Dávid Horváth,

21

Basandomi sulla bella risposta visiva su Maps di @shevchyk, ecco la mia opinione:

╔══════════════╦═════════════════════╦═══════════════════╦═════════════════════╗
   Property          HashSet             TreeSet           LinkedHashSet   
╠══════════════╬═════════════════════╬═══════════════════╬═════════════════════╣
                no guarantee order  sorted according                       
   Order       will remain constant to the natural        insertion-order  
                    over time          ordering                            
╠══════════════╬═════════════════════╬═══════════════════╬═════════════════════╣
 Add/remove           O(1)              O(log(n))             O(1)         
╠══════════════╬═════════════════════╬═══════════════════╬═════════════════════╣
                                      NavigableSet                         
  Interfaces           Set                Set                  Set         
                                       SortedSet                           
╠══════════════╬═════════════════════╬═══════════════════╬═════════════════════╣
                                       not allowed                         
  Null values        allowed        1st element only        allowed        
                                        in Java 7                          
╠══════════════╬═════════════════════╩═══════════════════╩═════════════════════╣
                 Fail-fast behavior of an iterator cannot be guaranteed      
   Fail-fast   impossible to make any hard guarantees in the presence of     
   behavior              unsynchronized concurrent modification              
╠══════════════╬═══════════════════════════════════════════════════════════════╣
      Is                                                                     
 synchronized               implementation is not synchronized               
╚══════════════╩═══════════════════════════════════════════════════════════════╝

13

Il motivo per cui la maggior parte degli usi HashSetè che le operazioni sono (in media) O (1) anziché O (log n). Se il set contiene elementi standard, non dovrai "scherzare con le funzioni hash" come è stato fatto per te. Se il set contiene classi personalizzate, devi implementarlo hashCodeper usare HashSet(anche se Java efficace mostra come), ma se usi un TreeSetdevi farlo Comparableo fornire unComparator . Questo può essere un problema se la classe non ha un ordine particolare.

A volte l'ho usato TreeSet(o effettivamenteTreeMap ) insiemi / mappe molto piccoli (<10 articoli) anche se non ho verificato per vedere se c'è qualche guadagno reale nel farlo. Per grandi set la differenza può essere considerevole.

Ora, se hai bisogno dell'ordinamento, allora TreeSetè appropriato, anche se anche se gli aggiornamenti sono frequenti e la necessità di un risultato ordinato è poco frequente, a volte copiare i contenuti in un elenco o in un array e ordinarli può essere più veloce.


qualsiasi dato punta su per questi elementi di grandi dimensioni come 10K o più
kuhajeyan

11

Se non stai inserendo elementi sufficienti per provocare frequenti rehash (o collisioni, se il tuo HashSet non può ridimensionare), un HashSet ti offre sicuramente il vantaggio di un accesso costante nel tempo. Ma sui set con molta crescita o contrazione, potresti effettivamente ottenere prestazioni migliori con Treeset, a seconda dell'implementazione.

Il tempo ammortizzato può essere vicino a O (1) con un albero rosso-nero funzionale, se la memoria mi serve. Il libro di Okasaki avrebbe una spiegazione migliore di quella che posso fare. (O vedi il suo elenco di pubblicazioni )


7

Le implementazioni di HashSet sono, ovviamente, molto più veloci - meno sovraccarico perché non c'è ordine. Una buona analisi delle varie implementazioni di Set in Java è fornita su http://java.sun.com/docs/books/tutorial/collections/implementations/set.html .

La discussione in questa sede evidenzia anche un interessante approccio di "via di mezzo" alla domanda Tree vs Hash. Java fornisce un LinkedHashSet, che è un HashSet con un elenco collegato "orientato all'inserzione" che lo attraversa, ovvero l'ultimo elemento dell'elenco collegato è anche l'ultimo inserito nell'Hash. Ciò consente di evitare l'irregolarità di un hash non ordinato senza incorrere nel costo aumentato di un TreeSet.


4

Il TreeSet è uno dei due raccolta differenziata (l'altro è TreeMap). Utilizza una struttura ad albero rosso-nero (ma lo sapevi) e garantisce che gli elementi saranno in ordine crescente, secondo l'ordine naturale. Opzionalmente, puoi costruire un TreeSet con un costruttore che ti consenta di dare alla collezione le tue regole per ciò che dovrebbe essere l'ordine (piuttosto che fare affidamento sull'ordinamento definito dalla classe degli elementi) usando un Comparable o Comparator

e LinkedHashSet è una versione ordinata di HashSet che mantiene un elenco doppiamente collegato tra tutti gli elementi. Usa questa classe invece di HashSet quando ti preoccupi dell'ordine di iterazione. Quando si itera attraverso un HashSet l'ordine è imprevedibile, mentre un LinkedHashSet consente di scorrere gli elementi nell'ordine in cui sono stati inseriti


3

Sono state fornite molte risposte, basate su considerazioni tecniche, in particolare riguardo alle prestazioni. Secondo me, la scelta tra TreeSete HashSetconta.

Ma preferirei dire che la scelta dovrebbe essere guidata prima da considerazioni concettuali .

Se, per gli oggetti che hai bisogno di manipolare, un ordinamento naturale non ha senso, allora non usare TreeSet.
È un insieme ordinato, poiché implementa SortedSet. Quindi significa che è necessario ignorare la funzione avrebbe senso, dal momento che non esiste un ordinamento naturale tra gli studenti. Puoi ordinarli in base al loro voto medio, ok, ma questo non è un "ordinamento naturale". FunzionecompareTo , che dovrebbe essere coerente con ciò che restituisce la funzione equals. Ad esempio, se hai una serie di oggetti di una classe chiamata Studente, allora non penso aTreeSetcompareTorestituirebbe 0 non solo quando due oggetti rappresentano lo stesso studente, ma anche quando due studenti diversi hanno lo stesso voto. Per il secondo caso, equalsrestituirebbe false (a meno che non si decida di rendere quest'ultimo restituito vero quando due studenti diversi hanno lo stesso voto, il che renderebbe la equalsfunzione ha un significato fuorviante, per non dire un significato sbagliato.)
Si noti che questa coerenza tra equalse compareToè facoltativo, ma fortemente raccomandato. Altrimenti il ​​contratto di interfacciaSet si interrompe, rendendo il codice fuorviante per altre persone, portando quindi anche a comportamenti imprevisti.

Questo link potrebbe essere una buona fonte di informazioni riguardo a questa domanda.


3

Perché avere le mele quando puoi avere le arance?

Seriamente ragazzi e ragazze: se la vostra collezione è grande, letta e scritta in milioni di volte e state pagando per i cicli della CPU, la scelta della collezione è rilevante SOLO se AVETE BISOGNO di prestazioni migliori. Tuttavia, nella maggior parte dei casi, questo non ha molta importanza: alcuni millisecondi qua e là passano inosservati in termini umani. Se è davvero così importante, perché non stai scrivendo codice in assembler o C? [cue un'altra discussione]. Quindi il punto è se sei felice di usare qualsiasi collezione tu abbia scelto, e risolve il tuo problema [anche se non è specificamente il miglior tipo di raccolta per l'attività] buttati fuori. Il software è malleabile. Ottimizza il tuo codice dove necessario. Lo zio Bob afferma che l'ottimizzazione precoce è la radice di tutti i mali. Lo dice lo zio Bob


1

Modifica messaggio ( riscrittura completa ) Quando l'ordine non ha importanza, ecco quando. Entrambi dovrebbero dare Log (n) - sarebbe utile vedere se l'uno o l'altro è più del cinque percento più veloce dell'altro. HashSet può dare O (1) test in un ciclo dovrebbe rivelare se lo è.


-3
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;

public class HashTreeSetCompare {

    //It is generally faster to add elements to the HashSet and then
    //convert the collection to a TreeSet for a duplicate-free sorted
    //Traversal.

    //really? 
    O(Hash + tree set) > O(tree set) ??
    Really???? Why?



    public static void main(String args[]) {

        int size = 80000;
        useHashThenTreeSet(size);
        useTreeSetOnly(size);

    }

    private static void useTreeSetOnly(int size) {

        System.out.println("useTreeSetOnly: ");
        long start = System.currentTimeMillis();
        Set<String> sortedSet = new TreeSet<String>();

        for (int i = 0; i < size; i++) {
            sortedSet.add(i + "");
        }

        //System.out.println(sortedSet);
        long end = System.currentTimeMillis();

        System.out.println("useTreeSetOnly: " + (end - start));
    }

    private static void useHashThenTreeSet(int size) {

        System.out.println("useHashThenTreeSet: ");
        long start = System.currentTimeMillis();
        Set<String> set = new HashSet<String>();

        for (int i = 0; i < size; i++) {
            set.add(i + "");
        }

        Set<String> sortedSet = new TreeSet<String>(set);
        //System.out.println(sortedSet);
        long end = System.currentTimeMillis();

        System.out.println("useHashThenTreeSet: " + (end - start));
    }
}

1
Il post diceva che è generalmente più veloce aggiungere elementi a HashSet e quindi convertire la raccolta in TreeSet per un attraversamento ordinato senza duplicati. Impostare <String> s = new TreeSet <String> (hashSet); Mi chiedo perché non impostare <String> s = new TreeSet <String> () direttamente se sappiamo che verrà utilizzato per l'iterazione ordinata, quindi ho fatto questo confronto e il risultato ha mostrato quale è più veloce.
gli00001,

"In quali casi dovrei usare un HashSet su un TreeSet?"
Austin Henley,

1
il punto è che, se hai bisogno di ordinare, usare TreeSet da solo è meglio che mettere tutto in HashSet, quindi creare un TreeSet basato su tale HashSet. Non vedo affatto il valore di HashSet + TreeSet dal post originale.
gli00001,

@ gli00001: hai perso il punto. Se non hai sempre bisogno di ordinare il tuo set di elementi, ma lo manipolerai piuttosto spesso, allora ne varrà la pena per te utilizzare un hashset per beneficiare delle operazioni più veloci per la maggior parte del tempo. Per i momenti occasionali in cui è necessario elaborare gli elementi in ordine, quindi semplicemente avvolgere con un set di alberi. Dipende dal tuo caso d'uso, ma non è molto simile a un caso d'uso comune (e questo probabilmente presuppone un insieme che non contiene troppi elementi e con regole di ordinamento complesse).
Hayylem,
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.