昔、私はデータ構造の本をバーゲンテーブルから1.25ドルで購入しました。その中で、ハッシュ関数の説明は、「数学の性質」のため、最終的には素数でmodするべきだと述べていました。
1.25ドルの本に何を期待しますか?
とにかく、私は何年にもわたって数学の性質について考えてきましたが、それでもそれを理解することはできません。
素数のバケットがある場合でも、数の分布は本当にもっと多いのでしょうか?それとも、他の誰もがそれを受け入れるので、これは誰もが受け入れる古いプログラマーの物語ですか?
昔、私はデータ構造の本をバーゲンテーブルから1.25ドルで購入しました。その中で、ハッシュ関数の説明は、「数学の性質」のため、最終的には素数でmodするべきだと述べていました。
1.25ドルの本に何を期待しますか?
とにかく、私は何年にもわたって数学の性質について考えてきましたが、それでもそれを理解することはできません。
素数のバケットがある場合でも、数の分布は本当にもっと多いのでしょうか?それとも、他の誰もがそれを受け入れるので、これは誰もが受け入れる古いプログラマーの物語ですか?
回答:
通常、単純なハッシュ関数は、入力(文字列の場合は文字)の「コンポーネント部分」を取得し、定数の累乗で乗算して、整数型に加算します。たとえば、文字列の典型的な(特に良いわけではありませんが)ハッシュは次のようになります。
(first char) + k * (second char) + k^2 * (third char) + ...
次に、最初の文字がすべて同じ文字列の束が入力されると、少なくとも整数型がオーバーフローするまで、結果はすべてkを法として同じになります。
[例として、Javaの文字列hashCodeは不気味にこれに似ています-k = 31で文字を逆順にします。したがって、同じ方法で終了する文字列間で31を法とする印象的な関係が得られ、最後を除いて同じである文字列間で2 ^ 32を法とする印象的な関係が得られます。これはハッシュテーブルの振る舞いを深刻に台無しにすることはありません。]
ハッシュテーブルは、バケットの数に対するハッシュの係数を取得することで機能します。
衝突はハッシュテーブルの効率を低下させるため、可能性のあるケースでは衝突を発生させないことがハッシュテーブルで重要です。
さて、誰かがすべての最初の文字が同じであるように、アイテム間に何らかの関係がある値全体をハッシュテーブルに入れたとします。これはかなり予測可能な使用パターンであると私は言うので、あまりにも多くの衝突を発生させたくありません。
「数学の性質のため」、ハッシュで使用される定数とバケットの数が互いに素であれば、いくつかの一般的なケースでは衝突が最小限に抑えられます。彼らが素数でない場合の場合、衝突が最小化されない入力間にはかなり単純な関係がいくつかあります。すべてのハッシュは共通係数を法として等しい値で出力されます。つまり、すべてのハッシュは、共通係数を法としてその値を持つバケットの1 / n番目に分類されます。衝突はn倍になります。nは共通の要素です。nは少なくとも2であるため、かなり単純なユースケースでは、通常の2倍以上の衝突を生成することは許容できません。一部のユーザーがディストリビューションをバケットに分割する場合、単純な予測可能な使用法ではなく、異常な事故にしたいと考えています。
現在、ハッシュテーブルの実装は、それらに入れられるアイテムを制御できません。彼らはそれらが関連しているのを防ぐことはできません。したがって、行うべきことは、定数とバケット数が素数であることを確認することです。そうすることで、「最後の」コンポーネントだけに依存して、いくつかの小さな共通因子に関するバケットの係数を決定することはありません。私が知る限り、これを達成するために彼らは素数である必要はありません、ただ素数にしてください。
しかし、ハッシュ関数とハッシュテーブルが独立して書かれている場合、ハッシュテーブルはハッシュ関数がどのように機能するかを知りません。係数が小さい定数を使用している可能性があります。運が良ければ、動作はまったく異なり、非線形になる可能性があります。ハッシュが十分であれば、バケット数は問題ありません。しかし、偏執的なハッシュテーブルは良いハッシュ関数を想定できないため、素数のバケットを使用する必要があります。同様に、偏執的なハッシュ関数は、誰かが偶然に定数と共通の要素を持ついくつかのバケットを使用する可能性を減らすために、大きな素数定数を使用する必要があります。
実際には、バケットの数として2の累乗を使用するのはごく普通のことだと思います。これは便利であり、適切な大きさの素数を検索したり事前に選択したりする必要がなくなります。したがって、ハッシュ関数を使用して乗数さえも使用しないようにします。これは一般に安全な仮定です。ただし、上記のようなハッシュ関数に基づいて、時折悪いハッシュ動作が発生する可能性があり、プライムバケット数がさらに役立つ場合があります。
「すべてが素数でなければならない」という原則を説明することは、私が知る限り、ハッシュテーブル上で適切に分散するための十分条件ですが、必要条件ではありません。これにより、他のユーザーが同じルールに従っていると想定する必要なく、誰もが相互運用できます。
[編集:素数のバケットを使用する別のより特別な理由があります。これは、線形プローブで衝突を処理する場合です。次に、ハッシュコードからストライドを計算します。そのストライドがバケットカウントの要素であることが判明した場合は、最初に戻る前に(bucket_count / stride)プローブしか実行できません。もちろん、最も避けたいケースはストライド= 0です。これは特殊なケースでなければなりませんが、特殊なケースであるbucket_count / strideが小さな整数に等しいことを避けるには、bucket_countを素数にして、ストライドは、0でない場合に提供されます。]
ハッシュテーブルから挿入/受信するときに最初に行うことは、指定されたキーのhashCodeを計算し、hashCode%table_lengthを実行してhashCodeをhashTableのサイズにトリミングすることで正しいバケットを見つけることです。あなたがたぶんどこかで読んだ2つの「ステートメント」は次のとおりです
そしてここに証明があります。
hashCode関数の結果が他の{x、2x、3x、4x、5x、6x ...}の次のhashCodeであるとすると、これらはすべて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
http://computinglife.wordpress.com/2008/11/20/why-do-hash-functions-use-prime-numbers/
写真もあり、かなり明確な説明。
編集:要約として、選択した素数を値に掛けてそれらをすべて足し合わせると一意の値が得られる可能性が最も高いため、素数が使用されます。たとえば、文字列が指定されている場合、各文字の値に素数を掛けてからそれらをすべて加算すると、ハッシュ値が得られます。
より良い質問は、なぜ正確に31なのでしょうか?
*32
は、単純なビットシフト、またはさらに優れた即時アドレススケール係数(lea eax,eax*8; leax, eax,eax*4
x86 / x64など)です。ですから*31
、素数乗算の良い候補です。これは数年前にはほぼ真実でした-現在の最新のCPUアーキテクチャはほぼ瞬時に乗算を行っています-除算は常に遅いです...
index[hash(input)%2]
すべての可能なハッシュの半分と値の範囲で衝突が発生します。 index[hash(input)%prime]
すべての可能なハッシュのうち<2の衝突になります。除数をテーブルサイズに固定すると、その数がテーブルより大きくなることもなくなります。
素数が使用されるのは、Pを法とする多項式を使用する一般的なハッシュ関数の一意の値を取得する可能性が高いためです。たとえば、長さが<= Nの文字列にそのようなハッシュ関数を使用すると、衝突が発生します。これは、2つの異なる多項式がPを法として同じ値を生成することを意味します。これらの多項式の差は、同じ次数N(またはそれ以下)の多項式です。根はN個以下です(これは、この主張がフィールド上の多項式=>素数にのみ当てはまるため、ここでは数学の性質が示しています)。したがって、NがPよりもはるかに小さい場合は、衝突がない可能性があります。その後、実験により、37は長さが5〜10の文字列のハッシュテーブルの衝突を回避するのに十分な大きさであり、計算に使用するには十分に小さいことがおそらくわかるでしょう。
別の視点を提供するために、このサイトがあります:
http://www.codexon.com/posts/hash-functions-the-modulo-prime-myth
これは、素数のバケットに切り捨てるのではなく、可能な限り最大数のバケットを使用する必要があると主張しています。それは合理的な可能性のようです。直感的には、バケットの数が多い方が良いのは確かですが、これについて数学的な議論をすることはできません。
素数は一意の番号です。それらはユニークです。素数と他の数の積は、素数がそれを構成するために使用されるという事実により、ユニークである(コースの素数自体ほどユニークではない)可能性が最も高くなります。このプロパティはハッシュ関数で使用されます。
文字列「Samuel」を指定すると、構成する数字または文字のそれぞれに素数を掛けてそれらを加算することにより、一意のハッシュを生成できます。これが、素数が使用される理由です。
ただし、素数の使用は古い手法です。ここで重要なのは、十分に一意のキーを生成できる限り、他のハッシュ手法にも移行できることです。http://www.azillionmonkeys.com/qed/hash.htmlに関するこのトピックの詳細については、ここに アクセスしてください
http://computinglife.wordpress.com/2008/11/20/why-do-hash-functions-use-prime-numbers/
ハッシュ関数の選択に依存します。
多くのハッシュ関数は、マシンのワードサイズに対応する2の累乗を法とするいくつかの係数を掛けて、データ内のさまざまな要素を結合します(その係数は、計算をオーバーフローさせるだけで解放されます)。
データ要素を変更してもテーブル全体にデータが分散されない可能性があるため、データ要素の乗数とハッシュテーブルのサイズの間に共通の要素は必要ありません。テーブルのサイズに素数を選択した場合、そのような一般的な要素はほとんどありません。
一方、これらの要素は通常、奇数の素数で構成されているため、ハッシュテーブルに2のべき乗を使用しても安全です(たとえば、EclipseがJava hashCode()メソッドを生成するときに31を使用します)。
テーブルサイズ(またはモジュロの数)がT =(B * C)であるとします。入力のハッシュが(N * A * B)のような場合、Nは任意の整数にできるため、出力はうまく分散されません。nがC、2C、3Cなどになるたびに、出力が繰り返されます。つまり、出力はCの位置でのみ配布されます。ここでCは(T / HCF(table-size、hash))であることに注意してください。
この問題は、HCF 1を作成することで解消できます。素数はそのために非常に適しています。
もう1つの興味深いことは、Tが2 ^ Nの場合です。これらは、入力ハッシュの下位Nビットすべてとまったく同じ出力を提供します。すべての数は2の累乗で表すことができるので、Tを使用して任意の数のモジュロを取る場合、2以上の数の累乗を減算します。これは> = Nなので、入力に応じて特定のパターンの数を常に出力します。これも悪い選択です。
同様に、10 ^ NとしてのTも同様の理由(2進数ではなく数値の10進表記のパターン)のために悪いです。
そのため、素数を使用すると、より良い分散結果が得られる傾向があるため、テーブルサイズに適しています。
私の他の回答https://stackoverflow.com/a/43126969/917428からコピーします。詳細と例については、こちらをご覧ください。
ベース2でコンピューターが動作するという事実に関係していると思います。ベース10で同じことがどのように機能するかを考えてみてください。
数が何であってもかまいません。8で終わる限り、モジュロ10は8になります。
十分に大きな2のべき乗ではない数値を選択すると、ハッシュ関数が実際にはすべての入力ビットのサブセットではなく、すべての入力ビットの関数になります。
スティーブ・ジェソップの答えに何か付け加えたいと思います(評判が足りないのでコメントできません)。しかし、私はいくつかの役立つ資料を見つけました。彼の答えは非常に役立ちますが、彼は誤りを犯しました。バケットサイズは2の累乗であってはなりません。263ページのThomas Cormen、Charles Leisersenなどによる「アルゴリズムの概要」の本から引用します。
除算法を使用する場合、通常、mの特定の値を避けます。たとえば、m = 2 ^ pの場合、h(k)はkの最下位のpビットに過ぎないため、mは2のべき乗であってはなりません。すべての低次のpビットパターンが等しく可能性があることがわかっている場合を除き、ハッシュ関数をキーのすべてのビットに依存するように設計することをお勧めします。演習11.3-3で表示するように求められたため、kが基数2 ^ pで解釈される文字列である場合、m = 2 ^ p-1を選択することは適切ではありません。kの文字を置換してもハッシュ値は変更されないためです。
それが役に立てば幸い。
ハッシュ関数では、コリジョンを最小限に抑えることが重要であるだけでなく、数バイトを処理しながら同じハッシュにとどまることを不可能にすることが重要です。
あなたが方程式を持っているとしましょう:
(x + y*z) % key = x
と0<x<key
と0<z<key
。keyが素数である場合、n * y = keyはNのnごとにtrueになり、他のすべての数値に対してfalseになります。
keyが素数ではない例:x = 1、z = 2、key = 8 key / z = 4は自然数なので、4は方程式の解となり、この場合(n / 2) * y = keyはNのnごとに真です。8は素数ではないため、方程式の解の量は実質的に2倍になっています。
攻撃者がすでに8が方程式の可能な解決策であることを知っている場合、ファイルを8から4に変更しても、同じハッシュを取得できます。
上記の人気のある回答の上部にリンクされている人気のワードプレスのウェブサイトを読みました。私が理解したことから、私が行った簡単な観察を共有したいと思います。
詳細については、こちらの記事をご覧ください。ただし、次のことが当てはまると想定しています。
一般的なハッシュマップの実装では、2つのことを一意にする必要があります。
どうやって一意のインデックスを取得するにはよいですか?内部コンテナの初期サイズも素数にすることで。つまり、基本的に、primeが関係するのは、IDオブジェクトに使用し、内部コンテナー内のインデックスを見つけるという、固有の番号を生成するという固有の特性を持っているためです。
例:
key = "key"
値= "値"
uniqueId = "k" * 31 ^ 2 +
"e" * 31 ^ 1` +
"y"
一意のIDにマップします
今、私たちは価値のためにユニークな場所を望んでいます-それで私たちは
uniqueId % internalContainerSize == uniqueLocationForValue
、仮定internalContainerSize
も素数です。
私はこれが単純化されていることを知っていますが、私は一般的な考えを通過したいと思っています。
素数べき係数に関する「数学の性質」は、それらが有限体の 1つのビルディングブロックであることです。他の2つのビルディングブロックは、加算と乗算の演算です。素数係数の特別な特性は、それらが「正規の」加算および乗算演算で有限体を形成することであり、係数に適用されます。これは、すべての乗算が素数を法とする異なる整数にマッピングされるため、すべての加算が行われることを意味します。
素数係数は次の理由で有利です。
ただし、これらには大きな欠点があり、整数の除算が必要です。これは、最近のCPUでも、多くの(〜15-40)サイクルかかります。約半分の計算で、ハッシュが非常によく混ざっていることを確認できます。2つの乗算とxorshift演算は、プライムモドゥルスよりもよく混合されます。次に、どのハッシュテーブルサイズも使用でき、ハッシュリダクションは最も高速です。2つのテーブルサイズの合計で合計7つの演算が行われ、任意のサイズで約9つの演算が実行されます。
最近、最速のハッシュテーブル実装の多くを調べましたが、それらのほとんどは素数モジュライを使用していません。
この質問は、ハッシュテーブルが2の累乗ではなく、素数サイズの配列を使用する必要がある理由というより適切な質問とマージされました。 、glibcのように、素数サイズの配列を使用します。まだありません。
一般に、2つのテーブルの累乗ははるかに高速です。サイズがnの(「先行ゼロのカウント」)をh % n => h & bitmask
介してビットマスクを計算できる高価ながありclz
ます。モジュロ関数は、論理よりも約50倍遅い整数除算を行う必要がありますand
。Lemireのhttps://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/を使用するなど、モジュロを回避するためのいくつかのトリックがありますが、一般的に高速ハッシュテーブルはパワーを使用しますの2、および安全なハッシュテーブルは素数を使用します。
なんでそうなの?
この場合のセキュリティは、衝突解決戦略への攻撃によって定義されます。これは、ほとんどのハッシュテーブルでは、リンクされた衝突リスト内の単なる線形検索です。または、より高速なオープンアドレッシングテーブルを使用して、テーブルを直接線形検索します。したがって、2つのテーブルの累乗と、JSONインターフェースによって提供されるキーのリストのサイズや順序など、テーブルに関するいくつかの内部知識があれば、使用される正しいビットの数を取得できます。ビットマスク上の1の数。これは通常10ビット未満です。また、5〜10ビットの場合、最強のハッシュ関数と最も遅いハッシュ関数を使用しても、ブルートフォースコリジョンは簡単です。32ビットまたは64ビットのハッシュ関数の完全なセキュリティはもう得られません。そしてポイントは、高速な小さなハッシュ関数を使用することであり、つぶやきや怪物などのモンスターではありません。
したがって、DNSリゾルバやプログラミング言語など、ハッシュテーブルへの外部インターフェイスを提供する場合は、そのようなサービスをDOS化するのが好きな悪用者を気にする必要があります。そのような人々にとって、はるかに簡単な方法であなたの公共サービスをシャットダウンすることは通常より簡単ですが、それは実際に起こりました。だから人々は気にしました。
したがって、このような衝突攻撃を防ぐための最良の選択肢は、
1)プライムテーブルを使用する
2)実際の攻撃に対してより優れた対策を使用し、2つのサイズの高速パワーを使用します。
私が説明したように、より安全なハッシュ関数がそのような攻撃を防ぐのを助けるという広く通じた神話があります、それは間違っています。低ビットのみではセキュリティはありません。これは素数サイズのテーブルでのみ機能しますが、これは2つの最も遅いメソッドの組み合わせ、つまり低速ハッシュと低速素数モジュロを使用します。
ハッシュテーブルのハッシュ関数は、主に小さく(傾斜可能)かつ高速である必要があります。セキュリティは、衝突での線形検索を防ぐことからのみ得ることができます。また、一部の値に影響されないような(乗算を使用する場合の\ 0など)のような、ひどく悪いハッシュ関数を使用しないでください。
ランダムシードを使用することも良い選択肢です。人々は最初にそれを使い始めましたが、テーブルの十分な情報があれば、ランダムシードでもあまり役に立たず、動的言語は通常、他の方法でシードを取得するのは簡単です。既知のメモリ位置。
function eratosthenes(n) {
function getPrime(x) {
var middle = (x-(x%2))/2;
var arr_rest = [];
for(var j=2 ; j<=middle;j++){
arr_rest.push(x%j);
}
if(arr_rest.indexOf(0) == -1) {
return true
}else {
return false
}
}
if(n<2) {
return []
}else if(n==2){
return [2]
}else {
var arr = [2]
for(var i=3;i<n;i++) {
if(getPrime(i)){
arr.push(i)
}
}
}
return arr;
}