fungsi hash untuk string

124

Saya sedang mengerjakan tabel hash dalam bahasa C dan saya sedang menguji fungsi hash untuk string.

Fungsi pertama yang saya coba adalah menambahkan kode ascii dan menggunakan modulo (% 100) tetapi saya mendapatkan hasil yang buruk dengan tes pertama data: 40 tabrakan untuk 130 kata.

Data masukan akhir akan berisi 8 000 kata (ini adalah kamus yang disimpan dalam sebuah file). Tabel hash dideklarasikan sebagai tabel int [10000] dan berisi posisi kata dalam file txt.

Pertanyaan pertama adalah algoritma mana yang terbaik untuk hashing string? dan bagaimana cara menentukan ukuran tabel hash?

Terima kasih sebelumnya !

:-)

lilawood.dll
sumber
11
Jika tabel hash Anda memiliki 10K entri, mengapa Anda menggunakan modulo 100? Mendapatkan 40 tabrakan dari 130 kata tidak mengherankan dengan modulus sekecil itu.
Carey Gregory
13
Lihat burtleburtle.net/bob/hash/evahash.html dan partow.net/programming/hashfunctions yang merupakan sumber daya tentang berbagai hashing (dari umum ke string hingga kripto).
3
Untuk memperjelas @CareyGregory: Anda benar-benar menyadari bahwa, sebagai kebenaran matematika dasar, 130 item dalam 100 keranjang (yaitu, mod 100) harus menghasilkan 30 tabrakan (di mana tabrakan dihitung setiap kali item kedua, ketiga, dll. Dimasukkan ke dalam ember), benar? Jadi Anda hanya sedikit di atas itu.
derobert
4
@lilawood: Oke, itulah yang saya pikirkan, tetapi untuk menjadi tes yang lebih baik Anda harus menggunakan 80 kata dengan tabel hash 100 entri. Itu akan memberi Anda proporsi yang sama dengan data langsung Anda dan tidak akan memaksa tabrakan.
Carey Gregory
4
Kemungkinan duplikat dari Fungsi Hash yang Baik untuk String
MJ Rayburn

Jawaban:

185

Saya mendapatkan hasil yang bagus dengan 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;
}
cnicutar
sumber
37
halaman yang ditautkan dalam jawabannya sangat menarik.
Adrien Plisson
2
bagaimana program kehabisan while loop ?? = S
Daniel N.
1
@ danfly09 Jika c adalah nol. Setara dengan while (c = * str ++) akan menjadi (0! = (C = * str ++))
rxantos
5
@Josepas, fungsi hash idealnya mengembalikan size_tatau nilai unsigned lainnya (seperti unsigned long dalam kode ini). The pemanggil bertanggung jawab untuk mengambil modulo hasilnya untuk menyesuaikan dengan tabel hash. Pemanggil mengontrol slot tabel yang sedang di-hash; bukan fungsinya. Itu hanya mengembalikan beberapa nomor yang tidak ditandatangani.
WhozCraig
6
luar biasa. algoritma ini mengalahkan hash Murmur, hash varian FNV dan banyak lainnya! +1
David Haim
24

Pertama, Anda biasanya tidak ingin menggunakan hash kriptografi untuk tabel hash. Algoritme yang sangat cepat menurut standar kriptografi masih sangat lambat menurut standar tabel hash.

Kedua, Anda ingin memastikan bahwa setiap bit masukan dapat / akan mempengaruhi hasilnya. Salah satu cara mudah untuk melakukannya adalah dengan memutar hasil saat ini dengan beberapa bit, kemudian XOR kode hash saat ini dengan byte saat ini. Ulangi sampai Anda mencapai ujung benang. Perhatikan bahwa Anda biasanya tidak ingin rotasi menjadi kelipatan genap dari ukuran byte juga.

Misalnya, dengan asumsi kasus umum 8 bit byte, Anda mungkin memutar 5 bit:

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

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

Sunting: Perhatikan juga bahwa 10.000 slot jarang merupakan pilihan yang baik untuk ukuran tabel hash. Anda biasanya menginginkan salah satu dari dua hal: Anda ingin bilangan prima sebagai ukuran (diperlukan untuk memastikan kebenaran dengan beberapa jenis resolusi hash) atau pangkat 2 (sehingga mengurangi nilai ke kisaran yang benar dapat dilakukan dengan sederhana bit-mask).

Jerry Coffin
sumber
Ini bukan c, tetapi saya akan tertarik dengan pemikiran Anda untuk jawaban terkait ini: stackoverflow.com/a/31440118/3681880
Suragch
1
@Suragch: Sejak saya menulis ini, beberapa prosesor sudah mulai menyertakan perangkat keras khusus untuk mempercepat komputasi SHA, yang membuatnya jauh lebih kompetitif. Yang mengatakan, saya ragu kode Anda cukup aman seperti yang Anda pikirkan - misalnya, angka floating point IEEE memiliki dua pola bit berbeda (0 dan -0) yang seharusnya menghasilkan hash yang sama (mereka akan membandingkan sama satu sama lain ).
Jerry Coffin
@ Jerry Coffin perpustakaan mana yang saya perlukan untuk fungsi rol ()?
thanos.a
@ thanos.a: Saya tidak menyadarinya berada di perpustakaan, tetapi menggulung milik Anda hanya membutuhkan satu atau dua baris kode. Geser satu bagian ke kiri, bagian lainnya ke kanan, dan atau keduanya bersamaan.
Jerry Coffin
8

Wikipedia menunjukkan fungsi hash string yang bagus yang disebut Jenkins One At A Time Hash. Itu juga mengutip versi perbaikan dari hash ini.

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;
}
RushPL
sumber
8

Ada sejumlah implementasi hashtable yang ada untuk C, mulai dari library standar C hcreate / hdestroy / hsearch, hingga yang ada di APR dan glib , yang juga menyediakan fungsi hash bawaan. Saya sangat merekomendasikan untuk menggunakannya daripada menciptakan fungsi hashtable atau hash Anda sendiri; mereka telah sangat dioptimalkan untuk kasus penggunaan umum.

Namun, jika kumpulan data Anda statis, solusi terbaik Anda mungkin menggunakan hash yang sempurna . gperf akan menghasilkan hash yang sempurna untuk Anda untuk kumpulan data tertentu.

Nick Johnson
sumber
hsearch mencari dengan membandingkan string atau string ptr address? Saya pikir itu hanya memeriksa alamat ptr? Saya mencoba menggunakan petunjuk yang berbeda tetapi nilai string yang sama. hsearch gagal menyatakan tidak ada elemen yang ditemukan
mk ..
3

djb2 ​​memiliki 317 benturan untuk kamus bahasa Inggris 466k ini sementara MurmurHash tidak memiliki satupun untuk hash 64 bit, dan 21 untuk hash 32 bit (sekitar 25 diharapkan untuk hash 466k acak 32 bit). Rekomendasi saya adalah menggunakan MurmurHash jika tersedia, ini sangat cepat, karena membutuhkan beberapa byte sekaligus. Tetapi jika Anda memerlukan fungsi hash yang sederhana dan singkat untuk menyalin dan menempel ke proyek Anda, saya akan merekomendasikan menggunakan versi murmur satu byte-at-a-time:

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;
}

Ukuran optimal dari tabel hash adalah - singkatnya - sebesar mungkin sambil tetap masuk ke dalam memori. Karena kita biasanya tidak tahu atau ingin mencari berapa banyak memori yang kita miliki, dan bahkan mungkin berubah, ukuran tabel hash yang optimal kira-kira 2x jumlah elemen yang diharapkan untuk disimpan dalam tabel. Mengalokasikan lebih dari itu akan membuat tabel hash Anda lebih cepat tetapi dengan pengembalian yang berkurang dengan cepat, membuat tabel hash Anda lebih kecil dari itu akan membuatnya lebih lambat secara eksponensial. Ini karena ada trade-off non-linear antara kompleksitas ruang dan waktu untuk tabel hash, dengan faktor beban optimal 2-sqrt (2) = 0,58 ... tampaknya.

Wolfgang Brehm
sumber
2

Pertama, apakah 40 tabrakan untuk 130 kata di-hash ke 0..99 buruk? Anda tidak dapat mengharapkan hashing yang sempurna jika Anda tidak mengambil langkah-langkah khusus untuk mewujudkannya. Fungsi hash biasa tidak akan memiliki tabrakan yang lebih sedikit daripada generator acak di sebagian besar waktu.

Fungsi hash dengan reputasi yang baik adalah MurmurHash3 .

Terakhir, mengenai ukuran tabel hash, itu sangat tergantung pada jenis tabel hash yang Anda pikirkan, terutama, apakah bucket dapat diperpanjang atau satu slot. Jika bucket dapat diperpanjang, sekali lagi ada pilihan: Anda memilih panjang bucket rata-rata untuk batasan memori / kecepatan yang Anda miliki.

Pascal Cuoq
sumber
1
Jumlah tabrakan hash yang diharapkan adalah n - m * (1 - ((m-1)/m)^n) = 57.075.... 40 tabrakan lebih baik daripada yang diharapkan secara kebetulan (46 hingga 70 dengan p-score 0,999). Fungsi hash yang dimaksud lebih seragam daripada jika acak atau kita menyaksikan peristiwa yang sangat langka.
Wolfgang Brehm
2

Meskipun djb2, seperti yang disajikan di stackoverflow oleh cnicutar , hampir pasti lebih baik, saya rasa ada baiknya juga menampilkan hash K&R :

1) Ternyata algoritma hash yang buruk , seperti yang disajikan dalam K&R 1st edition ( sumber )

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

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

    return hash;
}

2) Mungkin algoritma hash yang lumayan bagus, seperti yang disajikan dalam K&R versi 2 (diverifikasi oleh saya di halaman 144 buku); NB: pastikan untuk menghapus % HASHSIZEdari pernyataan return jika Anda berencana melakukan modulus sizing-to-your-array-length di luar algoritma hash. Juga, saya sarankan Anda membuat tipe return dan "hashval" unsigned longdaripada yang simple unsigned(int).

unsigned hash(char *s)
{
    unsigned hashval;

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

Perhatikan bahwa jelas dari kedua algoritme bahwa salah satu alasan hash edisi pertama sangat buruk adalah karena TIDAK mempertimbangkan urutan karakter string , sehingga hash("ab")akan mengembalikan nilai yang sama seperti hash("ba"). Ini tidak demikian dengan hash edisi ke-2, yang (jauh lebih baik!) Mengembalikan dua nilai berbeda untuk string tersebut.

Fungsi hashing GCC C ++ 11 yang digunakan untuk unordered_map(template tabel hash) dan unordered_set(template kumpulan hash) tampak seperti berikut.

Kode:

// 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;
}
Gabriel Staples
sumber
2

Saya telah mencoba fungsi hash ini dan mendapatkan hasil sebagai berikut. Saya memiliki sekitar 960 ^ 3 entri, masing-masing sepanjang 64 byte, 64 karakter dalam urutan berbeda, nilai hash 32bit. Kode dari sini .

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

Satu hal yang aneh adalah bahwa hampir semua fungsi hash memiliki tingkat tabrakan 6% untuk data saya.

Xiaoning Bian
sumber
Meskipun tautan ini mungkin menjawab pertanyaan, lebih baik menyertakan bagian penting dari jawaban di sini dan menyediakan tautan untuk referensi. Jawaban link saja bisa menjadi tidak valid jika halaman tertaut berubah.
thewaywewere
Suara positif untuk tabel yang bagus, menempatkan kode sumber untuk setiap hash di jawaban Anda juga penting. Jika tidak, tautan dapat rusak dan kami kurang beruntung.
Gabriel Staples
Jumlah tabrakan yang diharapkan adalah 9.112499989700318E + 7 atau 0.103 * 960³ jika hashnya benar-benar acak, jadi saya tidak akan terkejut jika semuanya ada di sekitar nilai itu, tetapi 0.0616 * 960³ tampaknya sedikit meleset, hampir seolah-olah hash didistribusikan lebih merata daripada yang diharapkan secara kebetulan, dan pada panjang 64 byte, batas ini harus didekati. Dapatkah Anda membagikan kumpulan string yang Anda hash sehingga saya dapat mencoba mereproduksinya?
Wolfgang Brehm
0

Satu hal yang saya gunakan dengan hasil yang baik adalah yang berikut ini (saya tidak tahu apakah sudah disebutkan karena saya tidak ingat namanya).

Anda menghitung sebelumnya T tabel dengan nomor acak untuk setiap karakter dalam alfabet kunci Anda [0,255]. Anda mencirikan kunci Anda 'k0 k1 k2 ... kN' dengan mengambil T [k0] xor T [k1] xor ... xor T [kN]. Anda dapat dengan mudah menunjukkan bahwa ini sama acaknya dengan generator bilangan acak Anda dan secara komputasi sangat layak dan jika Anda benar-benar mengalami kejadian yang sangat buruk dengan banyak tabrakan, Anda dapat mengulangi semuanya menggunakan kumpulan bilangan acak baru.

Michael Nett
sumber
Jika saya tidak salah ini menderita masalah yang sama seperti K&R 1 dalam jawaban Gabriel; yaitu "ab" dan "ba" akan di-hash ke nilai yang sama.
Johann Oskarsson