In Java, ogni oggetto ha un metodo hashCode()
che restituisce un valore intero a 32 bit, che è sempre lo stesso per lo stesso oggetto. Nella versione più semplice, una tabella hash contiene semplicemente una matrice di dimensioni 2 ^ 32 e una coppia chiave-valore è memorizzata nell'indice corrispondente al codice hash della chiave. Poiché l'accesso dell'array per indice è O (1), l'accesso per chiave hashtable (per la memorizzazione o il recupero) può anche essere O (1).
Ovviamente è un po 'più complesso nella realtà. Primo, puoi sempre avere collisioni, cioè due oggetti diversi che danno lo stesso codice hash. Quindi gli elementi non sono memorizzati direttamente nell'array, piuttosto ogni indice dell'array contiene un "bucket", che è una lista ordinaria di coppie chiave-valore. (Nell'hashtable di Java i bucket sono implementati come elenchi collegati.) Devi cercare nel bucket per trovare l'elemento e questa ricerca sarà O (n), ma a meno che il tuo hashtable contenga un numero estremo di elementi (o il tuo algoritmo hash è sbagliato), gli elementi verranno distribuiti in modo uniforme sull'array e ogni bucket conterrà solo alcuni elementi. (Solo uno nel migliore dei casi.)
In secondo luogo, inizialmente non creerai una matrice di dimensioni 2 ^ 32, poiché sarebbe uno spreco di spazio. Invece, inizialmente si crea un array più piccolo, in cui ogni voce viene mappata su più hashcode. Ciò comporterà ovviamente un rischio maggiore di collisione. Tieni traccia del numero di voci e quando raggiungono una determinata soglia raddoppia la dimensione della matrice e quindi ridistribuisci gli elementi. Ovviamente questo avrà anche un costo in termini di prestazioni. C'è qualche compromesso nella progettazione nel decidere quando ridimensionare l'array. Più grande è l'array rispetto al numero di articoli, meno collisioni e quindi migliori prestazioni, ma anche più spreco di spazio.
Quindi trovare un oggetto è O (n) nel peggiore dei casi in cui tutti gli elementi si trovano nello stesso bucket, ma O (1) nel caso comune (data una funzione hash ben funzionante. hashCode()
ovviamente non è garantito.Se scrivi int hashCode(){return 17;}
ottieni sempre le peggiori prestazioni del caso). E se il numero di elementi aumenta più della dimensione hash, i bucket iniziano a crescere e di nuovo si ottiene la ricerca O (n). Sui sistemi a 32 bit si esaurirebbe la memoria prima che ciò accadesse, ma con la memoria a 64 bit potrebbe essere teoricamente un problema.
L'aggiunta di un elemento è anche O (1) nel caso comune, ma O (n) se l'aggiunta attiva un ridimensionamento dell'array. Tuttavia, il costo aggregato delle operazioni di ridimensionamento è prevedibile e proporzionale al numero di elementi, quindi il costo ammortizzato per gli add è ancora pari a O (1). Questo non è il caso peggiore con le ricerche, poiché se siamo sfortunati e tutti gli articoli finiscono nello stesso bucket ogni ricerca avrà le prestazioni peggiori e non c'è modo di ammortizzare questo costi.
Naturalmente sia il caso peggiore che il caso comune o medio possono essere rilevanti. In un sistema in tempo reale, è piuttosto importante conoscere le peggiori prestazioni di un'operazione. Per la maggior parte delle applicazioni aziendali, il caso medio è la metrica più importante.