dize için karma işlevi


124

C dilinde hash tablosu üzerinde çalışıyorum ve string için hash fonksiyonunu test ediyorum.

Denediğim ilk işlev ascii kodu eklemek ve modulo (% 100) kullanmaktı ancak ilk veri testinde kötü sonuçlar aldım: 130 kelime için 40 çarpışma.

Son giriş verisi 8000 kelime içerecektir (bir dosyada saklanan bir sözlüktür). Karma tablo int tablo [10000] olarak bildirilir ve kelimenin bir txt dosyasındaki konumunu içerir.

İlk soru, dizeyi hashing için en iyi algoritma hangisidir? ve hash tablosunun boyutu nasıl belirlenir?

şimdiden teşekkürler !

:-)


11
Karma tablonuzda 10K giriş varsa, neden modulo 100 kullanasınız? 130 kelimeden 40 çarpışma elde etmek bu kadar küçük bir modülle şaşırtıcı değil.
Carey Gregory


4
@CareyGregory'yi açıklığa kavuşturmak için: Temel bir matematiksel gerçek olarak, 100 kovadaki 130 öğenin (yani mod 100) 30 çarpışma üretmesi gerektiğinin farkındasınız (burada, bir ikinci, üçüncü, vb. Her öğe yerleştirildiğinde çarpışma sayılır) bir kova), doğru mu? Yani bunun sadece biraz üzerindesin.
derobert

4
@lilawood: Tamam, anladığım buydu, ama daha iyi bir test için 100 girişlik bir karma tablo ile 80 kelime kullanmalısınız. Bu size canlı verilerinizle aynı oranları verir ve çarpışmaları zorlamaz.
Carey Gregory

Yanıtlar:


186

djb2Dan Bernstein'dan güzel sonuçlar aldım .

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
cevapta bağlantısı verilen sayfa çok ilginç.
Adrien Plisson

2
program while döngüsü dışında nasıl çalışır? = S
Daniel N.

1
@ danfly09 c sıfır olduğunda. While (c = * str ++) 'nın eşdeğeri (0! = (C = * str ++))
rxantos

5
@Josepas hash işlevi ideal olarak bir size_tveya bu tür işaretsiz başka bir değer döndürmelidir (bu koddaki unsigned long gibi). Arayan karma tabloya uygun sonucun modulo alarak sorumludur. Arayan, hashing uygulanacak tablo aralığını kontrol eder; işlev değil. Sadece işaretsiz bir sayı döndürür.
WhozCraig

6
inanılmaz. bu algoritma, Murmur hash, FNV varyantları hashleri ​​ve diğer pek çok şeyi cehenneme çevirdi! +1
David Haim

24

İlk olarak, genellikle do not karma tablo için şifreli karma kullanmak istiyorum. Kriptografik standartlara göre çok hızlı olan bir algoritma, karma tablo standartlarına göre hala dayanılmaz derecede yavaştır.

İkinci olarak, girdinin her bitinin sonucu etkileyebileceğinden / etkileyeceğinden emin olmak istersiniz. Bunu yapmanın kolay bir yolu, geçerli sonucu birkaç bit döndürmek, ardından geçerli karma kodunu geçerli bayt ile XOR yapmaktır. İpin sonuna ulaşıncaya kadar tekrarlayın. Genellikle döndürmenin bayt boyutunun çift katı olmasını da istemediğinizi unutmayın .

Örneğin, 8 bit baytlık yaygın bir durumu varsayarsak, 5 bit döndürebilirsiniz:

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

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

Düzenleme: Ayrıca 10000 yuvanın bir karma tablo boyutu için nadiren iyi bir seçim olduğunu unutmayın. Genellikle iki şeyden birini istersiniz: boyut olarak bir asal sayı (bazı hash çözünürlük türlerinde doğruluğu sağlamak için gereklidir) veya 2'nin üssü (bu nedenle değeri doğru aralığa düşürmek basit bir şekilde yapılabilir. bit-maskesi).


Bu c değil, ancak bu ilgili cevapla ilgili düşüncelerinizle ilgilenebilirim: stackoverflow.com/a/31440118/3681880
Suragch

1
@Suragch: Bunu yazdığımdan beri, çok az sayıda işlemci SHA hesaplamasını hızlandırmak için özel donanımlardan birini kullanmaya başladı, bu da onu çok daha rekabetçi hale getirdi. Bununla birlikte, kodunuzun düşündüğünüz kadar güvenli olduğundan şüpheliyim - örneğin, IEEE kayan noktalı sayıların aynı hash'leri üretmesi gereken iki farklı bit modeli (0 ve -0) vardır (birbirlerine eşit olarak karşılaştırırlar. ).
Jerry Coffin

@Jerry Coffin rol () işlevi için hangi kitaplığa ihtiyacım var?
thanos.a

@ thanos.a: Bir kitaplıkta olduğunun farkında değilim, ancak kendi başlığınızı yuvarlamak yalnızca bir veya iki satır kod gerektirir. Bir parçayı sola, diğer parçayı sağa ve veya bunları birlikte kaydırın.
Jerry Tabut

8

Wikipedia, Jenkins One At A Time Hash adlı güzel bir string hash işlevi gösterir . Ayrıca, bu hash'in geliştirilmiş sürümlerinden alıntı yapar.

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

C standart kitaplığından hcreate / hdestroy / hsearch'ten APR ve glib içindekilere kadar , önceden oluşturulmuş hash işlevleri de sağlayan C için bir dizi hashtable uygulaması vardır . Kendi hashtable veya hash fonksiyonunuzu icat etmektense bunları kullanmanızı şiddetle tavsiye ederim; yaygın kullanım durumları için yoğun şekilde optimize edilmişlerdir.

Bununla birlikte, veri kümeniz statikse, en iyi çözüm muhtemelen mükemmel bir karma kullanmaktır . gperf , belirli bir veri kümesi için sizin için mükemmel bir karma oluşturur.


hsearch, dizeleri veya dize ptr adresini karşılaştırarak arar mı? Sanırım sadece ptr adresini kontrol ediyor? Farklı işaretçiler kullanmayı denedim ama aynı dize hesabı. hsearch hiçbir öğe bulunamadı
mk ..

3

djb2 bu 466k ingilizce sözlük için 317 çarpışmaya sahipken, MurmurHash 64 bit karma için hiçbirine ve 32 bit karma için 21 çarpışmaya sahip (466k rasgele 32 bit karmalar için yaklaşık 25 bekleniyor). Benim tavsiyem eğer varsa MurmurHash kullanmaktır , çok hızlıdır çünkü bir seferde birkaç bayt alır. Ancak, projenize kopyalayıp yapıştırmak için basit ve kısa bir hash işlevine ihtiyacınız varsa, bir seferde bir baytlık üfürüm kullanmanızı öneririm:

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

Bir karma tablonun optimum boyutu - kısacası - belleğe sığarken mümkün olduğu kadar büyüktür. Genellikle ne kadar belleğimiz olduğunu bilmediğimizden veya bakmak istemediğimizden ve hatta değişebilir, optimum karma tablo boyutu, tabloda depolanacak beklenen öğe sayısının kabaca 2 katıdır. Bundan çok daha fazlasını tahsis etmek, karma tablonuzu daha hızlı hale getirecek, ancak hızla azalan getirilerle, karma tablonuzu bundan daha küçük yapmak, onu katlanarak daha yavaş hale getirecektir. Bunun nedeni, hash tabloları için uzay ve zaman karmaşıklığı arasında doğrusal olmayan bir değiş tokuşun olmasıdır, görünüşe göre 2-sqrt (2) = 0.58 ... optimal yük faktörü ile.


2

Birincisi, 130 kelime için 40 çarpışma 0,99'a hash edilmiş mi? Bunun gerçekleşmesi için özel olarak adımlar atmıyorsanız, mükemmel bir hashing bekleyemezsiniz. Sıradan bir hash fonksiyonu, çoğu zaman rastgele bir jeneratörden daha az çarpışmaya sahip olmayacaktır.

İyi bir üne sahip bir hash işlevi MurmurHash3'tür .

Son olarak, karma tablonun boyutuyla ilgili olarak, gerçekten de aklınızda ne tür bir karma tablo olduğuna, özellikle de kovaların genişletilebilir mi yoksa tek yuvalı mı olduğuna bağlıdır. Kepçeler genişletilebilirse, yine bir seçenek vardır: Sahip olduğunuz bellek / hız kısıtlamaları için ortalama kepçe uzunluğunu seçersiniz.


1
Beklenen karma çarpışma sayısı n - m * (1 - ((m-1)/m)^n) = 57.075.... 40 çarpışma şans eseri beklenenden daha iyidir (0,999 p-skorunda 46'dan 70'e). Söz konusu hash işlevi, rastgele olduğundan daha tekdüzedir veya çok nadir bir olaya tanık oluyoruz.
Wolfgang Brehm

2

Gerçi djb2olarak, cnicutar tarafından stackoverflow sunulan , daha neredeyse kesin, ben gösteren 's değerinde düşünüyorum K & R da sağlamalarının:

1) Görünüşe göre korkunç bir hash algoritması, K&R 1. baskısında ( kaynak ) sunulduğu gibi

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

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

    return hash;
}

2) Muhtemelen oldukça iyi bir karma algoritma, K&R sürüm 2'de sunulduğu gibi (kitabın 144. sayfasında benim tarafımdan doğrulanmıştır); Not: % HASHSIZEKarma algoritmanın dışında dizi uzunluğunuza göre modül boyutlandırma yapmayı planlıyorsanız, return ifadesinden çıkardığınızdan emin olun . Ayrıca, unsigned longbasit unsigned(int) yerine dönüş ve "hashval" türünü yapmanızı öneririm .

unsigned hash(char *s)
{
    unsigned hashval;

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

İki algoritmadan, 1. sürüm hash'in bu kadar korkunç olmasının bir nedeninin, dize karakter sırasını dikkate ALMAMASI , hash("ab")dolayısıyla ile aynı değeri döndürmesidir hash("ba"). Ancak 2. baskı hash'inde durum böyle değildir , ki bu (çok daha iyi!) Bu dizeler için iki farklı değer döndürür.

unordered_map(Bir karma tablo şablonu) ve unordered_set(bir karma küme şablonu) için kullanılan GCC C ++ 11 karma işlevleri aşağıdaki gibi görünmektedir.

Kod:

// 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

Bu hash fonksiyonlarını denedim ve aşağıdaki sonucu aldım. Her biri 64 bayt uzunluğunda, farklı sırada 64 karakter, karma değeri 32 bit olan yaklaşık 960 ^ 3 girişim var. Buradan kodlar .

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

Garip bir şey, hemen hemen tüm hash fonksiyonlarının verilerim için% 6 çarpışma oranına sahip olmasıdır.


Bu bağlantı soruyu yanıtlasa da, cevabın temel kısımlarını buraya eklemek ve referans için bağlantıyı sağlamak daha iyidir. Bağlantılı sayfa değişirse yalnızca bağlantı yanıtları geçersiz hale gelebilir.
thewaywewere

İyi bir tablo için oy verildiğinde, cevabınızda bu karmaların her biri için kaynak kodunu yayınlamak da önemlidir. Aksi takdirde bağlantılar kopabilir ve şansımız kalmaz.
Gabriel Staples

Beklenen çarpışma sayısı 9.112499989700318E + 7 veya 0.103 * 960³ olmalıdır, eğer hashler gerçekten rastgele olsaydı, bu değerin etrafında olsalardı şaşırmazdım, ama 0.0616 * 960 a, neredeyse hash'ler şans eseri beklenenden daha eşit olarak dağıtılır ve 64 bayt uzunluğunda bu sınıra kesinlikle yaklaşılmalıdır. Karıştırdığınız dizeleri paylaşır mısınız, böylece onu yeniden üretmeyi deneyebilirim?
Wolfgang Brehm

0

İyi sonuçlarla kullandığım bir şey şudur (daha önce bahsedilip bahsedilmediğini bilmiyorum çünkü adını hatırlayamıyorum).

Anahtarınızın alfabesindeki [0,255] her karakter için rastgele bir sayı ile bir T tablosu önceden hesaplarsınız. T [k0] xor T [k1] xor ... xor T [kN] alarak 'k0 k1 k2 ... kN' anahtarınıza hashing uygulayabilirsiniz. Bunun rasgele sayı üreteciniz kadar rastgele olduğunu ve hesaplama açısından çok uygun olduğunu kolayca gösterebilirsiniz ve eğer gerçekten çok sayıda çarpışmanın olduğu çok kötü bir durumla karşılaşırsanız, yeni bir rasgele sayı grubu kullanarak her şeyi tekrarlayabilirsiniz.


Eğer yanılmıyorsam bu, Gabriel'in cevabındaki K&R 1 ile aynı sorundan muzdariptir; yani "ab" ve "ba" aynı değere karma oluşturacaktır.
Johann Oskarsson
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.