Quale funzione hash integer è buona che accetta una chiave hash intera?


Risposte:


47

Il metodo moltiplicativo di Knuth:

hash(i)=i*2654435761 mod 2^32

In generale, dovresti scegliere un moltiplicatore che sia nell'ordine della tua dimensione hash ( 2^32nell'esempio) e non abbia fattori comuni con esso. In questo modo la funzione hash copre tutto lo spazio hash in modo uniforme.

Modifica: il più grande svantaggio di questa funzione hash è che conserva la divisibilità, quindi se i tuoi numeri interi sono tutti divisibili per 2 o per 4 (il che non è raro), anche i loro hash lo saranno. Questo è un problema nelle tabelle hash: potresti ritrovarti con solo 1/2 o 1/4 dei bucket utilizzati.


36
È una funzione hash davvero pessima, anche se collegata a un nome famoso.
Seun Osewa

5
Non è affatto una cattiva funzione hash se usata con le dimensioni della tabella principale. Inoltre, è pensato per l' hashing chiuso . Se i valori hash non sono distribuiti in modo uniforme, l'hashing moltiplicativo garantisce che le collisioni da un valore difficilmente "disturbano" gli elementi con altri valori hash.
Paolo Bonzini

11
Per i curiosi, questa costante viene scelta come dimensione dell'hash (2 ^ 32) divisa per Phi
awdz9nld

7
Paolo: Il metodo di Knuth è "cattivo", nel senso che non fa valanga sulle punte superiori
awdz9nld

9
A un esame più attento, risulta che 2654435761 è in realtà un numero primo. Quindi questo è probabilmente il motivo per cui è stato scelto piuttosto che 2654435769.
karadoc

149

Ho trovato che il seguente algoritmo fornisce un'ottima distribuzione statistica. Ogni bit di ingresso influenza ogni bit di uscita con circa il 50% di probabilità. Non ci sono collisioni (ogni input risulta in un output diverso). L'algoritmo è veloce tranne se la CPU non dispone di un'unità di moltiplicazione di numeri interi incorporata. Codice C, supponendo che intsia a 32 bit (per Java, sostituire >>con >>>e rimuovere unsigned):

unsigned int hash(unsigned int x) {
    x = ((x >> 16) ^ x) * 0x45d9f3b;
    x = ((x >> 16) ^ x) * 0x45d9f3b;
    x = (x >> 16) ^ x;
    return x;
}

Il numero magico è stato calcolato utilizzando uno speciale programma di test multi-thread che ha funzionato per molte ore, che calcola l'effetto valanga (il numero di bit di uscita che cambiano se viene cambiato un singolo bit di ingresso; dovrebbe essere in media quasi 16), indipendenza di i bit di uscita cambiano (i bit di uscita non dovrebbero dipendere l'uno dall'altro) e la probabilità di un cambiamento in ogni bit di uscita se viene modificato un bit di ingresso. I valori calcolati sono migliori del finalizzatore a 32 bit utilizzato da MurmurHash e quasi altrettanto buoni (non del tutto) come quando si utilizza AES . Un leggero vantaggio è che la stessa costante viene utilizzata due volte (l'ultima volta che l'ho testata l'ha resa leggermente più veloce, non sono sicuro che sia ancora così).

È possibile invertire il processo (ottenere il valore di input dall'hash) se si sostituisce 0x45d9f3bcon 0x119de1f3(l' inverso moltiplicativo ):

unsigned int unhash(unsigned int x) {
    x = ((x >> 16) ^ x) * 0x119de1f3;
    x = ((x >> 16) ^ x) * 0x119de1f3;
    x = (x >> 16) ^ x;
    return x;
}

Per i numeri a 64 bit, suggerisco di utilizzare quanto segue, anche se potrebbe non essere il più veloce. Questo è basato su splitmix64 , che sembra essere basato sull'articolo del blog Better Bit Mixing (mix 13).

uint64_t hash(uint64_t x) {
    x = (x ^ (x >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);
    x = (x ^ (x >> 27)) * UINT64_C(0x94d049bb133111eb);
    x = x ^ (x >> 31);
    return x;
}

Per Java, usa long, aggiungi Lalla costante, sostituisci >>con >>>e rimuovi unsigned. In questo caso, la retromarcia è più complicata:

uint64_t unhash(uint64_t x) {
    x = (x ^ (x >> 31) ^ (x >> 62)) * UINT64_C(0x319642b2d24d8ec3);
    x = (x ^ (x >> 27) ^ (x >> 54)) * UINT64_C(0x96de1b173f119089);
    x = x ^ (x >> 30) ^ (x >> 60);
    return x;
}

Aggiornamento: potresti anche voler esaminare il progetto Hash Function Prospector , dove sono elencate altre costanti (possibilmente migliori).


2
le prime due righe sono esattamente le stesse! c'è un errore di battitura qui?
Kshitij Banerjee

3
No, questo non è un errore di battitura, la seconda riga mescola ulteriormente i bit. Usare solo una moltiplicazione non è così buono.
Thomas Mueller

3
Ho cambiato il numero magico perché secondo un test case ho scritto il valore 0x45d9f3b fornisce una migliore confusione e diffusione , specialmente che se un bit di uscita cambia, ogni altro bit di uscita cambia con circa la stessa probabilità (oltre a tutti i bit di uscita cambiano con il stessa probabilità se cambia un bit di ingresso). Come hai misurato 0x3335b369 funziona meglio per te? È un int 32 bit per te?
Thomas Mueller

3
Sto cercando una bella funzione hash per unsigned int a 64 bit a int unsigned a 32 bit. In tal caso, il numero magico sopra sarà lo stesso? Ho spostato 32 bit invece di 16 bit.
alessandro

3
Credo che in tal caso sarebbe meglio un fattore più grande, ma sarebbe necessario eseguire alcuni test. Oppure (questo è quello che faccio) prima uso x = ((x >> 32) ^ x)e poi usa le moltiplicazioni a 32 bit sopra. Non sono sicuro di cosa sia meglio. Potresti anche voler guardare il finalizzatore
Thomas Mueller

29

Dipende da come vengono distribuiti i dati. Per un semplice contatore, la funzione più semplice

f(i) = i

sarà buono (sospetto ottimale, ma non posso provarlo).


3
Il problema con questo è che è comune avere grandi insiemi di numeri interi divisibili per un fattore comune (indirizzi di memoria allineati a parole, ecc.). Ora, se la tua tabella hash è divisibile per lo stesso fattore, ti ritroverai con solo la metà (o 1/4, 1/8, ecc.) Bucket utilizzati.
Rafał Dowgird

8
@Rafal: Ecco perché la risposta dice "per un contatore semplice" e "Dipende da come vengono distribuiti i tuoi dati"
erikkallen

5
Questa è in realtà l'implementazione da parte di Sun del metodo hashCode () in java.lang.Integer grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/…
Juande Carrion

5
@JuandeCarrion Questo è fuorviante perché non è l'hash utilizzato. Dopo essere passato all'utilizzo della potenza di due dimensioni di tabella, Java rehash ogni hash restituito .hashCode(), vedi qui .
Esailija

8
La funzione di identità è abbastanza inutile come hash in molte applicazioni pratiche a causa delle sue proprietà distributive (o della loro mancanza), a meno che, ovviamente, la località non sia un attributo desiderato
awdz9nld

12

Le funzioni hash veloci e buone possono essere composte da permutazioni veloci con qualità minori, come

  • moltiplicazione con un numero intero irregolare
  • rotazioni binarie
  • xorshift

Per produrre una funzione di hashing con qualità superiori, come dimostrato con PCG per la generazione di numeri casuali.

Questa è infatti anche la ricetta che rrxmrrxmsx_0 e il murmur hash stanno usando, consapevolmente o inconsapevolmente.

Ho trovato personalmente

uint64_t xorshift(const uint64_t& n,int i){
  return n^(n>>i);
}
uint64_t hash(const uint64_t& n){
  uint64_t p = 0x5555555555555555ull; // pattern of alternating 0 and 1
  uint64_t c = 17316035218449499591ull;// random uneven integer constant; 
  return c*xorshift(p*xorshift(n,32),32);
}

essere abbastanza bravo.

Una buona funzione hash dovrebbe

  1. essere biettivi per non perdere informazioni, se possibile e avere il minor numero di collisioni
  2. cascata il più possibile e il più uniformemente possibile, ovvero ogni bit di ingresso dovrebbe invertire ogni bit di uscita con probabilità 0,5.

Diamo prima un'occhiata alla funzione di identità. Soddisfa 1. ma non 2.:

funzione di identità

Il bit di ingresso n determina il bit di uscita n con una correlazione del 100% (rosso) e nessun altro, sono quindi blu, dando una linea rossa perfetta attraverso.

Uno xorshift (n, 32) non è molto meglio, producendo una linea e mezza. Ancora soddisfacente 1., perché invertibile con una seconda applicazione.

xorshift

Una moltiplicazione con un intero senza segno è molto meglio, a cascata in modo più forte e capovolgendo più bit di output con una probabilità di 0,5, che è quello che vuoi, in verde. Soddisfa 1. poiché per ogni numero intero dispari c'è un inverso moltiplicativo.

knuth

La combinazione dei due dà il seguente output, ancora soddisfacente 1. poiché la composizione di due funzioni biiettive produce un'altra funzione biiettiva.

knuth • xorshift

Una seconda applicazione di moltiplicazione e xorshift produrrà quanto segue:

hash proposto

Oppure puoi usare le moltiplicazioni di campo di Galois come GHash , sono diventate ragionevolmente veloci sulle moderne CPU e hanno qualità superiori in un unico passaggio.

   uint64_t const inline gfmul(const uint64_t& i,const uint64_t& j){           
     __m128i I{};I[0]^=i;                                                          
     __m128i J{};J[0]^=j;                                                          
     __m128i M{};M[0]^=0xb000000000000000ull;                                      
     __m128i X = _mm_clmulepi64_si128(I,J,0);                                      
     __m128i A = _mm_clmulepi64_si128(X,M,0);                                      
     __m128i B = _mm_clmulepi64_si128(A,M,0);                                      
     return A[0]^A[1]^B[1]^X[0]^X[1];                                              
   }

gfmul: Il codice sembra essere uno pseudo-codice, poiché afaik non puoi usare le parentesi con __m128i. Ancora molto interessante. La prima riga sembra dire "prendi un __m128i unitializzato (I) e xor con (parametro) i. Dovrei leggerlo come inizializzare I con 0 e xor con i? In tal caso, sarebbe lo stesso di caricare I con i ed eseguire una non (funzionamento) in poi?
gen

@ Jan quello che vorrei è fare __m128i I = i; //set the lower 64 bits, ma non posso, quindi sto usando ^=. 0^1 = 1quindi no non coinvolto. Per quanto riguarda l'inizializzazione con il {}mio compilatore non mi sono mai lamentato, potrebbe non essere la soluzione migliore, ma quello che voglio è inizializzare tutto a 0 così posso fare ^=o |=. Penso di aver basato quel codice su questo post del blog che dà anche l'inversione, molto utile: D
Wolfgang Brehm

6

Questa pagina elenca alcune semplici funzioni di hash che tendono ad essere decentemente in generale, ma qualsiasi hash semplice ha casi patologici in cui non funziona bene.


6
  • Metodo moltiplicativo a 32 bit (molto veloce) vedi @rafal

    #define hash32(x) ((x)*2654435761)
    #define H_BITS 24 // Hashtable size
    #define H_SHIFT (32-H_BITS)
    unsigned hashtab[1<<H_BITS]  
    .... 
    unsigned slot = hash32(x) >> H_SHIFT
  • 32 bit e 64 bit (buona distribuzione) a: MurmurHash

  • Funzione hash intero

3

C'è una bella panoramica su alcuni algoritmi di hash su Eternally Confuzzled . Consiglierei l'hash one-at-a-time di Bob Jenkins che raggiunge rapidamente una valanga e quindi può essere utilizzato per un'efficiente ricerca nella tabella hash.


4
Questo è un buon articolo, ma è incentrato sull'hashing delle chiavi delle stringhe, non sui numeri interi.
Adrian Mouat

Giusto per essere chiari, sebbene i metodi nell'articolo funzionerebbero per i numeri interi (o potrebbero essere adattati a), presumo che ci siano algoritmi più efficienti per i numeri interi.
Adrian Mouat,

2

La risposta dipende da molte cose come:

  • Dove intendi impiegarlo?
  • Cosa stai cercando di fare con l'hash?
  • Hai bisogno di una funzione hash crittograficamente sicura?

Suggerisco di dare un'occhiata alla famiglia di funzioni hash Merkle-Damgard come SHA-1 ecc


1

Non credo si possa dire che una funzione hash sia "buona" senza conoscere i propri dati in anticipo! e senza sapere cosa farai con esso.

Esistono strutture di dati migliori delle tabelle hash per dimensioni di dati sconosciute (presumo che tu stia facendo l'hashing per una tabella hash qui). Personalmente userei una tabella hash quando so di avere un numero "finito" di elementi che devono essere memorizzati in una quantità limitata di memoria. Proverei a fare una rapida analisi statistica sui miei dati, vedere come sono distribuiti ecc. Prima di iniziare a pensare alla mia funzione hash.


1

Per i valori hash casuali, alcuni ingegneri hanno detto che il numero primo della sezione aurea (2654435761) è una cattiva scelta, con i risultati dei miei test, ho scoperto che non è vero; invece, 2654435761 distribuisce i valori hash abbastanza bene.

#define MCR_HashTableSize 2^10

unsigned int
Hash_UInt_GRPrimeNumber(unsigned int key)
{
  key = key*2654435761 & (MCR_HashTableSize - 1)
  return key;
}

La dimensione della tabella hash deve essere una potenza di due.

Ho scritto un programma di test per valutare molte funzioni hash per interi, i risultati mostrano che GRPrimeNumber è una buona scelta.

Ho provato:

  1. total_data_entry_number / total_bucket_number = 2, 3, 4; dove total_bucket_number = dimensione della tabella hash;
  2. mappare il dominio del valore hash nel dominio dell'indice del bucket; ovvero, converti il ​​valore hash nell'indice del bucket tramite Logical And Operation con (hash_table_size - 1), come mostrato in Hash_UInt_GRPrimeNumber ();
  3. calcolare il numero di collisioni di ogni benna;
  4. registrare il bucket che non è stato mappato, ovvero un bucket vuoto;
  5. scoprire il numero massimo di collisioni di tutte le benne; cioè la lunghezza della catena più lunga;

Con i risultati dei miei test, ho scoperto che Golden Ratio Prime Number ha sempre meno secchi vuoti o zero secchi vuoti e la lunghezza della catena di collisione più corta.

Alcune funzioni hash per gli interi sono ritenute valide, ma i risultati dei test mostrano che quando total_data_entry / total_bucket_number = 3, la lunghezza della catena più lunga è maggiore di 10 (numero massimo di collisioni> 10) e molti bucket non sono mappati (bucket vuoti ), che è pessimo, rispetto al risultato di zero secchio vuoto e lunghezza della catena più lunga 3 di Golden Ratio Prime Number Hashing.

A proposito, con i risultati dei miei test, ho scoperto che una versione delle funzioni hash shifting-xor è abbastanza buona (è condivisa da mikera).

unsigned int Hash_UInt_M3(unsigned int key)
{
  key ^= (key << 13);
  key ^= (key >> 17);    
  key ^= (key << 5); 
  return key;
}

2
Ma allora perché non spostare correttamente il prodotto, in modo da mantenere i bit più misti? Questo era il modo in cui avrebbe dovuto funzionare
Harold

1
@harold, il numero primo della sezione aurea è scelto con cura, anche se penso che non farà alcuna differenza, ma proverò a vedere se è molto meglio con "i bit più mescolati". Mentre il mio punto è che "Non è una buona scelta". non è vero, come mostrano i risultati del test, afferrare la parte inferiore dei bit è abbastanza buono, e anche meglio di molte funzioni hash.
Chen-ChungChia

(2654435761, 4295203489) è un rapporto aureo dei numeri primi.
Chen-ChungChia

(1640565991, 2654435761) è anche un rapporto aureo dei numeri primi.
Chen-ChungChia,

@harold, Spostare il prodotto a destra peggiora, anche se spostandosi a destra di 1 posizione (diviso per 2), peggiora ancora (anche se il secchio vuoto è ancora zero, ma la lunghezza della catena più lunga è maggiore); spostandosi a destra di più posizioni, il risultato diventa ancora peggiore. Perché? Penso che il motivo sia: spostare il prodotto a destra fa sì che più valori hash non siano coprimi, solo la mia ipotesi, la vera ragione coinvolge la teoria dei numeri.
Chen-ChungChia,

1

Ho usato splitmix64(indicato nella risposta di Thomas Mueller ) da quando ho trovato questo thread. Tuttavia, di recente mi sono imbattuto in Pelle Evensen rrxmrrxmsx_0 di , che ha prodotto una distribuzione statistica incredibilmente migliore rispetto al finalizzatore MurmurHash3 originale e ai suoi successori ( splitmix64e altri mix). Ecco lo snippet di codice in C:

#include <stdint.h>

static inline uint64_t ror64(uint64_t v, int r) {
    return (v >> r) | (v << (64 - r));
}

uint64_t rrxmrrxmsx_0(uint64_t v) {
    v ^= ror64(v, 25) ^ ror64(v, 50);
    v *= 0xA24BAED4963EE407UL;
    v ^= ror64(v, 24) ^ ror64(v, 49);
    v *= 0x9FB21C651E98DF25UL;
    return v ^ v >> 28;
}

Pelle fornisce anche un'analisi approfondita del mixer a 64 bit utilizzato nella fase finale di MurmurHash3e delle varianti più recenti.


2
Questa funzione non è biiettiva. Per tutti i v dove v = ror (v, 25), vale a dire tutti 0 e tutti 1, produrrà lo stesso output in due punti. Per tutti i valori v = ror64 (v, 24) ^ ror64 (v, 49), che sono almeno altri due e uguali a v = ror (v, 28), producendo un altro 2 ^ 4, per un totale di circa 22 collisioni non necessarie . Due applicazioni di splitmix sono probabilmente altrettanto buone e altrettanto veloci, ma comunque invertibili e prive di collisioni.
Wolfgang Brehm
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.