Domanda

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) anziché <=>.

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

È stato utile?

Soluzione

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 modesto di elementi subisca almeno una collisione. La notazione O grande ci consente di fare qualcosa di più avvincente. Osservalo 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 x k = (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à

Altri suggerimenti

Sembra che mescoli il comportamento nel 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 per metà, ha una performance di recupero di O (1) con un fattore molto piccolo (2, in effetti) nel caso previsto, entro un margine di varianza molto stretto. / p>

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.

Ricorda che o (1) non significa che ogni ricerca esamina solo un singolo articolo - significa che il numero medio di articoli controllati rimane costante w.r.t. il 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).

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

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

Hai ragione sul fatto che una mappa di hash non è realmente 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 grandi).

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 come TreeMaps quando superano una soglia, il che rende l'ora effettiva O(log n).

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 delle collisioni viene eseguita in uno dei modi usuali (ad esempio l'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, allora la ricerca è approssimativamente a tempo costante, ma non possiamo chiamarlo O (1) perché O è definito in termini di limite come n & # 8594; & # 8734;.

O(1+n/k) dove k è il numero di bucket.

Se l'implementazione imposta k = n/alpha, è O(1+alpha) = O(1) poiché alpha è una costante.

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 + & # 945;) con una buona funzione hash , dove & # 945; è 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 & # 920; (log n / log log n ) quando & # 945;. = 1

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

È 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 poiché si tratta del tempo di ricerca.

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, ad esempio:

location = (arraylength - 1) & keyhashcode

Qui l'amplificatore &; rappresenta l'operatore AND bit a bit.

Ad 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 archiviate nello stesso bucket, ciò si traduce in un attraversamento dell'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).

Questo vale sostanzialmente 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).

Dipende dall'algoritmo scelto per evitare le 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).

Accademici a parte, dal punto di vista pratico, HashMaps dovrebbe essere considerato avere un impatto sulle prestazioni insignificante (a meno che il tuo profiler non ti dica diversamente)

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

Ovviamente 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 ogni possibile ma è in maggior parte dei casi .

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

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top