HashMap ottiene / mette complessità


131

Siamo abituati a dire che le HashMap get/putoperazioni sono O (1). Tuttavia dipende dall'implementazione dell'hash. L'hash dell'oggetto predefinito è in realtà l'indirizzo interno nell'heap JVM. Siamo sicuri che sia abbastanza buono per affermare che get/putsono O (1)?

La memoria disponibile è un altro problema. Come ho capito dai javadocs, HashMap load factordovrebbe essere 0,75. Cosa succede se non disponiamo di memoria sufficiente in JVM e load factorsuperiamo il limite?

Quindi, sembra che O (1) non sia garantito. Ha senso o mi sto perdendo qualcosa?


1
Potresti voler cercare il concetto di complessità ammortizzata. Vedi ad esempio qui: stackoverflow.com/questions/3949217/time-complexity-of-hash-table La peggiore complessità del caso non è la misura più importante per una tabella hash
Dr G

3
Corretto - è ammortizzato O (1) - non dimenticare mai quella prima parte e non avrai questo tipo di domande :)
Ingegnere

Il caso peggiore della complessità temporale è O (logN) da Java 1.8 se non sbaglio.
Tarun Kolla,

Risposte:


216

Dipende da molte cose. Di solito è O (1), con un hash decente che a sua volta è un tempo costante ... ma potresti avere un hash che richiede molto tempo per il calcolo, e se ci sono più elementi nella mappa hash che restituiscono lo stesso codice hash, getdovrà iterare su di loro invitando equalsciascuno di loro a trovare una corrispondenza.

Nel peggiore dei casi, a HashMapha una ricerca O (n) a causa del passaggio attraverso tutte le voci nello stesso bucket hash (ad esempio se tutte hanno lo stesso codice hash). Fortunatamente, quello scenario peggiore non si presenta molto spesso nella vita reale, nella mia esperienza. Quindi no, O (1) non è certo garantito, ma di solito è quello che dovresti assumere quando consideri quali algoritmi e strutture di dati usare.

In JDK 8, HashMapè stato ottimizzato in modo che se le chiavi possono essere confrontate per l'ordinamento, allora qualsiasi bucket densamente popolato è implementato come un albero, in modo che anche se ci sono molte voci con lo stesso codice hash, la complessità è O (log n). Ciò può causare problemi se si dispone di un tipo di chiave in cui l'uguaglianza e l'ordine sono diversi, ovviamente.

E sì, se non hai abbastanza memoria per la mappa hash, sarai nei guai ... ma sarà vero qualunque sia la struttura di dati che usi.


@marcog: supponi O (n log n) per una singola ricerca ? Mi sembra stupido. Naturalmente dipenderà dalla complessità delle funzioni di hash e uguaglianza, ma è improbabile che dipenda dalle dimensioni della mappa.
Jon Skeet,

1
@marcog: Quindi cosa stai assumendo come O (n log n)? Inserimento di n articoli?
Jon Skeet,

1
+1 per una buona risposta. Forniresti link come questa voce di Wikipedia per la tabella hash nella tua risposta? In questo modo, il lettore più interessato potrebbe arrivare alla noia di capire perché hai dato la tua risposta.
David Weiser,

2
@SleimanJneidi: lo è ancora se la chiave non implementa Comparable <T> `- ma aggiornerò la risposta quando avrò più tempo.
Jon Skeet,

1
@ ip696: Sì, putè "O (1)" ammortizzato - di solito O (1), occasionalmente O (n) - ma raramente abbastanza per bilanciare.
Jon Skeet,

9

Non sono sicuro che l'hashcode predefinito sia l'indirizzo - ho letto la fonte OpenJDK per la generazione dell'hashcode qualche tempo fa e ricordo che era qualcosa di un po 'più complicato. Ancora non qualcosa che garantisce una buona distribuzione, forse. Tuttavia, questo è in qualche modo controverso, poiché poche classi che useresti come chiavi in ​​una hashmap usano l'hashcode predefinito: forniscono le loro implementazioni, il che dovrebbe essere buono.

Inoltre, ciò che potresti non sapere (di nuovo, questo è basato sulla lettura della fonte - non è garantito) è che HashMap mescola l'hash prima di usarlo, per mescolare l'entropia da tutta la parola nei bit inferiori, che è dove si trova necessario per tutti tranne gli hashmap più grandi. Questo aiuta a gestire gli hash che in particolare non lo fanno da soli, anche se non riesco a pensare a casi comuni in cui lo vedresti.

Infine, ciò che accade quando la tabella è sovraccarica è che degenera in una serie di liste collegate in parallelo: le prestazioni diventano O (n). In particolare, il numero di collegamenti attraversati sarà in media la metà del fattore di carico.


6
Dannazione. Ho scelto di credere che se non avessi dovuto digitare questo su un touchscreen per cellulare, avrei potuto battere Jon Sheet al massimo. C'è un distintivo per quello, giusto?
Tom Anderson,

8

L'operazione HashMap dipende dal fattore di implementazione di hashCode. Per lo scenario ideale, supponiamo che la buona implementazione dell'hash che fornisca un codice hash univoco per ogni oggetto (nessuna collisione dell'hash), lo scenario migliore, peggiore e medio sarebbe O (1). Consideriamo uno scenario in cui una cattiva implementazione di hashCode restituisce sempre 1 o tale hash che ha una collisione hash. In questo caso la complessità temporale sarebbe O (n).

Ora arriviamo alla seconda parte della domanda sulla memoria, quindi sì il vincolo di memoria sarebbe curato da JVM.


8

È già stato menzionato che gli hashmap sono O(n/m)in media, se nè il numero di elementi ed mè la dimensione. È stato anche menzionato che in linea di principio l'intera cosa potrebbe collassare in un elenco singolarmente collegato con O(n)tempo di interrogazione. (Tutto questo presuppone che il calcolo dell'hash sia un tempo costante).

Tuttavia, ciò che non viene spesso menzionato è che almeno con probabilità 1-1/n(quindi per 1000 oggetti con una probabilità del 99,9%) il secchio più grande non verrà riempito più di O(logn)! Pertanto, corrisponde alla complessità media degli alberi di ricerca binari. (E la costante è buona, un limite più stretto è (log n)*(m/n) + O(1)).

Tutto ciò che serve per questo limite teorico è che usi una funzione hash ragionevolmente buona (vedi Wikipedia: Universal Hashing . Può essere semplice come a*x>>m). E ovviamente che la persona che ti dà i valori dell'hash non sa come hai scelto le tue costanti casuali.

TL; DR: con una probabilità molto alta la peggiore delle ipotesi di ottenere / mettere la complessità di una hashmap è O(logn).


(E nota che nulla di tutto ciò assume dati casuali. La probabilità deriva esclusivamente dalla scelta della funzione hash)
Thomas Ahle,

Ho anche la stessa domanda sulla complessità di runtime di una ricerca in una mappa hash. Sembrerebbe che sia O (n) poiché si suppone che vengano eliminati fattori costanti. L'1 / m è un fattore costante e quindi viene lasciato cadere lasciando O (n).
nickdu

4

Sono d'accordo con:

  • la complessità generale ammortizzata di O (1)
  • una cattiva hashCode()implementazione potrebbe comportare più collisioni, il che significa che nel caso peggiore ogni oggetto va nello stesso bucket, quindi O ( N ) se ogni bucket è supportato da a List.
  • da Java 8, HashMapsostituisce dinamicamente i Nodi (elenco collegato) utilizzati in ogni bucket con TreeNodes (albero rosso-nero quando un elenco diventa più grande di 8 elementi) con conseguenti prestazioni peggiori di O ( logN ).

Ma questo NON è la verità se vogliamo essere precisi al 100%. L'implementazione hashCode()e il tipo di chiave Object(immutabile / memorizzata nella cache o essendo una raccolta) potrebbe anche influire sulla complessità reale in termini rigorosi.

Supponiamo che i seguenti tre casi:

  1. HashMap<Integer, V>
  2. HashMap<String, V>
  3. HashMap<List<E>, V>

Hanno la stessa complessità? Bene, la complessità ammortizzata della prima è, come previsto, O (1). Ma, per il resto, dobbiamo anche calcolare hashCode()l'elemento di ricerca, il che significa che potremmo dover attraversare matrici ed elenchi nel nostro algoritmo.

Supponiamo che la dimensione di tutti gli array / elenchi sopra sia k . Quindi, HashMap<String, V>e HashMap<List<E>, V>avrà la complessità ammortizzata O (k) e allo stesso modo, il caso peggiore di O ( k + logN ) in Java8.

* Si noti che l'utilizzo di una Stringchiave è un caso più complesso, poiché è immutabile e Java memorizza nella cache il risultato hashCode()in una variabile privata hash, quindi viene calcolata una sola volta.

/** Cache the hash code for the string */
    private int hash; // Default to 0

Ma quanto sopra ha anche il suo caso peggiore, poiché l' String.hashCode()implementazione di Java sta verificando se hash == 0prima dell'informatica hashCode. Ma ehi, ci sono stringhe non vuote che producono uno hashcodezero, come "f5a5a608", vedi qui , nel qual caso la memoizzazione potrebbe non essere utile.


2

In pratica, è O (1), ma in realtà è una semplificazione terribile e matematicamente senza senso. La notazione O () dice come si comporta l'algoritmo quando la dimensione del problema tende all'infinito. Hashmap get / put funziona come un algoritmo O (1) per dimensioni limitate. Il limite è abbastanza grande dalla memoria del computer e dal punto di vista dell'indirizzamento, ma lontano dall'infinito.

Quando si dice che hashmap get / put è O (1), si dovrebbe davvero dire che il tempo necessario per get / put è più o meno costante e non dipende dal numero di elementi nell'hashmap nella misura in cui l'hashmap può essere presentato sul sistema informatico reale. Se il problema va oltre quella dimensione e abbiamo bisogno di hashmap più grandi, dopo un po 'di certo aumenterà anche il numero dei bit che descrivono un elemento man mano che finiamo i possibili diversi elementi descrivibili. Ad esempio, se abbiamo usato una hashmap per memorizzare numeri a 32 bit e successivamente aumentiamo la dimensione del problema in modo da includere nella hashmap più di 2 ^ 32 bit, allora i singoli elementi verranno descritti con più di 32 bit.

Il numero dei bit necessari per descrivere i singoli elementi è log (N), dove N è il numero massimo di elementi, quindi get e put sono davvero O (log N).

Se lo confronti con un set di alberi, che è O (log n), allora il set di hash è O (long (max (n)) e riteniamo semplicemente che questo sia O (1), perché su una certa implementazione max (n) è fisso, non cambia (la dimensione degli oggetti che memorizziamo misurata in bit) e l'algoritmo che calcola il codice hash è veloce.

Infine, se trovare un elemento in qualsiasi struttura di dati fosse O (1), creeremmo informazioni dal nulla. Avendo una struttura di dati di n elemento posso selezionare un elemento in n modo diverso. Con ciò, posso codificare le informazioni del bit di registro (n). Se riesco a codificarlo in zero bit (questo è ciò che significa O (1)) allora ho creato un algoritmo ZIP a compressione infinita.


Non dovrebbe essere la complessità per il set di alberi O(log(n) * log(max(n))), allora? Mentre il confronto in ogni nodo può essere più intelligente, nel peggiore dei casi deve ispezionare tutti i O(log(max(n))bit, giusto?
maaartinus,
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.