Perché usare un numero primo in hashCode?


174

Mi chiedevo solo perché i numeri primi sono usati nel hashCode()metodo di una classe ? Ad esempio, quando si utilizza Eclipse per generare il mio hashCode()metodo, viene sempre 31utilizzato il numero primo :

public int hashCode() {
     final int prime = 31;
     //...
}

Riferimenti:

Ecco un buon primer su Hashcode e l'articolo su come funziona l'hashing che ho trovato (C # ma i concetti sono trasferibili): Linee guida e regole di Eric Lippert per GetHashCode ()



Questo è più o meno un duplicato della domanda stackoverflow.com/questions/1145217/… .
Hans-Peter Störr,

1
Controlla la mia risposta su stackoverflow.com/questions/1145217/… È correlata alle proprietà dei polinomi su un campo (non un anello!), Quindi numeri primi.
TT_

Risposte:


104

Perché vuoi che il numero che stai moltiplicando e il numero di bucket che stai inserendo abbiano fattorizzazioni primarie ortogonali.

Supponiamo che ci siano 8 secchi in cui inserire. Se il numero che stai utilizzando per moltiplicare è un multiplo di 8, il bucket inserito sarà determinato solo dalla voce meno significativa (quella non moltiplicata). Le voci simili si scontreranno. Non va bene per una funzione hash.

31 è un numero primo abbastanza grande che è improbabile che il numero di bucket sia divisibile per esso (e in effetti, le moderne implementazioni Java HashMap mantengono il numero di bucket a una potenza di 2).


9
Quindi una funzione hash che si moltiplica per 31 verrà eseguita in modo non ottimale. Tuttavia, considererei un'implementazione di tale tabella hash mal progettata, dato quanto 31 sia comune come moltiplicatore.
ILMTitan,

11
Quindi 31 viene scelto in base al presupposto che gli implementatori della tabella hash sappiano che 31 è comunemente usato nei codici hash?
Steve Kuo,

3
31 viene scelto in base all'idea che la maggior parte delle implementazioni hanno fattorizzazioni di numeri primi relativamente piccoli. 2s, 3s e 5s di solito. Può iniziare a 10 e crescere 3X quando diventa troppo pieno. La dimensione è raramente del tutto casuale. E anche se fosse, 30/31 non sono cattive probabilità di avere algoritmi hash ben sincronizzati. Può anche essere facile da calcolare, come altri hanno affermato.
ILMTitan,

8
In altre parole ... dobbiamo sapere qualcosa sull'insieme dei valori di input e sulle regolarità dell'insieme, al fine di scrivere una funzione progettata per eliminarli da tali regolarità, quindi i valori nell'insieme non si scontrano nello stesso secchi di hash. Moltiplicare / Dividere / Modulo per un numero primo ottiene quell'effetto, perché se hai un LOOP con oggetti X e salti gli spazi Y nel ciclo, non tornerai mai nello stesso punto fino a quando X diventa un fattore Y Dato che X è spesso un numero pari o una potenza di 2, allora è necessario che Y sia primo, quindi X + X + X ... non è un fattore Y, quindi 31 yay! : /
Triynko,

3
@FrankQ. È la natura dell'aritmetica modulare. (x*8 + y) % 8 = (x*8) % 8 + y % 8 = 0 + y % 8 = y % 8
ILMTitan,

136

I numeri primi sono scelti per distribuire al meglio i dati tra i bucket hash. Se la distribuzione degli input è casuale e uniformemente diffusa, la scelta del codice hash / modulo non ha importanza. Ha un impatto solo quando c'è un certo schema negli input.

Questo è spesso il caso quando si tratta di posizioni di memoria. Ad esempio, tutti gli interi a 32 bit sono allineati agli indirizzi divisibili per 4. Controlla la tabella seguente per visualizzare gli effetti dell'uso di un modulo primo e non primo:

Input       Modulo 8    Modulo 7
0           0           0
4           4           4
8           0           1
12          4           5
16          0           2
20          4           6
24          0           3
28          4           0

Notare la distribuzione quasi perfetta quando si utilizza un modulo primo rispetto a un modulo non primo.

Tuttavia, sebbene l'esempio sopra sia ampiamente inventato, il principio generale è che quando si ha a che fare con uno schema di input , l'uso di un modulo con numeri primi produrrà la migliore distribuzione.


17
Non stiamo parlando del moltiplicatore utilizzato per generare il codice hash, non del modulo utilizzato per ordinare quei codici hash in bucket?
ILMTitan,

3
Stesso principio. In termini di I / O, l'hash si inserisce nell'operazione modulo della tabella hash. Penso che il punto sia che se si moltiplica per numeri primi, si otterranno input distribuiti in modo più casuale nel punto in cui il modulo non avrà nemmeno importanza. Dato che la funzione hash rileva la debolezza della distribuzione migliore degli input, rendendoli meno regolari, è meno probabile che si scontrino, indipendentemente dal modulo utilizzato per metterli in un bucket.
Triynko,

9
Questo tipo di risposta è molto utile perché è come insegnare a qualcuno come pescare, piuttosto che catturarne uno per loro. Aiuta le persone a vedere e comprendere il principio alla base dell'utilizzo dei numeri primi per gli hash ... che è quello di distribuire gli input in modo irregolare in modo che cadano uniformemente in secchi una volta modellati :).
Triynko,

29

Per quello che vale, Effective Java 2nd Edition rinuncia al problema matematico e dice solo che il motivo per scegliere 31 è:

  • Perché è uno strano numero primo, ed è "tradizionale" usare i numeri primi
  • È anche uno in meno di una potenza di due, che consente l'ottimizzazione bit a bit

Ecco la citazione completa, dall'articolo 9: Sostituisci sempre hashCodequando esegui l'overrideequals :

Il valore 31 è stato scelto perché è un numero primo dispari. Se fosse pari e la moltiplicazione traboccasse, le informazioni verrebbero perse, poiché la moltiplicazione per 2 equivale allo spostamento. Il vantaggio di usare un numero primo è meno chiaro, ma è tradizionale.

Una bella proprietà di 31 è che la moltiplicazione può essere sostituita da uno spostamento ( §15.19 ) e sottrazione per prestazioni migliori:

 31 * i == (i << 5) - i

Le VM moderne eseguono automaticamente questo tipo di ottimizzazione.


Mentre la ricetta in questo articolo offre funzioni hash ragionevolmente buone, non produce funzioni hash all'avanguardia, né le librerie della piattaforma Java forniscono tali funzioni hash dalla versione 1.6. Scrivere tali funzioni di hash è un argomento di ricerca, meglio lasciato ai matematici e agli informatici teorici.

Forse una versione successiva della piattaforma fornirà funzioni hash all'avanguardia per le sue classi e metodi di utilità per consentire ai programmatori medi di costruire tali funzioni hash. Nel frattempo, le tecniche descritte in questo articolo dovrebbero essere adeguate per la maggior parte delle applicazioni.

Piuttosto semplicisticamente, si può dire che l'uso di un moltiplicatore con numerosi divisori comporterà più collisioni di hash . Poiché per un hashing efficace vogliamo ridurre al minimo il numero di collisioni, proviamo a utilizzare un moltiplicatore con meno divisori. Un numero primo per definizione ha esattamente due divisori positivi distinti.

Domande correlate


4
Eh, ma ci son molti adatti numeri primi che sono o 2 ^ n + 1 (i cosiddetti numeri primi di Fermat ), vale a dire 3, 5, 17, 257, 65537o 2 ^ n - 1 ( primi di Mersenne ): 3, 7, 31, 127, 8191, 131071, 524287, 2147483647. Tuttavia 31(e non, diciamo, 127) è optato.
Dmitry Bychenko il

4
"perché è uno strano numero primo" ... ce n'è solo uno primo: P
Martin Schneider,

Non mi piace la dicitura "è meno chiara, ma è tradizionale" in "Efficace Java". Se non vuole entrare nei dettagli matematici, dovrebbe invece scrivere qualcosa del tipo "ha ragioni matematiche [simili]". Il modo in cui scrive suona come se avesse solo un background storico :(
Qw3ry

5

Ho sentito che 31 è stato scelto in modo che il compilatore possa ottimizzare la moltiplicazione a 5 bit con spostamento a sinistra, quindi sottrarre il valore.


come potrebbe il compilatore ottimizzare in quel modo? x * 31 == x * 32-1 non è vero per tutti x dopotutto. Quello che volevi dire era lasciare il turno 5 (equivale a moltiplicare per 32) e quindi sottrarre il valore originale (x nel mio esempio). Mentre questo potrebbe essere più veloce di una moltiplicazione (a proposito non è per i moderni processori della cpu), ci sono altri fattori importanti da considerare quando si sceglie una moltiplicazione per un haschcode (viene in mente un'equa distribuzione dei valori di input ai bucket)
Grizzly

Fai un po 'di ricerca, questa è un'opinione abbastanza comune.
Steve Kuo,

4
L'opinione comune è irrilevante.
fattore

1
@Grizzly, è più veloce della moltiplicazione. IMul ​​ha una latenza minima di 3 cicli su qualsiasi CPU moderna. (vedi i manuali di agner fog) mov reg1, reg2-shl reg1,5-sub reg1,reg2può essere eseguito in 2 cicli. (il mov è solo una ridenominazione e richiede 0 cicli).
Johan

3

Ecco una citazione un po 'più vicina alla fonte.

Si riduce a:

  • 31 è primo, il che riduce le collisioni
  • 31 produce una buona distribuzione, con
  • un ragionevole compromesso in termini di velocità

3

Innanzitutto si calcola il valore di hash modulo 2 ^ 32 (la dimensione di un int ), quindi vuoi qualcosa di relativamente primo a 2 ^ 32 (relativamente primo significa che non ci sono divisori comuni). Qualsiasi numero dispari farebbe per quello.

Quindi per una data tabella hash l'indice viene solitamente calcolato dal valore hash modulo della dimensione della tabella hash, quindi si desidera qualcosa che sia relativamente primo rispetto alla dimensione della tabella hash. Spesso le dimensioni delle tabelle hash sono scelte come numeri primi per questo motivo. Nel caso di Java l'implementazione di Sun assicura che la dimensione sia sempre una potenza di due, quindi un numero dispari sarebbe sufficiente anche qui. C'è anche un ulteriore massaggio delle chiavi hash per limitare ulteriormente le collisioni.

L'effetto negativo se la tabella hash e il moltiplicatore avessero un fattore comune npotrebbe essere che in determinate circostanze verrebbero utilizzate solo 1 / n voci nella tabella hash.


2

Il motivo per cui vengono utilizzati i numeri primi è di ridurre al minimo le collisioni quando i dati mostrano alcuni schemi particolari.

Per prima cosa: se i dati sono casuali, non è necessario un numero primo, puoi eseguire un'operazione mod contro qualsiasi numero e avrai lo stesso numero di collisioni per ogni possibile valore del modulo.

Ma quando i dati non sono casuali, accadono cose strane. Ad esempio, considera i dati numerici che sono sempre multipli di 10.

Se usiamo la mod 4 troviamo:

10 mod 4 = 2

20 mod 4 = 0

30 mod 4 = 2

40 mod 4 = 0

50 mod 4 = 2

Quindi dai 3 possibili valori del modulo (0,1,2,3) solo 0 e 2 avranno collisioni, il che è negativo.

Se utilizziamo un numero primo come 7:

10 mod 7 = 3

20 mod 7 = 6

30 mod 7 = 2

40 mod 7 = 4

50 mod 7 = 1

eccetera

Notiamo anche che 5 non è una buona scelta ma 5 è il primo, il motivo è che tutte le nostre chiavi sono un multiplo di 5. Ciò significa che dobbiamo scegliere un numero primo che non divide le nostre chiavi, scegliendo un numero primo grande è di solito abbastanza.

Quindi, dal punto di vista dell'errore, di essere ripetitivi, il motivo per cui vengono utilizzati i numeri primi è quello di neutralizzare l'effetto dei modelli nei tasti nella distribuzione delle collisioni di una funzione hash.


1

31 è anche specifico di Java HashMap che utilizza un int come tipo di dati hash. Quindi la capacità massima di 2 ^ 32. Non ha senso usare i numeri primi Fermat o Mersenne più grandi.


0

In genere consente di ottenere una diffusione più uniforme dei dati tra i bucket hash, in particolare per le chiavi a bassa entropia.

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.