funzione hash per stringa


124

Sto lavorando su una tabella hash in linguaggio C e sto testando la funzione hash per la stringa.

La prima funzione che ho provato è aggiungere codice ASCII e utilizzare modulo (% 100) ma ho ottenuto scarsi risultati con il primo test di dati: 40 collisioni per 130 parole.

I dati di input finali conterranno 8 000 parole (è un dizionario memorizzato in un file). La tabella hash è dichiarata come tabella int [10000] e contiene la posizione della parola in un file txt.

La prima domanda è qual è il miglior algoritmo per la stringa di hashing? e come determinare la dimensione della tabella hash?

Grazie in anticipo !

:-)


11
Se la tua tabella hash ha 10K voci, perché dovresti usare modulo 100? Ottenere 40 collisioni su 130 parole non è sorprendente con un modulo così piccolo.
Carey Gregory

13
Vedi burtleburtle.net/bob/hash/evahash.html e partow.net/programming/hashfunctions per le quali sono risorse su vari hashing (dal generale alla stringa alla crittografia).

3
Per chiarire @CareyGregory: ti rendi conto che, come verità matematica di base, 130 elementi in 100 secchi (cioè, mod 100) devono produrre 30 collisioni (dove la collisione viene conteggiata ogni volta che viene inserito un secondo, terzo, ecc. un secchio), corretto? Quindi sei solo un po 'al di sopra di questo.
derobert

4
@lilawood: OK, è quello che ho pensato, ma per essere un test migliore dovresti usare 80 parole con una tabella hash di 100 voci. Ciò ti darebbe le stesse proporzioni dei tuoi dati in tempo reale e non forzerebbe le collisioni.
Carey Gregory

4
Possibile duplicato della buona funzione hash per stringhe
MJ Rayburn

Risposte:


185

Ho avuto ottimi risultati con djb2Dan Bernstein.

unsigned long
hash(unsigned char *str)
{
    unsigned long hash = 5381;
    int c;

    while (c = *str++)
        hash = ((hash << 5) + hash) + c; /* hash * 33 + c */

    return hash;
}

37
la pagina collegata nella risposta è molto interessante.
Adrien Plisson

2
come il programma esaurisce il ciclo while? = S
Daniel N.

1
@ danfly09 Quando c è zero. L'equivalente di while (c = * str ++) sarebbe (0! = (C = * str ++))
rxantos

5
@Josepas la funzione hash dovrebbe idealmente restituire uno size_to un altro valore non firmato (come il long unsigned in questo codice). Il chiamante è responsabile di prendere il modulo del risultato per adattarlo alla tabella hash. Il chiamante controlla lo slot della tabella su cui viene eseguito l'hashing; non la funzione. Restituisce solo un numero non firmato.
WhozCraig

6
sorprendente. questo algoritmo ha battuto a morte l'hash Murmur, gli hash delle varianti FNV e molti altri! +1
David Haim

24

In primo luogo, generalmente non si desidera utilizzare un hash crittografico per una tabella hash. Un algoritmo molto veloce per gli standard crittografici è ancora estremamente lento per gli standard delle tabelle hash.

Secondo, vuoi assicurarti che ogni bit dell'input possa / influenzerà il risultato. Un modo semplice per farlo è ruotare il risultato corrente di un certo numero di bit, quindi XOR il codice hash corrente con il byte corrente. Ripeti fino a raggiungere la fine della stringa. Nota che generalmente non vuoi che la rotazione sia un multiplo pari della dimensione in byte.

Ad esempio, supponendo il caso comune di 8 bit byte, potresti ruotare di 5 bit:

int hash(char const *input) { 
    int result = 0x55555555;

    while (*input) { 
        result ^= *input++;
        result = rol(result, 5);
    }
}

Modifica: si noti inoltre che 10000 slot sono raramente una buona scelta per le dimensioni di una tabella hash. Di solito vuoi una delle due cose: o vuoi un numero primo come dimensione (richiesto per garantire la correttezza con alcuni tipi di risoluzione hash) oppure una potenza di 2 (quindi ridurre il valore all'intervallo corretto può essere fatto con un semplice maschera di bit).


Questo non è c, ma sarei interessato nei vostri pensieri a questa risposta correlato: stackoverflow.com/a/31440118/3681880
Suragch

1
@ Suragch: Da quando ho scritto questo, parecchi processori hanno iniziato a includere hardware speciale per accelerare il calcolo SHA, il che lo ha reso molto più competitivo. Detto questo, dubito che il tuo codice sia sicuro come pensi: ad esempio, i numeri in virgola mobile IEEE hanno due diversi modelli di bit (0 e -0) che dovrebbero produrre gli stessi hash (si confronteranno uguali tra loro ).
Jerry Coffin

@ Jerry Coffin di quale libreria ho bisogno per la funzione rol ()?
grazie, un

@ thanos.a: Non sono a conoscenza del fatto che si trovi in ​​una libreria, ma il tuo lancio richiede solo una o due righe di codice. Sposta un blocco a sinistra, l'altro a destra e / o insieme.
Jerry Coffin

8

Wikipedia mostra una bella funzione hash di stringa chiamata Jenkins One At A Time Hash. Cita anche versioni migliorate di questo hash.

uint32_t jenkins_one_at_a_time_hash(char *key, size_t len)
{
    uint32_t hash, i;
    for(hash = i = 0; i < len; ++i)
    {
        hash += key[i];
        hash += (hash << 10);
        hash ^= (hash >> 6);
    }
    hash += (hash << 3);
    hash ^= (hash >> 11);
    hash += (hash << 15);
    return hash;
}

8

Esistono numerose implementazioni di tabelle hash esistenti per C, dalla libreria standard C hcreate / hdestroy / hsearch, a quelle in APR e glib , che forniscono anche funzioni hash predefinite. Consiglio vivamente di usarli piuttosto che inventare la tua tabella hash o funzione hash; sono stati fortemente ottimizzati per i casi d'uso comuni.

Se il set di dati è statico, tuttavia, la soluzione migliore è probabilmente quella di utilizzare un hash perfetto . gperf genererà un hash perfetto per te per un dato set di dati.


hsearch cerca confrontando le stringhe o la stringa ptr address? Penso che stia solo controllando l'indirizzo ptr? Ho provato a usare diversi puntatori ma la stessa stringa calue. hsearch non riesce affermando che non sono stati trovati elementi
mk ..

3

djb2 ​​ha 317 collisioni per questo dizionario inglese 466k mentre MurmurHash non ne ha per hash a 64 bit e 21 per hash a 32 bit (ci si aspetta circa 25 per 466k hash casuali a 32 bit). Il mio consiglio è usare MurmurHash se disponibile, è molto veloce, perché richiede diversi byte alla volta. Ma se hai bisogno di una funzione hash semplice e breve da copiare e incollare nel tuo progetto, ti consiglio di usare la versione mormora un byte alla volta:

uint32_t inline MurmurOAAT32 ( const char * key)
{
  uint32_t h(3323198485ul);
  for (;*key;++key) {
    h ^= *key;
    h *= 0x5bd1e995;
    h ^= h >> 15;
  }
  return h;
}

uint64_t inline MurmurOAAT64 ( const char * key)
{
  uint64_t h(525201411107845655ull);
  for (;*key;++key) {
    h ^= *key;
    h *= 0x5bd1e9955bd1e995;
    h ^= h >> 47;
  }
  return h;
}

La dimensione ottimale di una tabella hash è, in breve, la più grande possibile pur rimanendo inserita nella memoria. Poiché di solito non sappiamo o non vogliamo cercare la quantità di memoria che abbiamo a disposizione, e potrebbe persino cambiare, la dimensione ottimale della tabella hash è circa il doppio del numero previsto di elementi da memorizzare nella tabella. Allocare molto di più renderà la tua tabella hash più veloce ma con rendimenti in rapida diminuzione, rendendo la tua tabella hash più piccola di così sarà esponenzialmente più lenta. Questo perché esiste un compromesso non lineare tra complessità spazio e tempo per le tabelle hash, con un fattore di carico ottimale di 2-sqrt (2) = 0,58 ... apparentemente.


2

Primo, 40 collisioni per 130 parole con hash a 0..99 sono cattive? Non puoi aspettarti un hashing perfetto se non stai adottando misure specifiche affinché avvenga. Una normale funzione hash non avrà meno collisioni di un generatore casuale per la maggior parte del tempo.

Una funzione hash con una buona reputazione è MurmurHash3 .

Infine, per quanto riguarda la dimensione della tabella hash, dipende davvero dal tipo di tabella hash che hai in mente, in particolare, se i bucket sono estensibili o uno slot. Se i bucket sono estensibili, ancora una volta c'è una scelta: scegli la lunghezza media del bucket per i vincoli di memoria / velocità che hai.


1
Il numero previsto di collisioni hash è n - m * (1 - ((m-1)/m)^n) = 57.075.... 40 collisioni sono migliori di quanto ci si potrebbe aspettare per caso (da 46 a 70 con un punteggio p di 0,999). La funzione hash in questione è più uniforme rispetto a se fosse casuale o stiamo assistendo a un evento molto raro.
Wolfgang Brehm il

2

Sebbene djb2, come presentato su stackoverflow da cnicutar , sia quasi certamente migliore, penso che valga la pena mostrare anche gli hash K&R :

1) Apparentemente un terribile algoritmo di hash, come presentato nella prima edizione di K&R ( fonte )

unsigned long hash(unsigned char *str)
{
    unsigned int hash = 0;
    int c;

    while (c = *str++)
        hash += c;

    return hash;
}

2) Probabilmente un algoritmo hash abbastanza decente, come presentato nella versione 2 di K&R (verificato da me a pag. 144 del libro); NB: assicurati di rimuovere % HASHSIZEdall'istruzione return se prevedi di eseguire il dimensionamento del modulo in base alla lunghezza del tuo array al di fuori dell'algoritmo hash. Inoltre, ti consiglio di fare il tipo return e "hashval" unsigned longinvece del semplice unsigned(int).

unsigned hash(char *s)
{
    unsigned hashval;

    for (hashval = 0; *s != '\0'; s++)
        hashval = *s + 31*hashval;
    return hashval % HASHSIZE;
}

Si noti che è chiaro dai due algoritmi che uno dei motivi per cui l'hash della prima edizione è così terribile è perché NON prende in considerazione l' ordine dei caratteri delle stringhe , hash("ab")quindi restituirebbe lo stesso valore di hash("ba"). Tuttavia, non è così con l'hash della seconda edizione, che restituirebbe (molto meglio!) Due valori diversi per quelle stringhe.

Le funzioni hash di GCC C ++ 11 utilizzate per unordered_map(un modello di tabella hash) e unordered_set(un modello di set di hash) sembrano essere le seguenti.

Codice:

// Implementation of Murmur hash for 32-bit size_t.
size_t _Hash_bytes(const void* ptr, size_t len, size_t seed)
{
  const size_t m = 0x5bd1e995;
  size_t hash = seed ^ len;
  const char* buf = static_cast<const char*>(ptr);

  // Mix 4 bytes at a time into the hash.
  while (len >= 4)
  {
    size_t k = unaligned_load(buf);
    k *= m;
    k ^= k >> 24;
    k *= m;
    hash *= m;
    hash ^= k;
    buf += 4;
    len -= 4;
  }

  // Handle the last few bytes of the input array.
  switch (len)
  {
    case 3:
      hash ^= static_cast<unsigned char>(buf[2]) << 16;
      [[gnu::fallthrough]];
    case 2:
      hash ^= static_cast<unsigned char>(buf[1]) << 8;
      [[gnu::fallthrough]];
    case 1:
      hash ^= static_cast<unsigned char>(buf[0]);
      hash *= m;
  };

  // Do a few final mixes of the hash.
  hash ^= hash >> 13;
  hash *= m;
  hash ^= hash >> 15;
  return hash;
}

2

Ho provato queste funzioni hash e ho ottenuto il seguente risultato. Ho circa 960 ^ 3 voci, ciascuna lunga 64 byte, 64 caratteri in ordine diverso, valore hash 32 bit. Codici da qui .

Hash function    | collision rate | how many minutes to finish
==============================================================
MurmurHash3      |           6.?% |                      4m15s
Jenkins One..    |           6.1% |                      6m54s   
Bob, 1st in link |          6.16% |                      5m34s
SuperFastHash    |            10% |                      4m58s
bernstein        |            20% |       14s only finish 1/20
one_at_a_time    |          6.16% |                       7m5s
crc              |          6.16% |                      7m56s

Una cosa strana è che quasi tutte le funzioni hash hanno un tasso di collisione del 6% per i miei dati.


Sebbene questo collegamento possa rispondere alla domanda, è meglio includere le parti essenziali della risposta qui e fornire il collegamento come riferimento. Le risposte di solo collegamento possono diventare non valide se la pagina collegata cambia.
thewaywere

Votato per un buon tavolo, inserire il codice sorgente per ciascuno di quegli hash nella tua risposta è essenziale. In caso contrario, i collegamenti potrebbero interrompersi e siamo sfortunati.
Gabriel Staples l'

Il numero previsto di collisioni dovrebbe essere 9.112499989700318E + 7 o 0.103 * 960³ se gli hash fossero veramente casuali, quindi non sarei sorpreso se fossero tutti intorno a quel valore, ma 0.0616 * 960³ sembra un po 'fuori, quasi come se il Gli hash sono distribuiti in modo più uniforme di quanto ci si aspetterebbe per caso e con una lunghezza di 64 byte questo limite dovrebbe essere sicuramente raggiunto. Puoi condividere il set di stringhe che hai sottoposto a hashing in modo che io possa provare a riprodurlo?
Wolfgang Brehm,

0

Una cosa che ho usato con buoni risultati è la seguente (non so se sia già menzionato perché non ricordo il suo nome).

Precalcuti una tabella T con un numero casuale per ogni carattere dell'alfabeto della tua chiave [0,255]. Puoi eseguire il hash della tua chiave 'k0 k1 k2 ... kN' prendendo T [k0] xor T [k1] xor ... xor T [kN]. Puoi facilmente dimostrare che questo è casuale come il tuo generatore di numeri casuali ed è computazionalmente molto fattibile e se ti imbatti davvero in un'istanza pessima con molte collisioni puoi semplicemente ripetere l'intera cosa usando un nuovo lotto di numeri casuali.


Se non sbaglio, questo soffre dello stesso problema di K&R 1st nella risposta di Gabriel; cioè "ab" e "ba" avranno lo stesso valore.
Johann Oskarsson
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.