文字列のハッシュ関数


124

私はC言語でハッシュテーブルに取り組んでおり、文字列のハッシュ関数をテストしています。

私が試した最初の機能は、ASCIIコードを追加してモジュロ(%100)を使用することですが、データの最初のテストでは結果が良くありません。130ワードで40衝突です。

最終的な入力データには8 000ワードが含まれます(ファイル内の辞書ストアです)。ハッシュテーブルはint table [10000]として宣言され、txtファイル内の単語の位置を含みます。

最初の質問は、文字列のハッシュに最適なアルゴリズムはどれですか?そして、ハッシュテーブルのサイズを決定する方法は?

前もって感謝します !

:-)


11
ハッシュテーブルに10Kのエントリがある場合、なぜ100を法とするのですか?130の単語から40の衝突を取得することは、このような小さい係数で驚くことではありません。
Carey Gregory

13
さまざまなハッシュに関するリソース(一般から文字列、暗号まで)については、burtleburtle.net / bob / hash / evahash.htmlpartow.net/programming/hashfunctionsを参照してください。

3
@CareyGregoryを明確にするため:基本的な数学の真実として、100バケット(つまり、mod 100)の130アイテムは30の衝突を生成する必要があることに気づきます(衝突は、1秒、3番目などのアイテムが配置されるたびにカウントされます)。バケツ)、正しい?だから、あなたはその少し上です。
derobert、2011年

4
@lilawood:わかりました、それは私が考えたものですが、より良いテストのためには、100エントリのハッシュテーブルで80ワードを使用する必要があります。これにより、ライブデータと同じ比率が得られ、衝突が強制されることはありません。
Carey Gregory

回答:


185

djb2ダン・バーンスタインによる素晴らしい結果がありました。

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
回答にリンクされているページは非常に興味深いです。
Adrien Plisson、2011年

2
プログラムがどのようにwhileループを使い果たしますか?= S
Daniel N.

1
@ danfly09 cがゼロのとき。while(c = * str ++)に相当するものは(0!=(c = * str ++))
rxantos

5
@Josepasハッシュ関数は、理想的にはsize_t、このような符号なしの値(このコードではunsigned longなど)を返す必要があります。呼び出し側は、ハッシュテーブルにそれを合わせて、結果の剰余を取るための責任があります。呼び出し元は、ハッシュされるテーブルスロットを制御します。関数ではありません。いくつかの符号なしの数値を返すだけです。
WhozCraig

6
すごい。このアルゴリズムは、Murmurハッシュ、FNVバリアントハッシュ、およびその他の多くのハッシュを完全に打ち負かしました!+1
デビッドハイム2016

24

まず、一般に、ハッシュテーブルに暗号化ハッシュを使用すること望ましくありません。暗号規格で非常に高速なアルゴリズムは、ハッシュテーブル規格では依然として非常に低速です。

次に、入力のすべてのビットが結果に影響を与える可能性があるか、または影響を与えるようにする必要があります。これを行う簡単な方法の1つは、現在の結果をいくつかのビット数だけローテーションしてから、現在のハッシュコードと現在のバイトをXORすることです。文字列の最後に到達するまで繰り返します。通常、ローテーションをバイトサイズの偶数倍にすることも望まないことに注意してください。

たとえば、8ビットバイトの一般的なケースを想定すると、5ビットずつローテーションできます。

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

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

編集:また、10000スロットがハッシュテーブルサイズに適していることはめったにありません。通常、次の2つのいずれかが必要です。サイズとして素数が必要(一部のタイプのハッシュ解決で正確性を確保するために必要)または2の累乗が必要です(そのため、値を正しい範囲に減らすには、単純なビットマスク)。


これは、Cではありませんが、私はこの関連の答えに自分の考えに興味がある:stackoverflow.com/a/31440118/3681880
Suragch

1
@Suragch:私がこれを書いてから、かなりの数のプロセッサがSHA計算を高速化するためにいずれかの特別なハードウェアを搭載し始めており、それによりはるかに競争力が高まっています。そうは言っても、あなたのコードがあなたが思うほど安全であるとは思えません-たとえば、IEEE浮動小数点数には、同じハッシュを生成する2つの異なるビットパターン(0と-0)があります(それらは互いに等しいと比較されます) )。
Jerry Coffin

@Jerry Coffin rol()関数にはどのライブラリが必要ですか?
thanos.a

@ thanos.a:私はそれがライブラリにあることに気づいていませんが、あなた自身のロールは1行か2行のコードしかかかりません。1つのチャンクを左に、他のチャンクを右に、または一緒にシフトします。
Jerry Coffin

8

ウィキペディアには、Jenkins One At A Time 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

Cの既存のハッシュテーブル実装は、C標準ライブラリhcreate / hdestroy / hsearchから、APRおよびglibにあるものまであり、これらも事前に作成されたハッシュ関数を提供します。独自のハッシュテーブルやハッシュ関数を発明するのではなく、それらを使用することを強くお勧めします。それらは一般的なユースケースのために大幅に最適化されています。

ただし、データセットが静的な場合は、おそらく完全なハッシュを使用するのが最善の解決策です。gperfは、指定されたデータセットに対して完全なハッシュを生成します。


hsearchは、文字列または文字列のptrアドレスを比較して検索しますか?私は単にptrアドレスをチェックしていると思いますか?異なるポインターを使用してみましたが、文字列の計算は同じです。hsearch見つかりませ要素を記述していない障害が発生した
MKを..

3

djb2はこの466k英語辞書に対して317回の衝突を起こしていますが、MurmurHashは64ビットハッシュに対しては衝突がなく、32ビットハッシュに対しては21回(466kランダム32ビットハッシュに対しては約25回の予想)です。可能な場合はMurmurHashを使用することをお勧めします。一度に数バイトかかるため、非常に高速です。ただし、プロジェクトにコピーアンドペーストするためのシンプルで短いハッシュ関数が必要な場合は、1バイトずつバージョンをつぶやくバージョンを使用することをお勧めします。

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

ハッシュテーブルの最適なサイズは、メモリに収まる限り、できるだけ大きくなります。通常、使用可能なメモリの量がわからない、または調べたいと思っていないため、変化する可能性さえあるので、最適なハッシュテーブルのサイズは、テーブルに格納されると予想される要素数の約2倍です。それよりも多く割り当てると、ハッシュテーブルは高速になりますが、リターンが急速に減少し、ハッシュテーブルをそれよりも小さくすると、指数関数的に遅くなります。これは、ハッシュテーブルの空間と時間の複雑性の間に非線形のトレードオフがあり、最適な負荷係数が2-sqrt(2)= 0.58 ...であるためです。


2

最初に、130ワードの40回の衝突は0..99にハッシュされますか?完全なハッシュが発生するための特別な手順を実行していない場合、ハッシュは期待できません。通常のハッシュ関数は、ほとんどの場合、ランダムジェネレーターよりも衝突が少なくありません。

評判の良いハッシュ関数はMurmurHash3です。

最後に、ハッシュテーブルのサイズに関しては、どのようなハッシュテーブルを考えているか、特にバケットが拡張可能か1スロットかは実際に異なります。バケットが拡張可能である場合も、選択肢があります。メモリ/速度の制約に対して、バケットの長さの平均を選択します。


1
予想されるハッシュ衝突の数はn - m * (1 - ((m-1)/m)^n) = 57.075...です。40回の衝突は、偶然に予想されるものよりも優れています(pスコア0.999で46〜70回)。問題のハッシュ関数は、ランダムである場合や、非常にまれなイベントが発生している場合よりも均一です。
Wolfgang Brehm

2

しかしdjb2、としてcnicutarでStackOverflowの上で提示、ほぼ確実に優れている、私はそれの価値が示す考えるK&Rあまりにもハッシュを:

1)K&R第1版(ソース)に示されているように、恐ろしいハッシュアルゴリズム

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

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

    return hash;
}

2)K&Rバージョン2で提示されているように、かなりまともなハッシュアルゴリズム(本のpg。144で私が確認)。注意:% HASHSIZEハッシュアルゴリズムの外でアレイの長さに合わせた係数のサイズ設定を行う場合は、必ずreturnステートメントから削除してください。また、unsigned long単純なunsigned(int)ではなく、戻り値と "hashval"タイプを作成することをお勧めします。

unsigned hash(char *s)
{
    unsigned hashval;

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

2つのアルゴリズムから明らかなように、第1版のハッシュがひどいのは、文字列の文字の順序が考慮されていないためhash("ab")と同じ値が返されるためですhash("ba")。しかし、これは第2版のハッシュではそうではありません。(はるかに良い!)これらの文字列に対して2つの異なる値を返します。

unordered_map(ハッシュテーブルテンプレート)とunordered_set(ハッシュセットテンプレート)に使用されるGCC C ++ 11ハッシュ関数は次のように見えます。

コード:

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

私はこれらのハッシュ関数を試して、次の結果を得ました。約960 ^ 3のエントリがあり、それぞれ64バイトの長さ、64文字の順序で、ハッシュ値は32ビットです。ここからのコード。

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

奇妙なことに、ほとんどすべてのハッシュ関数は私のデータに対して6%の衝突率を持っています。


このリンクで質問に答えることができますが、回答の重要な部分をここに含め、参照用のリンクを提供することをお勧めします。リンクされたページが変更されると、リンクのみの回答が無効になる可能性があります。
thewaywereは

良い表に賛成して、それらのハッシュのそれぞれのソースコードをあなたの答えに投稿することも不可欠です。そうしないと、リンクが壊れて、運が悪くなります。
ガブリエルステープルズ

予想される衝突の数は9.112499989700318E + 7または0.103 * 960 beであり、ハッシュが本当にランダムである場合、その値の周りにすべてがあったとしても驚くことはありませんが、0.0616 *960³は少しずれているように見えます。ハッシュは、偶然に予想されるよりも均等に分散され、64バイトの長さでは、この制限に確実に近づく必要があります。ハッシュした文字列のセットを共有して、再現できるかどうか教えてください。
Wolfgang Brehm

0

良い結果を得るために使用したことの1つは次のとおりです(その名前を思い出せないため、既に言及されているかどうかはわかりません)。

鍵のアルファベット[0,255]の各文字の乱数を使ってテーブルTを事前計算します。T [k0] xor T [k1] xor ... xor T [kN]を使用して、キー 'k0 k1 k2 ... kN'をハッシュします。これが乱数ジェネレータと同じくらいランダムであり、計算上非常に実行可能であることを簡単に示すことができます。衝突が多く非常に悪いインスタンスに実際に遭遇した場合は、乱数の新しいバッチを使用して全体を繰り返すことができます。


私が誤解していないのであれば、これはガブリエルの答えの1番目のK&Rと同じ問題に苦しんでいます。つまり、「ab」と「ba」は同じ値にハッシュされます。
ヨハンオスカーソン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.