Le tabelle hash possono davvero essere O (1)?


114

Sembra essere risaputo che le tabelle hash possono raggiungere O (1), ma per me non ha mai avuto senso. Qualcuno può spiegarlo per favore? Ecco due situazioni che mi vengono in mente:

R. Il valore è un int più piccolo della dimensione della tabella hash. Pertanto, il valore è il proprio hash, quindi non esiste una tabella hash. Ma se ci fosse, sarebbe O (1) e sarebbe ancora inefficiente.

B. Devi calcolare un hash del valore. In questa situazione, l'ordine è O (n) per la dimensione dei dati cercati. La ricerca potrebbe essere O (1) dopo aver eseguito O (n), ma ai miei occhi viene comunque visualizzato O (n).

E a meno che tu non abbia un hashish perfetto o una tabella hash grande, probabilmente ci sono diversi elementi per bucket. Quindi, ad un certo punto si trasforma comunque in una piccola ricerca lineare.

Penso che le tabelle hash siano fantastiche, ma non ottengo la designazione O (1) a meno che non sia solo teorica.

L' articolo di Wikipedia per le tabelle hash fa costantemente riferimento a un tempo di ricerca costante e ignora totalmente il costo della funzione hash. È davvero una misura giusta?


Modifica: per riassumere ciò che ho imparato:

  • È tecnicamente vero perché la funzione hash non è richiesta per utilizzare tutte le informazioni nella chiave e quindi potrebbe essere un tempo costante, e perché una tabella abbastanza grande può ridurre le collisioni a un tempo quasi costante.

  • È vero in pratica perché nel tempo funziona solo finché la funzione hash e la dimensione della tabella vengono scelte per ridurre al minimo le collisioni, anche se questo spesso significa non utilizzare una funzione hash a tempo costante.


31
È ammortizzato O (1), non O (1).
kennytm

Ricorda che O () è il limite per un gran numero di operazioni. In media non si avranno molte collisioni - non è necessario che una singola operazione non abbia collisioni.
Martin Beckett

A seconda dell'implementazione della stringa, le stringhe potrebbero portare con sé il loro valore hash, quindi questo sarebbe costante. Il punto è che è irrilevante per la complessità della ricerca hash.
Rich Remer

@kennytm Certo, la ricerca dopo aver eseguito l'hashing dell'input viene ammortizzata O (1). Ma il costo del calcolo dell'hash è davvero trascurabile? Supponiamo di eseguire l'hashing di una stringa: un array di caratteri. Per generare l'hash, ogni carattere viene iterato, quindi l'hashing di una stringa è O (N) dove N è la lunghezza della stringa. Ecco come è documentato per C # ed è così che il hashCode()metodo Java viene implementato per un file String. grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/…
spaaarky21

1
@ spaaarky21 La N in O (N) di cui parli è la lunghezza della stringa, che è diversa dalla n dimensione della tabella hash. La risposta di Mark Byer ha già affrontato questo problema.
kennytm

Risposte:


65

Hai due variabili qui, m e n, dove m è la lunghezza dell'input en è il numero di elementi nell'hash.

La dichiarazione sulle prestazioni della ricerca O (1) fa almeno due presupposti:

  • I tuoi oggetti possono essere l'uguaglianza rispetto al tempo O (1).
  • Ci saranno poche collisioni di hash.

Se i tuoi oggetti sono di dimensioni variabili e un controllo di uguaglianza richiede di guardare tutti i bit, le prestazioni diventeranno O (m). La funzione hash tuttavia non deve essere O (m) - può essere O (1). A differenza di un hash crittografico, una funzione hash da utilizzare in un dizionario non deve guardare ogni bit nell'input per calcolare l'hash. Le implementazioni sono libere di esaminare solo un numero fisso di bit.

Per un numero sufficiente di elementi il ​​numero di elementi diventerà maggiore del numero di possibili hash e quindi si verificheranno collisioni che causano un aumento delle prestazioni al di sopra di O (1), ad esempio O (n) per un semplice attraversamento di elenchi collegati (o O (n * m) se entrambe le ipotesi sono false).

In pratica, sebbene l'affermazione O (1) sia tecnicamente falsa, è approssimativamente vera per molte situazioni del mondo reale, e in particolare quelle situazioni in cui valgono le ipotesi di cui sopra.


4
Oltre a quanto sopra, se stai usando oggetti immutabili come chiavi es. Java Strings, dopo aver calcolato l'hash una volta, puoi ricordarlo e non doverlo calcolare di nuovo. D'altra parte, di solito non puoi fare affidamento sull'hash per dire se due chiavi sono uguali una volta trovato il bucket giusto, quindi per le stringhe devi fare un attraversamento O (m) per scoprire se sono uguali.
JeremyP

1
@ JeremyP: buon punto sul confronto di uguaglianza O (m). Mi mancava questo - post aggiornato. Grazie!
Mark Byers

2
L' O(1)affermazione è vera se stai eseguendo l'hashing into qualcos'altro che si adatta a una parola macchina. Questo è ciò che la maggior parte della teoria sull'hashing assume.
Thomas Ahle

Mi piace quella spiegazione del tuo Mark, l'ho citata nel mio articolo sulle tabelle hash su meshfields.de/hash-tables
Steve K

3
In "m è la lunghezza dell'input" - l' input è eccessivamente vago - potrebbe significare che tutte le chiavi e i valori sono stati inseriti, ma diventa chiaro in seguito (almeno per coloro che già comprendono l'argomento) che intendi la chiave . Sto solo suggerendo di usare "chiave" nella risposta per chiarezza. BTW - esempio concreto - Le std::hashchiavi testuali di Visual C ++ combinano 10 caratteri equidistanti lungo il testo nel valore hash, quindi è O (1) indipendentemente dalla lunghezza del testo (ma notevolmente più incline alle collisioni di GCC!). Separatamente, le rivendicazioni di O (1) hanno un'altra ipotesi (normalmente correttamente) che m sia molto inferiore a n .
Tony Delroy

22

Devi calcolare l'hash, quindi l'ordine è O (n) per la dimensione dei dati da cercare. La ricerca potrebbe essere O (1) dopo aver eseguito O (n), ma ai miei occhi viene comunque visualizzato O (n).

Che cosa? L'hash di un singolo elemento richiede tempo costante. Perché dovrebbe essere qualcos'altro? Se stai inserendo nelementi, allora sì, devi calcolare gli nhash e questo richiede tempo lineare ... per cercare un elemento, calcoli un singolo hash di ciò che stai cercando, quindi trovi il bucket appropriato con quello . Non si ricalcolano gli hash di tutto ciò che è già nella tabella hash.

E a meno che tu non abbia un hash perfetto o una tabella hash grande, probabilmente ci sono diversi elementi per bucket, quindi ad un certo punto si trasforma comunque in una piccola ricerca lineare.

Non necessariamente. I bucket non devono essere necessariamente elenchi o array, possono essere qualsiasi tipo di contenitore, ad esempio un BST bilanciato. Ciò significa O(log n)caso peggiore. Ma questo è il motivo per cui è importante scegliere una buona funzione di hashing per evitare di mettere troppi elementi in un bucket. Come ha sottolineato KennyTM, in media avrai ancora O(1)tempo, anche se occasionalmente dovrai scavare in un secchio.

Il compromesso delle tabelle hash è ovviamente la complessità dello spazio. Stai scambiando spazio per tempo, il che sembra essere il solito caso nella scienza informatica.


Hai menzionato l'uso delle stringhe come chiavi in ​​uno dei tuoi altri commenti. Sei preoccupato per il tempo necessario per calcolare l'hash di una stringa, perché è composto da diversi caratteri? Come qualcun altro ha sottolineato di nuovo, non è necessariamente necessario esaminare tutti i caratteri per calcolare l'hash, anche se potrebbe produrre un hash migliore se lo facessi. In tal caso, se mnella tua chiave sono presenti in media caratteri e li hai usati tutti per calcolare il tuo hash, suppongo che tu abbia ragione, quella ricerca richiederebbe O(m). In m >> ntal caso potresti avere un problema. Probabilmente in questo caso staresti meglio con una BST. Oppure scegli una funzione di hashing più economica.


le tabelle hash non utilizzano BST. I BST non richiedono valori hash. Mappe e set possono essere implementati come BST.
Nick Dandoulakis

3
@ Nick: Eh? No ... i BST non richiedono valori hash ... questo è il punto. Supponiamo che a questo punto abbiamo già una collisione (stesso hash ... o almeno lo stesso bucket), quindi dobbiamo guardare qualcos'altro per trovare l'elemento giusto, cioè il valore effettivo.
mpen

oh, capisco il tuo punto. Ma non sono sicuro che mischiare BST e hash valga la pena. Perché non usare solo i BST?
Nick Dandoulakis

2
Sto solo dicendo che si potrebbe sbarazzarsi di quella O(n)per le collisioni. Se siete aspettate un sacco di collisioni, allora hai ragione, probabilmente meglio andare con un BST in primo luogo.
mpen

1
@ spaaarky21 Esatto, ma Nin questo caso è la lunghezza della stringa. Abbiamo solo bisogno di hash di una stringa per determinare in quale "bucket" deve entrare - non cresce con la lunghezza della hashmap.
mpen

5

L'hash è di dimensione fissa: cercare il bucket di hash appropriato è un'operazione a costo fisso. Ciò significa che è O (1).

Il calcolo dell'hash non deve essere un'operazione particolarmente costosa: non stiamo parlando di funzioni hash crittografiche qui. Ma questo è a proposito. Il calcolo della funzione hash in sé non dipende dal numero n di elementi; mentre potrebbe dipendere dalla dimensione dei dati in un elemento, questo non è ciò a cui n si riferisce. Quindi il calcolo dell'hash non dipende da n ed è anche O (1).


3
cercare l'hash bucket è O (1). Ma individuare la chiave giusta è una procedura O (n), dove n dipende dal numero di collisioni hash.
Nick Dandoulakis

1
Quindi di 3 passaggi, calcola l'hash, trova il secchio, cerca nel secchio, il passaggio intermedio è costante? La ricerca nel secchio è solitamente costante. Il calcolo dell'hash di solito è di diversi ordini di grandezza più economico rispetto ad altri mezzi per trovare il secchio. Ma questo significa davvero un tempo costante? In una ricerca ingenua di sottostringa, diresti O (n * m) per le due lunghezze, quindi perché la lunghezza della chiave viene ignorata qui?
ritirata entro

trovare una chiave di lunghezza fissa è solo O (n) solo se la sua lista è supportata, una tabella hash con supporto ad albero bilanciato sarà O (log (n))
jk.

@Jk Per buone funzioni hash, il caso peggiore è sempre logn, vedi la mia risposta su stackoverflow.com/questions/4553624/hashmap-get-put-complexity/…
Thomas Ahle

Nel peggiore dei casi la complessità sarà o (n) in caso di collisione
Saurabh Chandra Patel

3

L'hashing è O (1) solo se nella tabella è presente solo un numero costante di chiavi e vengono fatte alcune altre ipotesi. Ma in questi casi ha un vantaggio.

Se la tua chiave ha una rappresentazione a n bit, la tua funzione hash può utilizzare 1, 2, ... n di questi bit. Pensando a una funzione hash che utilizza 1 bit. La valutazione è sicuramente O (1). Ma stai solo partizionando lo spazio della chiave in 2. Quindi stai mappando fino a 2 ^ (n-1) chiavi nello stesso contenitore. utilizzando la ricerca BST, sono necessari fino a n-1 passaggi per individuare una chiave particolare se quasi piena.

Puoi estenderlo per vedere che se la tua funzione hash usa K bit, la dimensione del tuo bin è 2 ^ (nk).

quindi funzione hash K-bit ==> non più di 2 ^ K bin effettivi ==> fino a 2 ^ (nK) chiavi n-bit per bin ==> (nK) passaggi (BST) per risolvere le collisioni. In realtà la maggior parte delle funzioni hash sono molto meno "efficaci" e necessitano / utilizzano più di K bit per produrre 2 ^ k bin. Quindi anche questo è ottimistico.

Puoi visualizzarlo in questo modo: avrai bisogno di ~ n passaggi per poter distinguere in modo univoco una coppia di chiavi di n bit nel caso peggiore. Non c'è davvero alcun modo per aggirare questo limite della teoria dell'informazione, tabella hash o meno.

Tuttavia, questo NON è come / quando usi la tabella hash!

L'analisi della complessità presuppone che per chiavi a n bit, si potrebbero avere chiavi O (2 ^ n) nella tabella (ad es. 1/4 di tutte le chiavi possibili). Ma la maggior parte delle volte, se non sempre, utilizziamo una tabella hash, nella tabella abbiamo solo un numero costante di chiavi a n bit. Se vuoi solo un numero costante di chiavi nella tabella, diciamo C è il tuo numero massimo, allora potresti formare una tabella hash di bin O (C), che garantisca la collisione costante prevista (con una buona funzione hash); e una funzione hash che utilizza ~ logC degli n bit nella chiave. Quindi ogni query è O (logC) = O (1). Questo è il modo in cui le persone affermano che "l'accesso alla tabella hash è O (1)" /

Ci sono un paio di problemi qui: in primo luogo, dire che non hai bisogno di tutti i bit potrebbe essere solo un trucco per la fatturazione. Innanzitutto non puoi davvero passare il valore della chiave alla funzione hash, perché ciò sposterebbe n bit nella memoria che è O (n). Quindi devi fare ad esempio un passaggio di riferimento. Ma è ancora necessario archiviarlo già da qualche parte che era un'operazione O (n); semplicemente non lo fatturate all'hashing; il tuo compito di calcolo generale non può evitarlo. Secondo, si esegue l'hashing, si trova il cestino e si trovano più di 1 chiavi; il tuo costo dipende dal metodo di risoluzione: se esegui il confronto (BST o List), avrai un'operazione O (n) (la chiave di richiamo è n bit); se fai il secondo hash, beh, hai lo stesso problema se il secondo hash ha una collisione.

Considera l'alternativa, ad esempio BST, in questo caso. ci sono chiavi C, quindi un BST bilanciato sarà O (logC) in profondità, quindi una ricerca richiede passaggi O (logC). Tuttavia il confronto in questo caso sarebbe un'operazione O (n) ... quindi sembra che l'hashing sia una scelta migliore in questo caso.


1

TL; DR: le tabelle hash garantiscono il tempo O(1)previsto nel caso peggiore se scegli la tua funzione hash in modo uniforme e casuale da una famiglia universale di funzioni hash. Il caso peggiore previsto non è lo stesso del caso medio.

Disclaimer: non provo formalmente che le tabelle hash lo siano O(1), per questo dai un'occhiata a questo video di coursera [ 1 ]. Inoltre non discuto gli aspetti ammortizzati delle tabelle hash. Ciò è ortogonale alla discussione su hashing e collisioni.

Vedo una quantità sorprendentemente grande di confusione su questo argomento in altre risposte e commenti, e cercherò di correggerne alcuni in questa lunga risposta.

Ragionando sul caso peggiore

Esistono diversi tipi di analisi del caso peggiore. L'analisi che la maggior parte delle risposte ha fatto finora non è il caso peggiore, ma piuttosto il caso medio [ 2 ]. L' analisi media dei casi tende ad essere più pratica. Forse il tuo algoritmo ha un input nel caso peggiore, ma in realtà funziona bene per tutti gli altri input possibili. Il risultato finale è che il tuo runtime dipende dal set di dati su cui stai eseguendo.

Considera il seguente pseudocodice del getmetodo di una tabella hash. Qui presumo che gestiamo la collisione concatenando, quindi ogni voce della tabella è un elenco collegato di (key,value)coppie. Assumiamo anche che il numero di bucket msia fisso ma è O(n), dove nè il numero di elementi nell'input.

function get(a: Table with m buckets, k: Key being looked up)
  bucket <- compute hash(k) modulo m
  for each (key,value) in a[bucket]
    return value if k == key
  return not_found

Come altre risposte hanno sottolineato, questo avviene nella media O(1)e nel peggiore dei casi O(n). Possiamo fare un piccolo schizzo di una dimostrazione per sfida qui. La sfida è la seguente:

(1) Dai l'algoritmo della tua tabella hash a un avversario.

(2) L'avversario può studiarlo e prepararsi per tutto il tempo che vuole.

(3) Infine l'avversario ti dà un input di dimensione nda inserire nella tua tabella.

La domanda è: quanto è veloce la tua tabella hash sull'input dell'avversario?

Dal passaggio (1) l'avversario conosce la tua funzione hash; durante la fase (2) l'avversario può creare un elenco di nelementi con lo stesso hash modulo m, ad esempio calcolando in modo casuale l'hash di un gruppo di elementi; e poi in (3) possono darti quella lista. Ma ecco, dal momento che tutti gli nelementi vengono inseriti nello stesso bucket, il tuo algoritmo impiegherà O(n)tempo per attraversare l'elenco collegato in quel bucket. Non importa quante volte ripetiamo la sfida, l'avversario vince sempre, e questo è quanto sia cattivo il tuo algoritmo, nel peggiore dei casi O(n).

Come mai l'hashing è O (1)?

Ciò che ci ha sconcertati nella sfida precedente è stato il fatto che l'avversario conosceva molto bene la nostra funzione hash e poteva utilizzare quella conoscenza per creare il peggior input possibile. E se invece di usare sempre una funzione hash fissa, avessimo effettivamente un set di funzioni hash H, che l'algoritmo può scegliere casualmente in fase di esecuzione? Nel caso foste curiosi, Hsi chiama famiglia universale di funzioni hash [ 3 ]. Va bene, proviamo ad aggiungere un po 'di casualità a questo.

Per prima cosa supponiamo che la nostra tabella hash includa anche un seme re rsia assegnata a un numero casuale al momento della costruzione. Lo assegniamo una volta e poi viene corretto per quell'istanza di tabella hash. Ora rivisitiamo il nostro pseudocodice.

function get(a: Table with m buckets and seed r, k: Key being looked up)
  rHash <- H[r]
  bucket <- compute rHash(k) modulo m
  for each (key,value) in a[bucket]
    return value if k == key
  return not_found

Se proviamo ancora una volta la sfida: dal punto (1) l'avversario può conoscere tutte le funzioni hash in cui ci troviamo H, ma ora dipende dalla funzione hash specifica che usiamo r. Il valore di rè privato per la nostra struttura, l'avversario non può ispezionarlo in fase di esecuzione, né prevederlo in anticipo, quindi non può inventare un elenco che è sempre negativo per noi. Supponiamo che nello stadio (2) l'avversario sceglie una funzione hashin Ha caso, poi artigianato un elenco di ncollisioni sotto hash modulo m, e invia che per la fase (3), attraversando le dita che in fase di esecuzione H[r]saranno gli stessi hashhanno scelto.

Questa è una scommessa seria per l'avversario, l'elenco che ha creato si scontra hash, ma sarà solo un input casuale sotto qualsiasi altra funzione hash in H. Se vince questa scommessa, il nostro tempo di esecuzione sarà il peggiore O(n)come prima, ma se perde, beh, ci viene solo dato un input casuale che richiede il O(1)tempo medio . E infatti la maggior parte delle volte l'avversario perderà, vince solo una volta ogni |H|sfida, e possiamo fare di |H|essere molto grandi.

Confronta questo risultato con l'algoritmo precedente in cui l'avversario ha sempre vinto la sfida. Mano che ondeggia un po 'qui, ma poiché la maggior parte delle volte l'avversario fallirà, e questo è vero per tutte le possibili strategie che l'avversario può provare, ne consegue che sebbene il caso peggiore sia O(n), il caso peggiore previsto lo è in realtà O(1).


Ancora una volta, questa non è una prova formale. La garanzia che otteniamo da questa prevista analisi del caso peggiore è che il nostro tempo di esecuzione è ora indipendente da qualsiasi input specifico . Questa è una garanzia veramente casuale, al contrario dell'analisi del caso medio in cui abbiamo mostrato che un avversario motivato potrebbe facilmente creare input errati.


0

Ci sono due impostazioni in cui è possibile ottenere O (1) tempi peggiori.

  1. Se la tua configurazione è statica, l'hashing FKS ti darà garanzie O (1) nel caso peggiore . Ma come hai indicato, la tua impostazione non è statica.
  2. Se utilizzi l'hashing cuculo, le query e le eliminazioni sono O (1) nel caso peggiore, ma l'inserimento è previsto solo O (1) . L'hashing del cuculo funziona abbastanza bene se hai un limite superiore sul numero totale di inserti e imposta la dimensione della tabella in modo che sia circa il 25% più grande.

Copiato da qui


0

Sembra basato sulla discussione qui, che se X è il massimale di (# di elementi nella tabella / # di bin), allora una risposta migliore è O (log (X)) assumendo un'implementazione efficiente della ricerca bin.


0

R. Il valore è un int più piccolo della dimensione della tabella hash. Pertanto, il valore è il proprio hash, quindi non esiste una tabella hash. Ma se ci fosse, sarebbe O (1) e sarebbe ancora inefficiente.

Questo è un caso in cui è possibile mappare banalmente le chiavi a bucket distinti, quindi un array sembra una scelta migliore di struttura dati rispetto a una tabella hash. Tuttavia, le inefficienze non crescono con le dimensioni della tabella.

(Potresti comunque usare una tabella hash perché non ti fidi che gli int rimangano più piccoli della dimensione della tabella mentre il programma si evolve, vuoi rendere il codice potenzialmente riutilizzabile quando quella relazione non regge, o semplicemente non lo fai voglio che le persone che leggono / mantengono il codice debbano sprecare sforzi mentali per comprendere e mantenere la relazione).

B. Devi calcolare un hash del valore. In questa situazione, l'ordine è O (n) per la dimensione dei dati cercati. La ricerca potrebbe essere O (1) dopo aver eseguito O (n), ma ai miei occhi viene comunque visualizzato O (n).

Dobbiamo distinguere tra la dimensione della chiave (ad esempio in byte) e la dimensione del numero di chiavi memorizzate nella tabella hash. Affermare che le tabelle hash forniscono operazioni O (1) significano che le operazioni (inserisci / cancella / trova) non tendono a rallentare ulteriormente quando il numero di chiavi aumenta da centinaia a migliaia a milioni a miliardi (almeno non se tutti i dati è accessibile / aggiornato in una memoria altrettanto veloce, sia che si tratti di RAM o disco: gli effetti della cache possono entrare in gioco, ma anche il costo di un errore di cache nel caso peggiore tende ad essere un multiplo costante del successo nel migliore dei casi).

Considera un elenco telefonico: potresti avere nomi piuttosto lunghi, ma se il libro ha 100 nomi o 10 milioni, la lunghezza media del nome sarà abbastanza coerente e il caso peggiore nella storia ...

Il record mondiale di Guinness per il nome più lungo mai usato da chiunque è stato stabilito da Adolph Blaine Charles David Earl Frederick Gerald Hubert Irvin John Kenneth Lloyd Martin Nero Oliver Paul Quincy Randolph Sherman Thomas Uncas Victor William Xerxes Yancy Wolfeschlegelsteinhausenbergerdorff, Senior

... wcmi dice che è 215 caratteri - che non è un duro limite superiore alla lunghezza della chiave, ma non abbiamo bisogno di preoccuparsi di che vi sia in maniera massiccia di più.

Ciò vale per la maggior parte delle tabelle hash del mondo reale: la lunghezza media della chiave non tende a crescere con il numero di chiavi in ​​uso. Ci sono eccezioni, ad esempio una routine di creazione di chiavi potrebbe restituire stringhe che incorporano numeri interi incrementali, ma anche in questo caso ogni volta che aumenti il ​​numero di chiavi di un ordine di grandezza, aumenti solo la lunghezza della chiave di 1 carattere: non è significativo.

È anche possibile creare un hash da una quantità di dati chiave di dimensioni fisse. Ad esempio, Visual C ++ di Microsoft viene fornito con un'implementazione della libreria standard std::hash<std::string>che crea un hash che incorpora solo dieci byte equidistanti lungo la stringa, quindi se le stringhe variano solo in altri indici si ottengono collisioni (e quindi in pratica comportamenti non O (1) sul lato della ricerca post-collisione), ma il tempo per creare l'hash ha un limite superiore rigido.

E a meno che tu non abbia un hashish perfetto o una tabella hash grande, probabilmente ci sono diversi elementi per bucket. Quindi, ad un certo punto si trasforma comunque in una piccola ricerca lineare.

Generalmente vero, ma la cosa fantastica delle tabelle hash è che il numero di chiavi visitate durante quelle "piccole ricerche lineari" è - per l' approccio del concatenamento separato alle collisioni - una funzione del fattore di carico della tabella hash (rapporto tra chiavi e bucket).

Ad esempio, con un fattore di carico di 1,0 c'è una media di ~ 1,58 per la lunghezza di quelle ricerche lineari, indipendentemente dal numero di chiavi (vedi la mia risposta qui ). Per l' hashing chiuso è un po 'più complicato, ma non molto peggio quando il fattore di carico non è troppo alto.

È tecnicamente vero perché la funzione hash non è richiesta per utilizzare tutte le informazioni nella chiave e quindi potrebbe essere un tempo costante, e perché una tabella abbastanza grande può ridurre le collisioni a un tempo quasi costante.

Questo tipo di non coglie il punto. Qualsiasi tipo di struttura dati associativa alla fine deve eseguire operazioni su ogni parte della chiave a volte (la disuguaglianza a volte può essere determinata solo da una parte della chiave, ma l'uguaglianza generalmente richiede che ogni bit sia considerato). Come minimo, può eseguire l'hashing della chiave una volta e memorizzare il valore hash, e se utilizza una funzione hash abbastanza forte, ad esempio MD5 a 64 bit, potrebbe praticamente ignorare anche la possibilità che due chiavi abbiano lo stesso valore (un'azienda Ho lavorato per farlo esattamente per il database distribuito: il tempo di generazione dell'hash era ancora insignificante rispetto alle trasmissioni di rete WAN). Quindi, non ha molto senso essere ossessionati dal costo per elaborare la chiave: è inerente all'archiviazione delle chiavi indipendentemente dalla struttura dei dati e, come detto sopra, non lo fa '

Per quanto riguarda le tabelle hash abbastanza grandi che riducono le collisioni, anche questo manca il punto. Per il concatenamento separato, hai ancora una lunghezza media della catena di collisione costante con un dato fattore di carico: è solo più alta quando il fattore di carico è più alto e quella relazione non è lineare. L'utente di SO Hans commenta la mia risposta anche collegato sopra che:

la lunghezza media della benna condizionata a secchi non vuoti è una misura migliore dell'efficienza. È un / (1-e ^ {- a}) [dove a è il fattore di carico, e è 2,71828 ...]

Quindi, il fattore di carico da solo determina il numero medio di chiavi in ​​collisione che devi cercare durante le operazioni di inserimento / cancellazione / ricerca. Per il concatenamento separato, non si limita a essere costante quando il fattore di carico è basso, ma è sempre costante. Per l'indirizzamento aperto anche se la tua affermazione ha una certa validità: alcuni elementi in collisione vengono reindirizzati a bucket alternativi e possono quindi interferire con le operazioni su altre chiavi, quindi con fattori di carico più elevati (soprattutto> .8 o .9) la lunghezza della catena di collisione peggiora notevolmente.

È vero in pratica perché nel tempo funziona solo finché la funzione hash e la dimensione della tabella vengono scelte per ridurre al minimo le collisioni, anche se questo spesso significa non utilizzare una funzione hash a tempo costante.

Bene, la dimensione della tabella dovrebbe risultare in un fattore di carico sano data la scelta di hashing vicino o concatenamento separato, ma anche se la funzione hash è un po 'debole e le chiavi non sono molto casuali, avere un numero primo di bucket spesso aiuta a ridurre anche le collisioni ( hash-value % table-sizequindi si avvolge in modo tale che le modifiche solo a uno o due bit di ordine elevato nel valore hash si risolvano ancora in bucket distribuiti in modo pseudo-casuale tra diverse parti della tabella hash).

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.