私はC言語でハッシュテーブルに取り組んでおり、文字列のハッシュ関数をテストしています。
私が試した最初の機能は、ASCIIコードを追加してモジュロ(%100)を使用することですが、データの最初のテストでは結果が良くありません:130ワードで40回の衝突。
最終的な入力データには8 000ワードが含まれます(ファイル内の辞書ストアです)。ハッシュテーブルはint table [10000]として宣言され、txtファイル内のWordの位置が含まれています。
最初の質問は、文字列をハッシュするための最適なアルゴリズムはどれですか?そして、ハッシュテーブルのサイズを決定する方法は?
前もって感謝します !
:-)
Dan Bernsteinによる 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;
}
まず、一般的にnotハッシュテーブルに暗号化ハッシュを使用することを行います。暗号化標準ではvery高速であるアルゴリズムは、ハッシュテーブル標準では依然として耐え難いほど低速です。
次に、入力のすべてのビットが結果に影響する可能性があることを保証する必要があります。これを行う簡単な方法の1つは、現在の結果をいくつかのビット数だけローテーションしてから、現在のバイトで現在のハッシュコードをXORすることです。文字列の最後に達するまで繰り返します。通常はnotを行うことに注意してください。回転もバイトサイズの偶数倍にする必要があります。
たとえば、8ビットバイトの一般的なケースを想定すると、5ビットずつローテーションできます。
int hash(char const *input) {
int result = 0x55555555;
while (*input) {
result ^= *input++;
result = rol(result, 5);
}
}
編集:また、10000スロットがハッシュテーブルサイズに適していることはめったにないことに注意してください。通常、次の2つのいずれかが必要です:サイズとして素数(いくつかのタイプのハッシュ解決で正確性を確保するために必要)または2のべき乗(したがって、値を正しい範囲に減らすには、ビットマスク)。
Wikipediaが示しています 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;
}
djb2
は、 cnicutarによるstackoverflowで提示 のように、ほぼ確実に優れていますが、 K&R ハッシュも表示する価値があると思います:
1)K&R第1版( source )で示されているように、ひどいハッシュアルゴリズム
unsigned long hash(unsigned char *str)
{
unsigned int hash = 0;
int c;
while (c = *str++)
hash += c;
return hash;
}
2)K&Rバージョン2(本の144ページで検証済み)に示されている、かなりまともなハッシュアルゴリズム。 NB:ハッシュアルゴリズムの外側でモジュラスの配列長へのサイジングを行う予定がある場合は、returnステートメントから% HASHSIZE
を必ず削除してください。また、単純なunsigned
(int)の代わりにreturnおよび "hashval"型unsigned long
を作成することをお勧めします。
unsigned hash(char *s)
{
unsigned hashval;
for (hashval = 0; *s != '\0'; s++)
hashval = *s + 31*hashval;
return hashval % HASHSIZE;
}
2つのアルゴリズムから、第1版のハッシュがひどい理由の1つは、文字列文字orderを考慮していないためであるため、hash("ab")
は同じ値を返すことに注意してくださいhash("ba")
として。これはnotであるため、第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;
}
これらのハッシュ関数を試したところ、次の結果が得られました。約960 ^ 3のエントリがあり、それぞれ長さ64バイト、異なる順序で64文字、ハッシュ値32ビットです。 here のコード。
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
奇妙なことの1つは、ほとんどすべてのハッシュ関数が私のデータに対して6%の衝突率を持っていることです。
最初に、130ワードの40回の衝突が0..99にハッシュされていますか?それが起こるための特別な措置を講じていない場合、完全なハッシュを期待することはできません。通常のハッシュ関数は、ほとんどの場合、ランダムジェネレーターよりも衝突が少なくなりません。
評判の良いハッシュ関数は MurmurHash です。
最後に、ハッシュテーブルのサイズについては、特にバケットが拡張可能か1スロットかによって、どの種類のハッシュテーブルを念頭に置いているかによります。バケットが拡張可能な場合も、選択肢があります。所有するメモリ/速度の制約に応じて、バケットの平均長を選択します。
私が良い結果で使用したことの1つは、次のとおりです(名前が思い出せないため、既に言及されているかどうかはわかりません)。
キーのアルファベット[0,255]の各文字に乱数を使用してテーブルTを事前計算します。 T [k0] xor T [k1] xor ... xor T [kN]を使用して、キー 'k0 k1 k2 ... kN'をハッシュします。これは乱数ジェネレーターと同じくらいランダムであり、計算上非常に実行可能であることを簡単に示すことができます。衝突が非常に多い非常に悪いインスタンスに実際に遭遇した場合は、乱数の新鮮なバッチを使用して全体を繰り返すことができます。