web-dev-qa-db-ja.com

シンプルなハッシュ関数

ハッシュテーブルを使用してさまざまな単語を保存する[〜#〜] c [〜#〜]プログラムを作成しようとしていますが、いくつかのヘルプを使用できます。

まず、保存する必要がある単語の数に最も近い素数のサイズのハッシュテーブルを作成し、次にハッシュ関数を使用して各単語のアドレスを見つけます。私は最も単純な機能から始め、文字を一緒に追加しましたが、結局88%の衝突になりました。それから私は関数を試し始め、それを何に変更しても衝突が35%を下回らないことを発見しました。今私は使っています

unsigned int stringToHash(char *Word, unsigned int hashTableSize){
  unsigned int counter, hashAddress =0;
  for (counter =0; Word[counter]!='\0'; counter++){
    hashAddress = hashAddress*Word[counter] + Word[counter] + counter;
  }
  return (hashAddress%hashTableSize);
}

これは私が思いついた単なるランダム関数ですが、最高の結果が得られます-約35%の衝突。

私は過去数時間ハッシュ関数に関する記事を読んでおり、djb2などのいくつかの簡単なものを使用しようとしましたが、それらはすべてさらに悪い結果をもたらしました(djb2は37%の衝突を引き起こしました」はるかに悪いですが、私は悪いことよりも良いものを期待していました)また、murmur2など、他のより複雑なものの使用方法も知りません。パラメータ(キー、len 、シード)彼らは取ります。

Djb2を使用しても、35%を超える衝突が発生するのは正常ですか、それとも何か間違っていますか? key、len、seedの値は何ですか?

33
Hardell

Sdbmを試してください:

_hashAddress = 0;
for (counter = 0; Word[counter]!='\0'; counter++){
    hashAddress = Word[counter] + (hashAddress << 6) + (hashAddress << 16) - hashAddress;
}
_

またはdjb2:

_hashAddress = 5381;
for (counter = 0; Word[counter]!='\0'; counter++){
    hashAddress = ((hashAddress << 5) + hashAddress) + Word[counter];
}
_

またはAdler32:

_uint32_t adler32(const void *buf, size_t buflength) {
     const uint8_t *buffer = (const uint8_t*)buf;

     uint32_t s1 = 1;
     uint32_t s2 = 0;

     for (size_t n = 0; n < buflength; n++) {
        s1 = (s1 + buffer[n]) % 65521;
        s2 = (s2 + s1) % 65521;
     }     
     return (s2 << 16) | s1;
}

// ...

hashAddress = adler32(Word, strlen(Word));
_

しかし、これらはどれも本当に素晴らしいものではありません。本当に良いハッシュが必要な場合、たとえば lookup のようなもっと複雑なものが必要です。

ハッシュテーブルは、いっぱいになるとすぐに多くの衝突が予想されることに注意してくださいby 70-80%以上。これは完全に正常な動作であり、非常に優れたハッシュアルゴリズムを使用している場合でも発生します。ハッシュテーブルに何かを追加し、比_capacity * 1.5_が既に0.7から0.8を超えるとすぐに、ほとんどのハッシュテーブル実装はハッシュテーブルの容量を増やします(例えば_capacity * 2_または_size / capacity_)。 。容量を増やすと、新しいハッシュテーブルがより高い容量で作成され、現在の値がすべて新しい値に追加されます(そのため、新しいインデックスはほとんどの場合異なるため、すべてを再ハッシュする必要があります)、新しいhastable配列古いものを置き換え、古いものを解放/解放します。 1000ワードのハッシュを計画している場合、1250以上のハッシュテーブル容量を推奨します。1400または1500を推奨します。

ハッシュテーブルは、少なくとも高速で効率的である場合(少なくとも常に予備の容量が必要です)には、「満杯」になることは想定されていません。それはハッシュテーブルのダウンサイズであり、高速です(O(1))、それでも同じデータを別の構造に保存するのに必要なスペースよりも多くのスペースを無駄にします(ソートされた配列として保存する場合、 1000ワードに対して1000の容量のみが必要です;ダウンサイズは、その場合、O(log n)よりもルックアップを高速化できないことです)。ほとんどの場合、どちらの方法でも衝突のないハッシュテーブルは使用できません。ほとんどすべてのハッシュテーブルの実装は、衝突が発生することを期待しており、通常、それらに対処する何らかの方法があります(通常、衝突によりルックアップが多少遅くなりますが、多くの場合、ハッシュテーブルは引き続き機能し、他のデータ構造を破ります)。

また、かなり良いハッシュ関数を使用している場合、ハッシュテーブルのモジュロ(_%_)を使用してハッシュ値をトリミングする場合、ハッシュテーブルに2のべき乗の容量がある場合、要件はありませんが、利点さえありません。終わり。多くのハッシュテーブル実装が常に2のべき乗の容量を使用する理由は、彼らはモジュロを使用せず、代わりにAND(_&_)を使用するためですAND演算はほとんどのCPUで見つかる最速の演算の1つであるため、トリミングします(モジュロはANDよりも高速になることはありません。最良の場合は同等に高速になり、ほとんどの場合ははるかに低速になります)。ハッシュテーブルが2のべき乗のサイズを使用している場合、任意のモジュールをAND演算で置き換えることができます。

_x % 4  == x & 3
x % 8  == x & 7
x % 16 == x & 15
x % 32 == x & 31
...
_

ただし、これは2のべき乗のサイズでのみ機能します。モジュロを使用する場合、ハッシュが非常に悪い「ビット分布」を伴う非常に悪いハッシュである場合、2サイズの累乗は何かしか買えません。不良ビットの分布は、通常、ビットシフト(_>>_または_<<_)を使用しないハッシュ、またはビットシフトと同様の効果をもたらす他の操作によって発生します。

私はあなたのためにlookup3の実装を削除しました:

_#include <stdint.h>
#include <stdlib.h>

#define rot(x,k) (((x)<<(k)) | ((x)>>(32-(k))))

#define mix(a,b,c) \
{ \
  a -= c;  a ^= rot(c, 4);  c += b; \
  b -= a;  b ^= rot(a, 6);  a += c; \
  c -= b;  c ^= rot(b, 8);  b += a; \
  a -= c;  a ^= rot(c,16);  c += b; \
  b -= a;  b ^= rot(a,19);  a += c; \
  c -= b;  c ^= rot(b, 4);  b += a; \
}

#define final(a,b,c) \
{ \
  c ^= b; c -= rot(b,14); \
  a ^= c; a -= rot(c,11); \
  b ^= a; b -= rot(a,25); \
  c ^= b; c -= rot(b,16); \
  a ^= c; a -= rot(c,4);  \
  b ^= a; b -= rot(a,14); \
  c ^= b; c -= rot(b,24); \
}

uint32_t lookup3 (
  const void *key,
  size_t      length,
  uint32_t    initval
) {
  uint32_t  a,b,c;
  const uint8_t  *k;
  const uint32_t *data32Bit;

  data32Bit = key;
  a = b = c = 0xdeadbeef + (((uint32_t)length)<<2) + initval;

  while (length > 12) {
    a += *(data32Bit++);
    b += *(data32Bit++);
    c += *(data32Bit++);
    mix(a,b,c);
    length -= 12;
  }

  k = (const uint8_t *)data32Bit;
  switch (length) {
    case 12: c += ((uint32_t)k[11])<<24;
    case 11: c += ((uint32_t)k[10])<<16;
    case 10: c += ((uint32_t)k[9])<<8;
    case 9 : c += k[8];
    case 8 : b += ((uint32_t)k[7])<<24;
    case 7 : b += ((uint32_t)k[6])<<16;
    case 6 : b += ((uint32_t)k[5])<<8;
    case 5 : b += k[4];
    case 4 : a += ((uint32_t)k[3])<<24;
    case 3 : a += ((uint32_t)k[2])<<16;
    case 2 : a += ((uint32_t)k[1])<<8;
    case 1 : a += k[0];
             break;
    case 0 : return c;
  }
  final(a,b,c);
  return c;
}
_

このコードは、元のコードほどパフォーマンスが最適化されていないため、はるかに簡単です。また、元のコードほどポータブルではありませんが、今日使用されているすべての主要なコンシューマプラットフォームに移植可能です。また、CPUエンディアンを完全に無視していますが、それは実際には問題ではなく、大小のエンディアンCPUで動作します。ビッグエンディアンCPUとリトルエンディアンCPUの同じデータに対して同じハッシュを計算しないことに注意してください。ただし、それは要件ではありません。両方の種類のCPUで適切なハッシュを計算し、1台のマシン上の同じ入力データに対して常に同じハッシュを計算することが重要です。

この関数は次のように使用します。

_unsigned int stringToHash(char *Word, unsigned int hashTableSize){
  unsigned int initval;
  unsigned int hashAddress;

  initval = 12345;
  hashAddress = lookup3(Word, strlen(Word), initval);
  return (hashAddress%hashTableSize);
  // If hashtable is guaranteed to always have a size that is a power of 2,
  // replace the line above with the following more effective line:
  //     return (hashAddress & (hashTableSize - 1));
}
_

initvalが何であるか疑問に思うでしょう。まあ、それはあなたが望むものは何でもです。あなたはそれを塩と呼ぶことができます。ハッシュ値に影響しますが、このためにハッシュ値の品質が向上したり悪化したりすることはありません(少なくとも平均的なケースでは、非常に特定のデータに対して多少の衝突が発生する可能性があります)。例えば。同じデータを2回ハッシュする場合は、異なるinitval値を使用できますが、毎回異なるハッシュ値を生成する必要があります(保証されるわけではありませんが、initval異なる;同じ値を作成する場合、これは非常に不運な偶然であり、それを一種の衝突として扱う必要があります)。同じハッシュテーブルのデータをハッシュするとき、異なるinitval値を使用することはお勧めできません(これにより、平均して衝突が多くなります)。 initvalのもう1つの用途は、ハッシュを他のデータと結合する場合です。その場合、他のデータをハッシュするときに既存のハッシュはinitvalになります(したがって、他のデータと以前のハッシュの両方が影響します)ハッシュ関数の結果)。ハッシュテーブルの作成時にランダム値を選択または選択する場合は、initvalを_0_に設定することもできます(そして、このハッシュテーブルのインスタンスには常にこのランダム値を使用しますが、各ハッシュテーブルには独自のランダム値があります) )。

衝突に関する注意:

衝突は通常、実際にはそれほど大きな問題ではありません。通常、衝突を避けるためだけに大量のメモリを無駄にすることはありません。問題はむしろ、あなたがそれらを効率的な方法でどのように扱うかです。

あなたは現在9000語を扱っていると言いました。並べ替えられていない配列を使用していた場合、配列内のWordを見つけるには、平均で4500回の比較が必要になります。私のシステムでは、4500の文字列比較(単語の長さが3〜20文字であると仮定)には38マイクロ秒(0.000038秒)が必要です。そのため、このような単純で効果のないアルゴリズムでも、ほとんどの目的には十分に高速です。 Wordリストを並べ替えてバイナリ検索を使用すると仮定すると、配列内のWordを見つけるのに必要な比較は平均で13回だけです。 13の比較は時間の点ではほとんどありません。ベンチマークを確実に行うには少なすぎます。したがって、ハッシュテーブルでWordを見つけるのに2〜4回の比較が必要な場合、それが大きなパフォーマンスの問題であるかどうかを1秒も無駄にしないでしょう。

あなたの場合、バイナリ検索でソートされたリストは、ハッシュテーブルをはるかに上回る可能性があります。確かに、13回の比較には2〜4回の比較よりも時間がかかりますが、ハッシュテーブルの場合、最初に入力データをハッシュしてルックアップを実行する必要があります。ハッシュだけでは、すでに13回の比較よりも時間がかかる場合があります。 betterハッシュ、longerは同じ量のデータがハッシュされるために必要です。したがって、ハッシュテーブルは、非常に大量のデータがある場合、または頻繁にデータを更新する必要がある場合(たとえば、テーブルに単語を常に追加/削除する場合)、これらの操作はハッシュテーブルよりもコストが低いため、パフォーマンス面で効果がありますソートされたリスト用です)。 hashatbleがO(1)であるという事実は、その大きさに関係なく、ルックアップが約常に同じ時間が必要です。 O(log n)は、ルックアップがワード数とともに対数的に増加することのみを意味します。つまり、ワード数が増え、ルックアップが遅くなります。しかし、Big-O表記は絶対速度については何も述べていません!これは大きな誤解です。 O(1)アルゴリズムがO(log n)アルゴリズムよりも常に高速に実行されるとは言われていません。 Big-O表記は、O(log n)アルゴリズムが特定の数の値に対して高速で、値の数を増やし続けると、O(1)アルゴリズムがO(log n)ある時点でのアルゴリズムですが、現在のワード数はその時点よりはるかに少ない場合があります。両方のアプローチのベンチマークを行わないと、Big-O表記を見ただけではどちらが速いかわかりません。

衝突に戻ります。衝突が発生した場合はどうすればよいですか?衝突の数が少なく、ここでは衝突の総数(ハッシュテーブルで衝突している単語の数)ではなく、インデックス1あたり(同じハッシュテーブルのインデックスに格納されている単語の数)あなたの場合はおそらく2-4)、最も簡単なアプローチはそれらをリンクリストとして保存することです。このテーブルインデックスでこれまでに衝突がなかった場合、キーと値のペアは1つだけです。衝突があった場合、キー/値ペアのリンクリストがあります。その場合、コードはリンクリストを反復処理し、各キーを検証し、一致する場合は値を返す必要があります。数字で見ると、このリンクリストには4つ以上のエントリはありません。4つの比較を行うことは、パフォーマンスの点では重要ではありません。そのため、インデックスの検索はO(1)で、値の検索(またはこのキーがテーブルにないことの検出)はO(n)ですが、ここでnはリンクされたリストエントリ(したがって、最大で4)。

衝突の回数が増えると、リンクリストが遅くなり、動的なサイズのソートされたキー/値ペアの配列を保存することもできます。これにより、O(log n)およびnはその配列内のキーの数であり、hastable内のすべてのキーの数ではありません。 1つのインデックスで100回の衝突があったとしても、適切なキー/値のペアを見つけるには最大7回の比較が必要です。それはまだほとんど何もありません。 1つのインデックスで実際に100回の衝突がある場合、ハッシュアルゴリズムがキーデータに適していないか、ハッシュテーブルの容量が小さすぎるという事実にもかかわらず。動的にサイズ設定され、並べ替えられた配列の欠点は、キーの追加/削除がリンクリストの場合よりも多少手間がかかることです(コード単位、必​​ずしもパフォーマンス単位ではありません)。したがって、通常、衝突の数を十分に低く抑え、リンクリストを使用してCで実装し、既存のハッシュテーブルの実装に追加することはほとんど簡単です。

私が持っているほとんどのハッシュテーブルの実装は、衝突に対処するためにそのような「代替データ構造へのフォールバック」を使用しているようです。欠点は、これらが代替データ構造を格納するために少し余分なメモリを必要とし、その構造内のキーを検索するためにもう少しコードを必要とすることです。ハッシュテーブル自体に衝突を保存し、追加のメモリを必要としないソリューションもあります。ただし、これらのソリューションにはいくつかの欠点があります。最初の欠点は、衝突が発生するたびに、データが追加されるにつれて衝突がさらに増える可能性があることです。 2つ目の欠点は、キーのルックアップ時間がこれまでの衝突の数に比例して減少する一方で(前述したように、データが追加されるたびにすべての衝突がさらに多くの衝突につながる)、ハッシュテーブルにないキーのルックアップ時間がさらに悪化することです最後に、ハッシュテーブルにないキーのルックアップを実行すると(ルックアップを実行しないとわかりません)、ルックアップにはハッシュテーブル全体の線形検索(YUCK !!!) 。したがって、余分なメモリを節約できる場合は、衝突を処理するための代替構造を探してください。

73
Mecki

まず、保存する必要のある単語の数に近い素数のサイズのハッシュテーブルを作成し、次にハッシュ関数を使用して各単語のアドレスを見つけます。

...

return(hashAddress%hashTableSize);

異なるハッシュの数は単語の数に匹敵するので、衝突がずっと少ないとは期待できません。

ランダムハッシュ(達成できる最善の方法)を使用して簡単な統計テストを行ったところ、#words == #differentハッシュがある場合、26%が制限衝突率であることがわかりました。

2