Una hashmap Java è davvero O (1)?


159

Ho visto alcune affermazioni interessanti su SO con hashaps Java e il loro O(1)tempo di ricerca. Qualcuno può spiegare perché è così? A meno che questi hashmap non siano molto diversi dagli algoritmi di hashing su cui sono stato acquistato, deve sempre esistere un set di dati che contenga collisioni.

In tal caso, la ricerca sarebbe O(n)piuttosto che O(1).

Qualcuno può spiegare se sono O (1) e, in tal caso, come ottengono questo risultato?


1
So che questa potrebbe non essere una risposta, ma ricordo che Wikipedia ha un ottimo articolo su questo. Non perdere la sezione di analisi delle prestazioni
victor hugo,

28
La notazione O grande dà un limite superiore per il particolare tipo di analisi che stai facendo. Devi comunque specificare se sei interessato al caso peggiore, al caso medio, ecc.
Dan Homerick,

Risposte:


127

Una caratteristica particolare di una HashMap è che a differenza, diciamo, di alberi bilanciati, il suo comportamento è probabilistico. In questi casi sarebbe di solito più utile parlare di complessità in termini di probabilità che si verifichi un evento nel caso peggiore. Per una mappa hash, questo è ovviamente il caso di una collisione rispetto a quanto la mappa sembra piena. Una collisione è abbastanza facile da stimare.

p collisione = n / capacità

Quindi è molto probabile che una mappa hash con un numero anche modesto di elementi subisca almeno una collisione. La notazione O grande ci consente di fare qualcosa di più avvincente. Osservare che per qualsiasi costante arbitraria fissa k.

O (n) = O (k * n)

Possiamo usare questa funzione per migliorare le prestazioni della mappa hash. Potremmo invece pensare alla probabilità di al massimo 2 collisioni.

p collisione x 2 = (n / capacità) 2

Questo è molto più basso. Poiché il costo di gestione di una collisione aggiuntiva è irrilevante per le prestazioni di Big O, abbiamo trovato un modo per migliorare le prestazioni senza modificare l'algoritmo! Possiamo generalzie questo a

p collisione xk = (n / capacità) k

E ora possiamo ignorare un numero arbitrario di collisioni e finire con una probabilità evanescente minuscola di più collisioni di quelle che stiamo spiegando. Puoi ottenere la probabilità a un livello arbitrariamente piccolo scegliendo il k corretto, il tutto senza alterare l'implementazione effettiva dell'algoritmo.

Ne parliamo dicendo che la mappa hash ha accesso O (1) con alta probabilità


Anche con HTML, non sono ancora molto contento delle frazioni. Puliscili se riesci a pensare a un modo carino per farlo.
SingleNegationElimination

4
In realtà, ciò che dice sopra è che gli effetti O (log N) sono sepolti, per valori non estremi di N, dall'overhead fisso.
Hot Licks,

Tecnicamente, quel numero che hai dato è il valore atteso del numero di collisioni, che può eguagliare la probabilità di una singola collisione.
Simon Kuang,

1
È simile all'analitica ammortizzata?
lostsoul29,

1
@ OleV.V. le buone prestazioni di una HashMap dipendono sempre da una buona distribuzione della funzione hash. Puoi scambiare una migliore qualità dell'hash con la velocità di hashing usando una funzione di hash crittografica sul tuo input.
SingleNegationElimination

38

Sembra che mescoli il comportamento del caso peggiore con il runtime nel caso medio (previsto). Il primo è effettivamente O (n) per le tabelle di hash in generale (cioè non usando un hashing perfetto) ma questo è raramente rilevante nella pratica.

Qualsiasi implementazione affidabile della tabella hash, unita a un hash decente mezzo, ha una performance di recupero di O (1) con un fattore molto piccolo (2, in effetti) nel caso previsto, con un margine di varianza molto stretto.


6
Ho sempre pensato che il limite superiore fosse il caso peggiore, ma sembra che mi sia sbagliato - puoi avere il limite superiore per il caso medio. Quindi sembra che le persone che rivendicano O (1) avrebbero dovuto chiarire che era per un caso medio. Il caso peggiore è un set di dati in cui ci sono molte collisioni che lo rendono O (n). Adesso ha senso.
paxdiablo,

2
Probabilmente dovresti chiarire che quando usi la notazione O grande per il caso medio stai parlando di un limite superiore della funzione di runtime prevista che è una funzione matematica chiaramente definita. Altrimenti la tua risposta non ha molto senso.
Cane,

1
gmatt: Non sono sicuro di aver capito la tua obiezione: la notazione big-O è un limite superiore alla funzione per definizione . Cos'altro potrei quindi dire?
Konrad Rudolph,

3
di solito nella letteratura informatica si vede una grande notazione O che rappresenta un limite superiore per le funzioni di runtime o complessità dello spazio di un algoritmo. In questo caso il limite superiore è in realtà sull'aspettativa che non è essa stessa una funzione ma un operatore di funzioni (variabili casuali) ed è in realtà un integrale (lebesgue.) Il fatto stesso che è possibile legare una cosa del genere non dovrebbe essere preso per scontato e non è banale.
martedì

31

In Java, HashMap funziona utilizzando hashCode per individuare un bucket. Ogni bucket è un elenco di elementi che risiedono in quel bucket. Gli articoli vengono scansionati, usando uguale a confronto. Quando si aggiungono elementi, HashMap viene ridimensionato una volta raggiunta una determinata percentuale di carico.

Quindi, a volte dovrà confrontarsi con alcuni elementi, ma generalmente è molto più vicino a O (1) rispetto a O (n). Ai fini pratici, questo è tutto ciò che dovresti sapere.


11
Bene, poiché big-O dovrebbe specificare i limiti, non fa alcuna differenza se è più vicino a O (1) o meno. Anche O (n / 10 ^ 100) è ancora O (n). Ottengo il tuo punto di vista sull'efficienza riducendo quindi il rapporto, ma ciò pone comunque l'algoritmo su O (n).
paxdiablo,

4
L'analisi delle mappe hash è di solito sul caso medio, che è O (1) (con collusioni) Nel caso peggiore, puoi avere O (n), ma di solito non è così. per quanto riguarda la differenza - O (1) significa che ottieni lo stesso tempo di accesso indipendentemente dalla quantità di articoli sul grafico, e di solito è così (fintanto che c'è una buona proporzione tra la dimensione della tabella e 'n ')
Liran Orevi,

4
Vale anche la pena notare che è ancora esattamente O (1), anche se la scansione del bucket richiede un po 'di tempo perché ci sono già alcuni elementi al suo interno. Finché i bucket hanno una dimensione massima fissa, questo è solo un fattore costante irrilevante per la classificazione O (). Ma ovviamente ci possono essere ancora più elementi con chiavi "simili" aggiunte, in modo che questi secchi traboccino e non puoi più garantire una costante.
sth,

@sth Perché i secchi dovrebbero mai avere una dimensione massima fissa !?
Navin,

31

Ricorda che o (1) non significa che ogni ricerca esamina solo un singolo articolo - significa che il numero medio di articoli controllati rimane costante rispetto al numero di articoli nel contenitore. Quindi, se sono necessari in media 4 confronti per trovare un articolo in un contenitore con 100 articoli, dovrebbero essere necessari in media 4 confronti per trovare un articolo in un contenitore con 10000 articoli e per qualsiasi altro numero di articoli (c'è sempre un un po 'di varianza, specialmente attorno ai punti in cui la tabella hash si ripete, e quando c'è un numero molto piccolo di elementi).

Pertanto, le collisioni non impediscono al contenitore di eseguire o (1) operazioni, purché il numero medio di chiavi per bucket rimanga all'interno di un limite fisso.


16

So che questa è una vecchia domanda, ma in realtà c'è una nuova risposta ad essa.

Hai ragione sul fatto che una mappa hash non è davvero O(1), a rigor di termini, perché quando il numero di elementi diventa arbitrariamente grande, alla fine non sarai in grado di cercare in tempo costante (e la notazione O è definita in termini di numeri che possono diventare arbitrariamente grande).

Ma non ne consegue che la complessità in tempo reale sia - O(n)perché non esiste una regola che dice che i bucket devono essere implementati come un elenco lineare.

In effetti, Java 8 implementa i bucket TreeMapsquando superano una soglia, il che rende l'ora effettiva O(log n).


4

Se il numero di bucket (chiamalo b) viene mantenuto costante (il solito caso), la ricerca è in realtà O (n).
Man mano che n diventa grande, il numero di elementi in ciascun bucket è in media n / b. Se la risoluzione della collisione viene eseguita in uno dei modi usuali (ad esempio elenco collegato), la ricerca è O (n / b) = O (n).

La notazione O riguarda ciò che accade quando n diventa sempre più grande. Può essere fuorviante se applicato a determinati algoritmi e le tabelle hash sono un esempio significativo. Scegliamo il numero di bucket in base a quanti elementi ci aspettiamo di trattare. Quando n ha circa la stessa dimensione di b, la ricerca è approssimativamente a tempo costante, ma non possiamo chiamarlo O (1) perché O è definito in termini di limite come n → ∞.



2

Abbiamo stabilito che la descrizione standard delle ricerche nella tabella hash essendo O (1) si riferisce al tempo previsto nel caso medio, non alle rigide prestazioni nel caso peggiore. Per una tabella hash che risolve le collisioni con il concatenamento (come la hashmap di Java) questo è tecnicamente O (1 + α) con una buona funzione hash , dove α è il fattore di carico della tabella. Rimane costante fintanto che il numero di oggetti che stai memorizzando non è altro che un fattore costante maggiore della dimensione della tabella.

È stato anche spiegato che in senso stretto è possibile costruire input che richiedono ricerche O ( n ) per qualsiasi funzione hash deterministica. Ma è anche interessante considerare il tempo previsto nel caso peggiore , che è diverso dal tempo medio di ricerca. Usando il concatenamento è O (1 + la lunghezza della catena più lunga), ad esempio Θ (log n / log log n ) quando α = 1.

Se sei interessato a metodi teorici per ottenere ricerche nel caso peggiore attese nel tempo costante, puoi leggere l' hashing dinamico perfetto che risolve le collisioni in modo ricorsivo con un'altra tabella hash!


2

È O (1) solo se la tua funzione di hashing è molto buona. L'implementazione della tabella hash Java non protegge dalle funzioni hash non valide.

La necessità di espandere la tabella quando si aggiungono elementi o meno non è rilevante per la domanda perché si tratta del tempo di ricerca.


2

Gli elementi all'interno di HashMap sono memorizzati come una matrice di elenco collegato (nodo), ogni elenco collegato nella matrice rappresenta un bucket per un valore hash univoco di una o più chiavi.
Durante l'aggiunta di una voce in HashMap, l'hashcode della chiave viene utilizzato per determinare la posizione del bucket nell'array, qualcosa del tipo:

location = (arraylength - 1) & keyhashcode

Qui l'operatore & rappresenta bitwise AND.

Per esempio: 100 & "ABC".hashCode() = 64 (location of the bucket for the key "ABC")

Durante l'operazione get utilizza lo stesso modo per determinare la posizione del bucket per la chiave. Nel migliore dei casi, ogni chiave ha un hashcode univoco e si traduce in un bucket univoco per ogni chiave, in questo caso il metodo get impiega tempo solo per determinare la posizione del bucket e recuperare il valore che è costante O (1).

Nel peggiore dei casi, tutte le chiavi hanno lo stesso hashcode e sono memorizzate nello stesso bucket, ciò si traduce in un passaggio attraverso l'intero elenco che porta a O (n).

Nel caso di java 8, il bucket Elenco collegato viene sostituito con una TreeMap se la dimensione aumenta a più di 8, questo riduce l'efficienza di ricerca nel caso peggiore a O (log n).


1

Questo vale fondamentalmente per la maggior parte delle implementazioni della tabella hash nella maggior parte dei linguaggi di programmazione, poiché l'algoritmo stesso non cambia davvero.

Se nella tabella non sono presenti collisioni, è necessario eseguire una sola ricerca, pertanto il tempo di esecuzione è O (1). Se sono presenti collisioni, è necessario eseguire più di una ricerca, il che riduce le prestazioni verso O (n).


1
Ciò presuppone che il tempo di esecuzione sia limitato dal tempo di ricerca. In pratica troverai molte situazioni in cui la funzione hash fornisce il limite (String)
Stephan Eggermont,

1

Dipende dall'algoritmo scelto per evitare collisioni. Se l'implementazione utilizza un concatenamento separato, si verifica lo scenario peggiore in cui ogni elemento di dati viene sottoposto a hash sullo stesso valore (ad esempio, una scelta errata della funzione hash). In tal caso, la ricerca dei dati non è diversa da una ricerca lineare in un elenco collegato, ovvero O (n). Tuttavia, la probabilità che ciò accada è trascurabile e le ricerche migliori e i casi medi rimangono costanti, ovvero O (1).


1

A parte gli accademici, da un punto di vista pratico, HashMaps dovrebbe essere considerato avere un impatto sulle prestazioni insignificante (a meno che il profiler non ti dica diversamente).


4
Non in applicazioni pratiche. Non appena usi una stringa come chiave, noterai che non tutte le funzioni di hash sono ideali e alcune sono molto lente.
Stephan Eggermont,

1

Solo nel caso teorico, quando gli hashcode sono sempre diversi e il bucket per ogni codice hash è diverso, esiste O (1). Altrimenti, è di ordine costante, cioè all'incremento dell'hashmap, il suo ordine di ricerca rimane costante.


0

Naturalmente le prestazioni dell'hashmap dipenderanno dalla qualità della funzione hashCode () per l'oggetto dato. Tuttavia, se la funzione è implementata in modo tale che la possibilità di collisioni sia molto bassa, avrà una prestazione molto buona (ciò non è strettamente O (1) in tutti i casi possibili, ma nella maggior parte dei casi).

Ad esempio l'implementazione predefinita in Oracle JRE è l'uso di un numero casuale (che viene archiviato nell'istanza dell'oggetto in modo che non cambi - ma disabilita anche il blocco parziale, ma questa è un'altra discussione) quindi la possibilità di collisioni è molto basso.


"è nella maggior parte dei casi". Più specificamente, il tempo totale tenderà verso K volte N (dove K è costante) mentre N tende verso l'infinito.
ChrisW,

7
Questo è sbagliato. L'indice nella tabella hash verrà determinato tramite il hashCode % tableSizeche significa che possono esserci sicuramente delle collisioni. Non stai sfruttando appieno i 32 bit. È un po 'il punto delle tabelle hash ... riduci un grande spazio di indicizzazione in uno piccolo.
FogleBird,

1
"sei sicuro che non ci saranno collisioni" No, non lo sei perché la dimensione della mappa è inferiore alla dimensione dell'hash: ad esempio se la dimensione della mappa è due, allora è garantita una collisione (non importa che hash) se / quando provo a inserire tre elementi.
ChrisW,

Ma come si converte da una chiave all'indirizzo di memoria in O (1)? Intendo come x = array ["chiave"]. La chiave non è l'indirizzo di memoria, quindi dovrebbe comunque essere una ricerca O (n).
paxdiablo,

1
"Credo che se non si implementa hashCode, utilizzerà l'indirizzo di memoria dell'oggetto". Potrebbe usarlo, ma l'hashCode predefinito per Oracle Java standard è in realtà un numero casuale a 25 bit memorizzato nell'intestazione dell'oggetto, quindi 64/32 bit non ha alcuna conseguenza.
Boann,
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.