web-dev-qa-db-ja.com

ハッシュ関数が素数モジュラスを使用する必要があるのはなぜですか?

ずいぶん前に、私は1.25ドルでお買い得表からデータ構造の本を買いました。その中で、ハッシュ関数の説明は、「数学の性質」のため、最終的に素数でmodする必要があると述べました。

1.25ドルの本に何を期待しますか?

とにかく、私は数学の性質について長年考えてきましたが、それでも理解できません。

バケットの素数が存在する場合でも、数字の分布は本当に多いですか?それとも、みんなelseが受け入れるので、誰もが受け入れる古いプログラマーの物語ですか?

318
theschmitzer

通常、単純なハッシュ関数は、入力の「コンポーネント部分」(文字列の場合は文字)を取得し、それらに定数の累乗を掛け、整数型で加算することで機能します。したがって、たとえば、文字列の典型的な(特に良いとは言えないが)ハッシュは次のようになります。

(first char) + k * (second char) + k^2 * (third char) + ...

次に、最初の文字がすべて同じ文字列の束が入力されると、少なくとも整数型がオーバーフローするまで、結果はすべてモジュロkになります。

[例として、Javaの文字列hashCodeはこれに不気味に似ています-k = 31で文字の順序を逆にします。したがって、同じように終了する文字列間で31を法とする顕著な関係、および終わり近くを除いて同じ文字列間で2 ^ 32を法とする顕著な関係が得られます。これは、ハッシュテーブルの動作を真剣に混乱させません。]

ハッシュテーブルは、バケットの数でハッシュのモジュラスを取ることにより機能します。

衝突はハッシュテーブルの効率を低下させるので、ハッシュテーブルでは、起こりそうな場合に衝突を起こさないことが重要です。

今、誰かがすべての値をハッシュテーブルに入れて、すべてのアイテムが同じ最初の文字を持つように、アイテム間に何らかの関係があると仮定します。これはかなり予測可能な使用パターンであるため、あまり多くの衝突が発生することは望ましくありません。

ハッシュで使用される定数とバケットの数が coprime である場合、「数学の性質のため」、一般的な場合には衝突が最小化されることがわかります。 coprime でない場合、衝突が最小化されない入力間にかなり単純な関係があります。すべてのハッシュは、共通因子を法とするモジュロに等しくなります。つまり、すべてのハッシュは、共通因子を法とするその値を持つバケットの1/n番目に分類されます。衝突はn倍発生します。ここで、nは共通の要因です。 nは少なくとも2なので、かなり単純なユースケースでは、通常の2倍以上の衝突を生成することは受け入れられないと思います。一部のユーザーがディストリビューションをバケットに分割する場合、単純な予測可能な使用法ではなく、異常な事故にしたいと考えています。

現在、ハッシュテーブルの実装は、明らかに、それらに入れられる項目を制御できません。彼らはそれらが関連していることを防ぐことはできません。そのため、定数とバケットカウントが互いに素であることを確認する必要があります。そうすれば、いくつかの小さな共通要因に関してバケットのモジュラスを決定するために「最後の」コンポーネントだけに依存することはありません。私が知る限り、彼らはこれを達成するために素数である必要はなく、単に素数である。

しかし、ハッシュ関数とハッシュテーブルが独立して記述されている場合、ハッシュテーブルはハッシュ関数がどのように機能するかを知りません。小さな係数の定数を使用している可能性があります。運がよければ、完全に異なって動作し、非線形になる可能性があります。ハッシュが十分であれば、バケット数は問題ありません。しかし、偏執的なハッシュテーブルは適切なハッシュ関数を想定できないため、素数のバケットを使用する必要があります。同様に、偏執的なハッシュ関数は、誰かが定数と共通の要因を持っているバケットの数を使用する可能性を減らすために、大きな素数の定数を使用する必要があります。

実際には、バケットの数として2のべき乗を使用するのはかなり普通だと思います。これは便利で、適切な大きさの素数を検索したり事前に選択したりする必要がなくなります。したがって、乗数さえ使用しないようにハッシュ関数に依存します。これは一般に安全な仮定です。ただし、上記のようなハッシュ関数に基づいて、ときどき不正なハッシュ動作が発生する可能性があり、プライムバケットカウントがさらに役立つ可能性があります。

「すべてが素数でなければならない」という原則について言えば、ハッシュテーブル上で適切に配布するために必要な条件ではなく、十分な条件を知っている限りです。これにより、誰もが他の人が同じルールに従っていると仮定する必要なく相互運用できます。

[編集:素数のバケットを使用するもう1つのより特殊な理由があります。これは、線形プローブで衝突を処理する場合です。次に、ハッシュコードからストライドを計算し、そのストライドがバケットカウントの要因になる場合、開始した場所に戻る前に(bucket_count /ストライド)プローブしか実行できません。最も避けたいケースは、もちろんストライド= 0であり、これは特別なケースである必要がありますが、特殊なケースであるbucket_count/strideが小さな整数に等しくならないようにするには、bucket_countを素数にして、 0ではないストライドが提供されます。]

232
Steve Jessop

ハッシュテーブルから挿入/取得するときに最初に行うことは、指定されたキーのhashCodeを計算し、hashCode%table_lengthを実行してhashCodeをhashTableのサイズにトリミングして正しいバケットを見つけることです。ここにあなたがおそらくどこかで読んだ2つの「ステートメント」があります

  1. Table_lengthに2のべき乗を使用する場合、(hashCode(key)%2 ^ n)を見つけることは(hashCode(key)&(2 ^ n -1))と同じくらい簡単で迅速です。ただし、特定のキーのhashCodeを計算する関数が適切でない場合は、いくつかのハッシュバケットに多くのキーがクラスター化されることは間違いありません。
  2. しかし、table_lengthに素数を使用すると、少し愚かなhashCode関数があっても、計算されたhashCodeは異なるハッシュバケットにマップされる可能性があります。

そして、ここにその証拠があります。

HashCode関数の結果、次のようなhashCodeが特に{x、2x、3x、4x、5x、6x ...}の場合、これらはすべてm個のバケットにクラスター化されます。ここで、m = table_length/GreatestCommonFactor (table_length、x)。 (これを確認/導出するのは簡単です)。クラスタリングを回避するために、次のいずれかを実行できるようになりました

{x、2x、3x、4x、5x、6x ...}のように、別のhashCodeの倍数であるhashCodeをあまり多く生成しないようにしてください。ただし、hashTableが何百万ものエントリ。または、GreatestCommonFactor(table_length、x)を1に等しくすることで、つまりtable_lengthをxと素数にすることにより、mをtable_lengthに等しくします。また、xがほぼ任意の数である場合は、table_lengthが素数であることを確認してください。

から- http://srinvis.blogspot.com/2006/07/hash-table-lengths-and-prime-numbers.html

28
user177612

http://computinglife.wordpress.com/2008/11/20/why-do-hash-functions-use-prime-numbers/

写真でもかなり明確な説明。

編集:要約すると、選択した素数で値を乗算し、それらをすべて加算するときに一意の値を取得する可能性が最も高いため、素数が使用されます。たとえば、文字列が与えられ、各文字の値に素数を乗算し、それらをすべて加算すると、そのハッシュ値が得られます。

より良い質問は、なぜ正確に31番なのでしょうか?

10
AlbertoPL

tl; dr

index[hash(input)%2]は、可能なすべてのハッシュの半分と値の範囲で衝突を引き起こします。 index[hash(input)%prime]は、考えられるすべてのハッシュの<2の衝突をもたらします。除数をテーブルサイズに固定すると、数値がテーブルより大きくなることもなくなります。

9
Indolering

素数が使用されるのは、Pを法とする多項式を使用する典型的なハッシュ関数に対して一意の値を取得する可能性が高いためです。これは、2つの異なる多項式がPを法とする同じ値を生成することを意味します。これらの多項式の差は、同じ次数N(またはそれ以下)の多項式です。これはN個以下の根を持ちます(この主張は、フィールド=>素数上の多項式についてのみ当てはまるため、ここでは数学の性質が示しています)。したがって、NがPよりもはるかに小さい場合、衝突は発生しない可能性があります。その後、実験で、長さが5〜10の文字列のハッシュテーブルの衝突を回避するのに十分な37であり、計算に使用するのに十分小さいことを実験で示すことができます。

8
TT_

別の視点を提供するために、このサイトがあります:

http://www.codexon.com/posts/hash-functions-the-modulo-prime-myth

これは、素数のバケットに切り捨てるのではなく、可能な限り多くのバケットを使用する必要があると主張しています。それは合理的な可能性のようです。直感的には、バケットの数が多いほど良いことは確かにわかりますが、これについて数学的な議論をすることはできません。

5
Falaina

ハッシュ関数の選択に依存します。

多くのハッシュ関数は、データのさまざまな要素を、マシンのワードサイズに対応する2のべき乗を法とするいくつかの係数で乗算することで結合します(モジュラスは計算をオーバーフローさせるだけで自由になります)。

データ要素の乗数とハッシュテーブルのサイズの間に共通の要素は必要ありません。データ要素を変化させてもデータがテーブル全体に分散しないことがあるためです。テーブルのサイズに素数を選択した場合、このような一般的な要因はほとんどありません。

一方、これらの要因は通常奇数の素数で構成されているため、ハッシュテーブルに2のべき乗を使用しても安全です(たとえば、EclipseはJava hashCode()メソッドを生成するときに31を使用します) 。

3
starblue

プライムは一意の番号です。ユニークなのは、プライムと他の数の積が、プライムがそれを構成するために使用されるという事実により、ユニークである可能性が最も高いことです(もちろん、プライム自体のようにユニークではありません)。このプロパティは、ハッシュ関数で使用されます。

「Samuel」という文字列を指定すると、各構成数字または文字に素数を乗算して加算することにより、一意のハッシュを生成できます。これが素数が使用される理由です。

ただし、素数の使用は古い手法です。ここで重要なのは、十分に一意なキーを生成できる限り、他のハッシュ手法に移行できることを理解することです。 http://www.azillionmonkeys.com/qed/hash.html に関するこのトピックの詳細については、こちらをご覧ください。

http://computinglife.wordpress.com/2008/11/20/why-do-hash-functions-use-prime-numbers/

3
user105033

テーブルサイズ(またはモジュロの数)がT =(B * C)であるとします。入力のハッシュが(N * A * B)のような場合(Nは任意の整数)、出力は適切に分散されません。 nがC、2C、3Cなどになるたびに、出力が繰り返されます。つまり、出力はCの位置でのみ配布されます。ここのCは(T/HCF(table-size、hash))であることに注意してください。

この問題はHCF 1を作成することで解消できます。素数はそのために非常に適しています。

もう1つの興味深い点は、Tが2 ^ Nの場合です。これらは、input-hashの下位Nビットすべてとまったく同じ出力を提供します。すべての数字は2の累乗で表現できるため、Tで任意の数のモジュロを取る場合、2以上のフォーム数(> = N)をすべて減算するため、入力に応じて常に特定のパターンの数を生成します。これも悪い選択です。

同様に、10 ^ NとしてのTも同様の理由(バイナリではなく数字の10進表記のパターン)のために悪いです。

そのため、素数はより良い分布結果を与える傾向があるため、テーブルサイズに適しています。

2

私の他の答えからコピー https://stackoverflow.com/a/43126969/917428 。詳細と例については、それを参照してください。

コンピューターがベース2で動作するという事実に関係していると思います。ベース10でも同じことがどのように機能するかを考えてください。

  • 8%10 = 8
  • 18%10 = 8
  • 87865378%10 = 8

数字が何であるかは関係ありません。8で終わる限り、そのモジュロ10は8になります。

十分に大きく、2のべき乗でない数を選択すると、ハッシュ関数が実際にはそれらのサブセットではなく、すべての入力ビットの関数になります。

2
Ste_95

スティーブジェソップの答えに何かを追加したいと思います(評判が足りないのでコメントできません)。しかし、私はいくつかの有用な資料を見つけました。彼の答えは非常に助けですが、彼は間違いを犯しました。バケットサイズは2のべきではありません。

除算法を使用する場合、通常、mの特定の値を避けます。たとえば、m = 2 ^ pの場合、h(k)はkのp最下位ビットであるため、mは2のべき乗であってはなりません。すべての低次のpビットパターンが同様に発生する可能性があることがわかっている場合を除き、キーのすべてのビットに依存するようにハッシュ関数を設計することをお勧めします。演習11.3-3で示すように、kが基数2 ^ pで解釈される文字列である場合、m = 2 ^ p-1を選択するのは適切ではありません。

それが役に立てば幸い。

1
iefgnoix

上の人気のある回答のいくつかにリンクされている人気のあるwordpressウェブサイトを読みました。私が理解したことから、私が行った簡単な観察を共有したいと思います。

詳細は記事 here で見つけることができますが、次のことが当てはまると仮定します。

  • 素数を使用すると、一意の値の「ベストチャンス」が得られます。

一般的なハッシュマップの実装では、2つのことを一意にする必要があります。

  • niqueのハッシュコードkey
  • nique実際のインデックスを保存するインデックスvalue

どのようにユニークインデックスを取得しますか?内部コンテナの初期サイズも同様にプライムにします。したがって、基本的には、primeが関係しているのは、固有の番号を生成する固有の特性を備えているため、最終的にオブジェクトのIDを使用して内部コンテナー内のインデックスを見つけるためです。

例:

key = "key"

値= "値" uniqueId = "k" * 31 ^ 2 + "e" * 31 ^ 1` + "y"

一意のIDにマップします

ここで、値に一意の場所が必要です。

uniqueId % internalContainerSize == uniqueLocationForValueは、internalContainerSizeも素数であると仮定しています。

私はこれが単純化されていることを知っていますが、一般的な考え方を理解したいと思っています。

0
Ryan

ハッシュ関数では、一般にコリジョンを最小限に抑えるだけでなく、数バイトを変更しながら同じハッシュを維持することを不可能にすることが重要です。

方程式があるとします:(x + y*z) % key = x0<x<key0<z<key。 keyが素数の場合、n * y = keyはNのnごとにtrueであり、他のすべての数に対してfalseです。

キーが主要な例ではない例:x = 1、z = 2、key = 8 * y = keyはNのnごとにtrueです。8は素数ではないため、方程式の解の量は実際に2倍になりました。

攻撃者が既に8が方程式の解である可能性があることを知っている場合、ファイルを8から4に変更しても同じハッシュを取得できます。

0
Christian