Perché le funzioni hash dovrebbero usare un modulo con numeri primi?


336

Molto tempo fa, ho acquistato un libro sulle strutture dati dal tavolo degli affari per $ 1,25. In esso, la spiegazione di una funzione di hashing ha affermato che alla fine dovrebbe essere mod di un numero primo a causa della "natura della matematica".

Cosa ti aspetti da un libro da $ 1,25?

Ad ogni modo, ho avuto anni per pensare alla natura della matematica, e ancora non riesco a capirlo.

La distribuzione dei numeri è davvero maggiore anche in presenza di un numero primo di bucket? Oppure è una vecchia storia da programmatore che tutti accettano perché tutti lo accettano?


1
Domanda perfettamente ragionevole: perché dovrebbe esserci un numero primo di secchi?
Draemon,

1
Questa domanda sembra fuori tema perché più che probabilmente appartiene a Informatica .
Corse di leggerezza in orbita,

2
cs.stackexchange.com/a/64191/64222 un'altra spiegazione ben argomentata.
Green Tree


Ecco un'altra grande spiegazione a una domanda in qualche modo correlata con alcuni numeri probatori sorprendenti - quora.com/…
AnBisw

Risposte:


242

Di solito una semplice funzione hash funziona prendendo le "parti componenti" dell'input (caratteri nel caso di una stringa) e moltiplicandole per i poteri di una costante e sommandole in un tipo intero. Quindi ad esempio un tipico hash (anche se non particolarmente buono) di una stringa potrebbe essere:

(first char) + k * (second char) + k^2 * (third char) + ...

Quindi, se viene inserito un gruppo di stringhe che hanno tutte lo stesso primo carattere, i risultati saranno tutti lo stesso modulo k, almeno fino a quando il tipo intero non trabocca.

[Ad esempio, la stringa hashCode di Java è stranamente simile a questa - fa in modo che i caratteri si invertano, con k = 31. In questo modo si ottengono relazioni sorprendenti modulo 31 tra stringhe che terminano allo stesso modo e relazioni sorprendenti modulo 2 ^ 32 tra stringhe uguali tranne vicino alla fine. Questo non confonde seriamente il comportamento hashtable.]

Una tabella hash funziona prendendo il modulo dell'hash sul numero di bucket.

In una tabella hash è importante non produrre collisioni per casi probabili, poiché le collisioni riducono l'efficienza della tabella.

Supponiamo ora che qualcuno inserisca un intero gruppo di valori in una tabella hash che abbia qualche relazione tra gli oggetti, come se tutti avessero lo stesso primo carattere. Questo è un modello di utilizzo abbastanza prevedibile, direi, quindi non vogliamo che produca troppe collisioni.

Si scopre che "a causa della natura della matematica", se la costante utilizzata nell'hash e il numero di bucket sono coprimi , le collisioni sono minimizzate in alcuni casi comuni. Se non sono coprimi, quindi ci sono alcune relazioni abbastanza semplici tra input per i quali le collisioni non sono minimizzate. Tutti gli hash escono allo stesso modo del fattore comune, il che significa che cadranno tutti nell'1 / n dei secchi che hanno quel valore modulo il fattore comune. Ottieni n volte più collisioni, dove n è il fattore comune. Poiché n è almeno 2, direi che è inaccettabile che un caso d'uso abbastanza semplice generi almeno il doppio delle collisioni rispetto al normale. Se un utente sta per dividere la nostra distribuzione in secchi, vogliamo che sia un incidente strano, non un semplice utilizzo prevedibile.

Ora, le implementazioni di hashtable ovviamente non hanno alcun controllo sugli oggetti messi in esse. Non possono impedire che siano collegati. Quindi la cosa da fare è assicurarsi che i conteggi della costante e del bucket siano coprimi. In questo modo non si fa affidamento sul solo "ultimo" componente per determinare il modulo del bucket rispetto ad alcuni piccoli fattori comuni. Per quanto ne so non devono essere primi per raggiungere questo obiettivo, solo coprimi.

Ma se la funzione hash e la tabella hash sono scritte in modo indipendente, allora la tabella hash non sa come funziona la funzione hash. Potrebbe usare una costante con piccoli fattori. Se sei fortunato, potrebbe funzionare in modo completamente diverso ed essere non lineare. Se l'hash è abbastanza buono, qualsiasi conteggio dei bucket va bene. Ma un hashtable paranoico non può assumere una buona funzione hash, quindi dovrebbe usare un numero primo di bucket. Allo stesso modo una funzione hash paranoica dovrebbe usare una costante primaria di grandi dimensioni, per ridurre la possibilità che qualcuno usi un numero di bucket che hanno un fattore comune con la costante.

In pratica, penso che sia abbastanza normale usare una potenza di 2 come numero di secchi. Questo è conveniente e consente di evitare di cercare o preselezionare un numero primo della giusta grandezza. Quindi fai affidamento sulla funzione hash per non usare nemmeno i moltiplicatori, che è generalmente un presupposto sicuro. Ma puoi ancora ottenere comportamenti di hashing occasionali basati su funzioni di hash come quella sopra e il conteggio dei bucket principali potrebbe aiutare ulteriormente.

Mettere sul principio che "tutto deve essere primo" è per quanto ne so una condizione sufficiente ma non necessaria per una buona distribuzione su hashtable. Permette a tutti di interagire senza la necessità di presumere che gli altri abbiano seguito la stessa regola.

[Modifica: c'è un altro motivo più specializzato per utilizzare un numero primo di bucket, ovvero se si gestiscono le collisioni con sondaggi lineari. Quindi calcoli una falcata dall'hashcode e se quella falcata risulta essere un fattore del conteggio dei bucket, puoi fare solo (bucket_count / stride) sonde prima di tornare da dove hai iniziato. Il caso che vuoi evitare di più è stride = 0, ovviamente, che deve essere con maiuscole / minuscole, ma per evitare anche maiuscole / minuscole con maiuscole / minuscole pari a un numero intero piccolo, puoi semplicemente rendere bucket_count primo e non preoccuparti di cosa passo è fornito non è 0.]


Proprio come una nota a
margine

9
questa è una risposta fantastica puoi per favore spiegare questo ulteriore "Quindi ottieni relazioni sorprendenti modulo 31 tra stringhe che finiscono allo stesso modo, e relazioni sorprendenti modulo 2 ^ 32 tra stringhe che sono uguali tranne che vicino alla fine. Questo non compromette seriamente il comportamento hashtable. " Soprattutto non capisco la parte 2 ^ 32
ordinaria il

2
Nota aggiuntiva per rendere le cose più chiare su questo: "Tutti gli hash escono uguali modulo il fattore comune" -> Questo perché, se si considera la funzione hash di esempio hash = 1st char + 2nd char * k + ..., e prendi le stringhe con lo stesso primo carattere, l'hash% k sarà lo stesso per queste stringhe. Se M è la dimensione dell'hashtable e g è il gcd di M e k, allora (hash% k)% g è uguale a hash% g (poiché g divide k) e quindi l'hash% g sarà lo stesso per queste stringhe. Ora considera (hash% M)% g, questo è uguale all'hash% g (poiché g divide M). Quindi (hash% M)% g è uguale per tutte queste stringhe.
Quark,

1
@DanielMcLaury Joshua Bloch ha spiegato il perché di Java: è stato raccomandato in due libri popolari (K&R, il libro del Drago) e si è comportato bene con basse collisioni sul dizionario inglese. È veloce (usa il metodo di Horner ). Apparentemente anche K&R non ricorda da dove provenisse. Funzione simile è Rabin impronte digitali da algoritmo di Rabin-Karp (1981), ma K & R (1978) è precedente che.
Guadagna il

1
@SteveJessop, per favore, puoi spiegare "relazioni sorprendenti modulo 2 ^ 32 tra stringhe che sono uguali tranne che verso la fine". Grazie.
Khanna111,

29

La prima cosa da fare quando si inserisce / recupera dalla tabella hash è calcolare l'hashCode per la chiave data e quindi trovare il bucket corretto tagliando l'hashCode alla dimensione dell'hashTable eseguendo hashCode% table_length. Ecco 2 "dichiarazioni" che molto probabilmente hai letto da qualche parte

  1. Se usi una potenza di 2 per table_length, trovare (hashCode (chiave)% 2 ^ n) è semplice e veloce come (hashCode (chiave) & (2 ^ n -1)). Ma se la tua funzione di calcolare hashCode per una determinata chiave non è buona, soffrirai sicuramente il clustering di molte chiavi in ​​alcuni bucket hash.
  2. Ma se usi i numeri primi per table_length, i codici hash calcolati potrebbero essere mappati nei diversi bucket hash anche se hai una funzione hashCode leggermente stupida.

Ed ecco la prova.

Se supponi che la tua funzione hashCode comporti, tra gli altri, i seguenti hashCode {x, 2x, 3x, 4x, 5x, 6x ...}, allora tutti questi saranno raggruppati in solo m numero di bucket, dove m = table_length / GreatestCommonFactor (table_length, x). (È banale verificarlo / derivarlo). Ora puoi effettuare una delle seguenti operazioni per evitare il clustering

Assicurati di non generare troppi hashCode che sono multipli di un altro hashCode come in {x, 2x, 3x, 4x, 5x, 6x ...}. Ma questo potrebbe essere difficile se la tua hashTable dovrebbe avere milioni di voci. O semplicemente rendere m uguale a table_length rendendo GreatestCommonFactor (table_length, x) uguale a 1, ovvero facendo coprime table_length con x. E se x può essere praticamente qualsiasi numero, assicurati che table_length sia un numero primo.

Da - http://srinvis.blogspot.com/2006/07/hash-table-lengths-and-prime-numbers.html


11

http://computinglife.wordpress.com/2008/11/20/why-do-hash-functions-use-prime-numbers/

Spiegazione abbastanza chiara, anche con le immagini.

Modifica: come riepilogo, i numeri primi sono usati perché hai le migliori possibilità di ottenere un valore univoco quando moltiplichi i valori per il numero primo scelto e li sommi tutti. Ad esempio, data una stringa, moltiplicando il valore di ogni lettera con il numero primo e quindi aggiungendo tutti quelli in alto otterrai il suo valore di hash.

Una domanda migliore sarebbe: perché esattamente il numero 31?


5
Sebbene, penso che un riassunto sarebbe utile, nel caso in cui quel sito fosse mai morto, alcuni resti del suo contenuto verranno salvati qui su SO.
Thomas Owens,

2
L'articolo non spiega perché, ma dice "I ricercatori hanno scoperto che l'uso di un numero primo di 31 offre una migliore distribuzione delle chiavi e un minore numero di collisioni. Nessuno sa perché ..." Divertente, ponendo la stessa domanda come me in effetti .
theschmitzer,

> Una domanda migliore sarebbe: perché esattamente il numero 31? Se intendi il motivo per cui viene utilizzato il numero 31, l'articolo che indichi ti spiega perché, vale a dire perché è veloce da moltiplicare e cos test dimostrano che è il migliore da usare. L'altro moltiplicatore popolare che ho visto è 33, che dà peso alla teoria secondo cui il problema della velocità era (almeno inizialmente) un fattore importante. Se vuoi dire, di cosa si tratta 31 che lo rende migliore nei test, allora temo di non saperlo.
sgmoore,

Esatto, quindi l'unica ragione per cui avrebbe potuto essere usato come moltiplicatore era perché era facile moltiplicare per. (Quando dico di aver visto 33 usato come moltiplicatore, non intendo di recente, questo è stato probabilmente decenni fa, e possibile prima che si facessero molte analisi sull'hashish).
sgmoore,

3
@SteveJessop Il numero 31 è facilmente ottimizzato dalla CPU come un'operazione (x * 32) -1, in cui si *32tratta di un semplice spostamento di bit, o ancora meglio di un fattore di scala dell'indirizzo immediato (ad esempio lea eax,eax*8; leax, eax,eax*4su x86 / x64). Quindi *31è un buon candidato per la moltiplicazione dei numeri primi. Questo era praticamente vero alcuni anni fa - ora l'architettura delle CPU più recenti ha una moltiplicazione quasi istantanea - la divisione è sempre più lenta ...
Arnaud Bouchez,

10

tl; dr

index[hash(input)%2]provocherebbe una collisione per metà di tutti gli hash possibili e un intervallo di valori. index[hash(input)%prime]provoca una collisione di <2 di tutti gli hash possibili. Il fissaggio del divisore alle dimensioni della tabella garantisce inoltre che il numero non possa essere maggiore della tabella.


1
2 è un tizio numero primo
Ganesh Chowdhary Sadanala,

8

I primi sono usati perché hai buone possibilità di ottenere un valore univoco per una tipica funzione hash che utilizza polinomi modulo P. Supponi che usi tale funzione hash per stringhe di lunghezza <= N e hai una collisione. Ciò significa che 2 polinomi diversi producono lo stesso valore modulo P. La differenza di questi polinomi è di nuovo un polinomio dello stesso grado N (o inferiore). Non ha più di N radici (questa è la natura della matematica che si mostra, poiché questa affermazione è vera solo per un polinomio su un campo => numero primo). Quindi, se N è molto inferiore a P, è probabile che tu non abbia una collisione. Successivamente, l'esperimento può probabilmente mostrare che 37 è abbastanza grande da evitare collisioni per una tabella hash di stringhe che ha una lunghezza di 5-10, ed è abbastanza piccolo da usare per i calcoli.


1
Mentre la spiegazione sembra ora ovvia, mi è venuto in mente dopo aver letto un libro di A.Shen "Programmazione: teoremi e problemi" (in russo), vedi la discussione sull'algoritmo di Rabin. Non sono sicuro che esista una traduzione in inglese.
TT_

5

Solo per fornire un punto di vista alternativo c'è questo sito:

http://www.codexon.com/posts/hash-functions-the-modulo-prime-myth

Ciò significa che è necessario utilizzare il maggior numero possibile di bucket anziché arrotondare per difetto a un numero primo di bucket. Sembra una ragionevole possibilità. Intuitivamente, posso certamente vedere come un numero maggiore di bucket sarebbe migliore, ma non sono in grado di formulare un argomento matematico al riguardo.


Un numero maggiore di benne significa meno collisioni: vedi il principio del buco del piccione.
Sconosciuto il

11
@Sconosciuta: non credo sia vero. Per favore, correggimi se sbaglio, ma credo che l'applicazione del principio del buco di piccione ai tavoli di hash ti permetta solo di affermare che ci saranno collisioni se hai più elementi dei bidoni, non trarre conclusioni sulla quantità o sulla densità delle collisioni. Credo comunque che il maggior numero di bin sia la strada corretta, tuttavia.
Falaina,

Se si presume che le collisioni siano casuali a tutti gli effetti, dal paradosso del compleanno uno spazio più ampio (secchi) ridurrà la probabilità che si verifichi una collisione.
Sconosciuto il

1
@Sconosciuta hai perso che le collisioni dipendono anche dalla funzione hash stessa. Quindi se la funzione ha è davvero pessima, quindi non importa quanto aumenti le dimensioni, potrebbe esserci ancora una quantità significativa di collisioni
Suraj Chandran

L'articolo originale sembra essere sparito, ma ci sono alcuni commenti perspicaci qui, inclusa una discussione con l'autore originale. news.ycombinator.com/item?id=650487
Adrian McCarthy,

3

I Primes sono numeri unici. Sono unici in questo, il prodotto di un numero primo con qualsiasi altro numero ha le migliori possibilità di essere unico (non unico come il primo stesso ovviamente) a causa del fatto che un numero primo viene utilizzato per comporlo. Questa proprietà viene utilizzata nelle funzioni di hashing.

Data una stringa "Samuele", puoi generare un hash univoco moltiplicando ciascuna delle cifre o lettere costituenti con un numero primo e aggiungendole. Questo è il motivo per cui vengono utilizzati i numeri primi.

Tuttavia, l'utilizzo dei numeri primi è una vecchia tecnica. La chiave qui per capire che finché puoi generare una chiave sufficientemente unica puoi anche passare ad altre tecniche di hashing. Vai qui per ulteriori informazioni su questo argomento su http://www.azillionmonkeys.com/qed/hash.html

http://computinglife.wordpress.com/2008/11/20/why-do-hash-functions-use-prime-numbers/


1
hahahah .... in realtà il prodotto di 2 numeri primi non ha maggiori possibilità di essere "unico" rispetto al prodotto di un numero primo e di qualsiasi altro numero?
HasaniH,

@Beska Qui "unicità" è definita in modo ricorsivo, quindi credo che la "non unicità" debba essere definita allo stesso modo :)
TT_

3

Dipende dalla scelta della funzione hash.

Molte funzioni di hash combinano i vari elementi nei dati moltiplicandoli con alcuni fattori per la potenza di due corrispondenti alla dimensione della parola della macchina (quel modulo è libero lasciando semplicemente il calcolo traboccante).

Non si desidera alcun fattore comune tra un moltiplicatore per un elemento di dati e la dimensione della tabella hash, perché in tal caso potrebbe accadere che la variazione dell'elemento di dati non distribuisca i dati sull'intera tabella. Se si sceglie un numero primo per la dimensione della tabella, un fattore comune è altamente improbabile.

D'altra parte, questi fattori sono generalmente costituiti da numeri primi dispari, quindi dovresti anche essere sicuro usando potenze di due per la tua tabella hash (ad esempio Eclipse usa 31 quando genera il metodo hashCode () di Java).


2

Supponiamo che la dimensione della tabella (o il numero per modulo) sia T = (B * C). Ora se l'hash per il tuo input è come (N * A * B) dove N può essere qualsiasi numero intero, il tuo output non sarà ben distribuito. Perché ogni volta che n diventa C, 2C, 3C ecc., L'output inizierà a ripetersi. cioè il tuo output sarà distribuito solo in posizioni C. Nota che C qui è (T / HCF (dimensioni tabella, hash)).

Questo problema può essere eliminato creando HCF 1. I numeri primi sono molto utili.

Un'altra cosa interessante è quando T è 2 ^ N. Ciò fornirà l'output esattamente come tutti gli N bit inferiori di input-hash. Poiché ogni numero può essere rappresentato con potenze di 2, quando prenderemo modulo di qualsiasi numero con T, sottrarremo tutte le potenze di 2 numero di modulo, che sono> = N, quindi emettiamo sempre il numero di un modello specifico, dipendente dall'input . Anche questa è una cattiva scelta.

Allo stesso modo, T come 10 ^ N è anche cattivo a causa di ragioni simili (modello in notazione decimale di numeri anziché binario).

Quindi, i numeri primi tendono a dare risultati distribuiti migliori, quindi sono una buona scelta per le dimensioni della tabella.


2

Copia dall'altra mia risposta https://stackoverflow.com/a/43126969/917428 . Guardalo per maggiori dettagli ed esempi.

Credo che abbia a che fare con il fatto che i computer funzionano con la base 2. Pensa solo a come funziona la stessa cosa per la base 10:

  • 8% 10 = 8
  • 18% 10 = 8
  • 87865378% 10 = 8

Non importa quale sia il numero: fintanto che termina con 8, il suo modulo 10 sarà 8.

Scegliere un numero abbastanza grande, senza potenza di due, farà in modo che la funzione hash sia realmente una funzione di tutti i bit di input, piuttosto che un sottoinsieme di essi.


1

Vorrei aggiungere qualcosa per la risposta di Steve Jessop (non posso commentarlo perché non ho abbastanza reputazione). Ma ho trovato del materiale utile. La sua risposta è di grande aiuto, ma ha commesso un errore: la dimensione del secchio non dovrebbe essere una potenza di 2. Citerò solo il libro "Introduzione all'algoritmo" di Thomas Cormen, Charles Leisersen, e altri a pagina 263:

Quando si utilizza il metodo di divisione, solitamente si evitano determinati valori di m. Ad esempio, m non dovrebbe essere una potenza di 2, poiché se m = 2 ^ p, allora h (k) è solo i bit p di ordine inferiore di k. A meno che non sappiamo che tutti i modelli p-bit di ordine inferiore sono ugualmente probabili, è meglio progettare la funzione hash in modo che dipenda da tutti i bit della chiave. Come l'Esercizio 11.3-3 ti chiede di mostrare, scegliere m = 2 ^ p-1 quando k è una stringa di caratteri interpretata in radix 2 ^ p può essere una scelta sbagliata, perché permutare i caratteri di k non cambia il suo valore di hash.

Spero che sia d'aiuto.


0

Per una funzione hash non è solo importante minimizzare le colisioni in generale, ma rendere impossibile rimanere con lo stesso hash mentre si cambiano pochi byte.

Supponi di avere un'equazione: (x + y*z) % key = xcon 0<x<keye 0<z<key. Se chiave è un numero primo n * y = chiave è vera per ogni n in N e falsa per ogni altro numero.

Un esempio in cui chiave non è un esempio primo: x = 1, z = 2 e chiave = 8 Poiché key / z = 4 è ancora un numero naturale, 4 diventa una soluzione per la nostra equazione e in questo caso (n / 2) * y = chiave è vera per ogni n in N. La quantità di soluzioni per l'equazione è praticamente raddoppiata perché 8 non è un numero primo.

Se il nostro aggressore sa già che 8 è la soluzione possibile per l'equazione, può cambiare il file da produrre 8 a 4 e ottenere comunque lo stesso hash.


0

Ho letto il famoso sito Web wordpress collegato in alcune delle risposte popolari sopra in alto. Da quello che ho capito, vorrei condividere una semplice osservazione che ho fatto.

Puoi trovare tutti i dettagli nell'articolo qui , ma supponiamo che quanto segue sia vero:

  • L'uso di un numero primo ci dà la "migliore possibilità" di un valore unico

Un'implementazione hashmap generale vuole che 2 cose siano uniche.

  • Unico codice hash per la chiave
  • Indice univoco per memorizzare il valore effettivo

Come otteniamo l'indice univoco? Rendendo anche la dimensione iniziale del contenitore interno un numero primo. Quindi, in sostanza, prime è coinvolto perché possiede questa caratteristica unica di produrre numeri univoci che finiamo per usare per identificare gli oggetti e trovare gli indici all'interno del contenitore interno.

Esempio:

chiave = "chiave"

valore = "valore" uniqueId = "k" * 31 ^ 2 + "e" * 31 ^ 1` + "y"

viene mappato su ID univoco

Ora vogliamo una posizione unica per il nostro valore, così anche noi

uniqueId % internalContainerSize == uniqueLocationForValue, supponendo che internalContainerSizesia anche un numero primo.

So che questo è semplificato, ma spero di riuscire a far passare l'idea generale.


0

"La natura della matematica" per quanto riguarda i moduli di potenza primaria è che sono un elemento costitutivo di un campo finito . Gli altri due blocchi costitutivi sono un'aggiunta e un'operazione di moltiplicazione. La proprietà speciale dei moduli primi è che formano un campo finito con le operazioni "regolari" di addizione e moltiplicazione, appena portate al modulo. Ciò significa che ogni moltiplicazione si associa a un numero intero diverso dal primo, così come ogni aggiunta.

I moduli primi sono vantaggiosi perché:

  • Offrono la massima libertà nella scelta del moltiplicatore secondario nell'hash secondario, tutti i moltiplicatori tranne 0 finiranno per visitare tutti gli elementi esattamente una volta
  • Se tutti gli hash sono inferiori al modulo non ci saranno collisioni
  • I numeri primi casuali si mescolano meglio della potenza di due moduli e comprimono le informazioni di tutti i bit, non solo un sottoinsieme

Tuttavia hanno un grande svantaggio, richiedono una divisione intera, che richiede molti (~ 15-40) cicli, anche su una CPU moderna. Con circa la metà del calcolo si può assicurarsi che l'hash sia mischiato molto bene. Due moltiplicazioni e operazioni di xorshift si mescolano meglio di un moudulus primario. Quindi possiamo usare qualsiasi dimensione della tabella hash e la riduzione dell'hash è più veloce, dando 7 operazioni in totale per una potenza di 2 dimensioni della tabella e circa 9 operazioni per dimensioni arbitrarie.

Di recente ho esaminato molti dei implementazioni più rapide della tabella hash e la maggior parte di esse non utilizza i moduli primi.


0

Questa domanda è stata unita alla domanda più appropriata, perché le tabelle hash dovrebbero usare array di dimensioni primi e non la potenza di 2. Per le stesse funzioni hash ci sono molte buone risposte qui, ma per la domanda correlata, perché alcune tabelle hash critiche per la sicurezza , come glibc, usa array di dimensioni principali, non ce ne sono ancora.

Generalmente la potenza di 2 tavoli è molto più veloce. C'è il costoso h % n => h & bitmask, dove la maschera di bit può essere calcolata tramite clz("conta gli zeri iniziali") della dimensione n. Una funzione modulo deve eseguire una divisione intera che è circa 50 volte più lenta di una logica and. Ci sono alcuni trucchi per evitare un modulo, come usare l' https : //lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/ di Lemire , ma in genere le tabelle hash veloci usano il potere di 2 e le tabelle hash sicure utilizzano i numeri primi.

Perchè così?

La sicurezza in questo caso è definita da attacchi alla strategia di risoluzione delle collisioni, che con la maggior parte delle tabelle hash è solo una ricerca lineare in un elenco collegato di collisioni. O con le più veloci ricerche lineari a indirizzamento aperto direttamente nella tabella. Quindi, con la potenza di 2 tabelle e una certa conoscenza interna della tabella, ad esempio la dimensione o l'ordine dell'elenco di chiavi fornito da alcune interfacce JSON, si ottiene il numero di bit corretti utilizzati. Il numero di quelli sulla maschera di bit. Questo è generalmente inferiore a 10 bit. E per 5-10 bit è banale collidere con la forza bruta anche con le funzioni di hash più forti e lente. Non hai più la piena sicurezza delle tue funzioni hash a 32 bit o 64 bit. E il punto è usare piccole funzioni hash veloci, non mostri come mormorio o persino sifone.

Quindi, se fornisci un'interfaccia esterna alla tua tabella hash, come un resolver DNS, un linguaggio di programmazione, ... vuoi preoccuparti delle persone che abusano di DOS che amano questi servizi. Normalmente è più facile per queste persone chiudere il servizio pubblico con metodi molto più semplici, ma è successo. Quindi alla gente importava.

Quindi anche le migliori opzioni per prevenire tali attacchi di collisione

1) utilizzare le tabelle primi, perché allora

  • tutti i 32 o 64 bit sono rilevanti per trovare il bucket, non solo alcuni.
  • la funzione di ridimensionamento della tabella hash è più naturale del doppio. La migliore funzione di crescita è la sequenza dei fibonacci e i numeri primi si avvicinano a quelli del raddoppio.

2) usare misure migliori contro l'attacco reale, insieme a una potenza veloce di 2 dimensioni.

  • contare le collisioni e interrompere o dormire sugli attacchi rilevati, ovvero i numeri di collisione con una probabilità <1%. Come 100 con tabelle hash a 32 bit. Questo è ciò che fa ad esempio il risolutore DNS di DJJ.
  • converte l'elenco collegato delle collisioni in albero con O (log n) cerca non O (n) quando viene rilevato un attacco di collisione. Questo è ciò che fa ad esempio Java.

Esiste un mito diffuso secondo cui funzioni hash più sicure aiutano a prevenire tali attacchi, il che è sbagliato come ho spiegato. Non c'è sicurezza solo con bit bassi. Funzionerebbe solo con tabelle di dimensioni primi, ma utilizzerebbe una combinazione dei due metodi più lenti, hash lento più modulo principale lento.

Le funzioni di hash per le tabelle di hash devono essere principalmente piccole (per essere inlinabili) e veloci. La sicurezza può venire solo impedendo la ricerca lineare nelle collisioni. E non usare funzioni hash banalmente cattive, come quelle insensibili ad alcuni valori (come \ 0 quando si usa la moltiplicazione).

Anche l'uso di semi casuali è una buona opzione, le persone hanno iniziato con quello prima, ma con sufficienti informazioni sulla tabella anche un seme casuale non aiuta molto, e i linguaggi dinamici in genere rendono banale ottenere il seme tramite altri metodi, poiché è archiviato in posizioni di memoria note.


-1
function eratosthenes(n) {

    function getPrime(x) {
        var middle = (x-(x%2))/2;
        var arr_rest = [];
        for(var j=2 ; j<=middle;j++){
            arr_rest.push(x%j);
        }

        if(arr_rest.indexOf(0) == -1) {
            return true
        }else {
            return false
        }

    }
    if(n<2)  {
        return []
    }else if(n==2){
        return [2]
    }else {
        var arr = [2]
        for(var i=3;i<n;i++) {
            if(getPrime(i)){
                arr.push(i)
            }
        }
    }

    return arr;
}

2
Potresti aggiungere commenti per spiegare la tua soluzione, per favore?
pom421,
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.