クラスのhashCode()
メソッドでプライムが使用されるのはなぜだろうと思っていましたか?たとえば、Eclipseを使用してhashCode()
メソッドを生成する場合、常に使用される素数31
があります。
public int hashCode() {
final int prime = 31;
//...
}
参照:
Hashcodeの優れた入門書と、私が見つけたハッシュの仕組みに関する記事(C#ですが、概念は移転可能です): Eric LippertのガイドラインとGetHashCode()のルール
乗算する数と挿入するバケットの数が必要なため、直交素因数分解が必要です。
挿入するバケットが8個あるとします。乗算に使用している数値が8の倍数である場合、挿入されるバケットは最下位のエントリ(乗算されていないエントリ)によってのみ決定されます。同様のエントリが衝突します。ハッシュ関数には適していません。
31は十分に大きい素数であり、バケットの数はそれで割り切れそうにありません(実際、現代のJava HashMap実装はバケットの数を2の累乗に保ちます)。
ハッシュバケット間でデータを最適に分散するために素数が選択されます。入力の分布がランダムで均等に分散している場合、ハッシュコード/モジュラスの選択は重要ではありません。入力に特定のパターンがある場合にのみ影響します。
これは、メモリロケーションを処理する場合によくあります。たとえば、すべての32ビット整数は、4で割り切れるアドレスに揃えられます。下の表をチェックして、素数と非素数のモジュラスを使用した効果を視覚化します。
Input Modulo 8 Modulo 7
0 0 0
4 4 4
8 0 1
12 4 5
16 0 2
20 4 6
24 0 3
28 4 0
プライムモジュラスと非プライムモジュラスを使用する場合、ほぼ完全な分布に注意してください。
ただし、上記の例は大きく工夫されていますが、一般的な原則は、入力のパターンを扱う場合、素数モジュラスを使用すると最適な分布が得られることです。
有効なJava 2nd Edition数学の問題を回避し、31を選択する理由は:
以下は、からの完全な引用です。項目9:hashCode
をオーバーライドするときは、常にequals
をオーバーライドします。
奇数の素数であるため、値31が選択されました。偶数で乗算がオーバーフローした場合、2の乗算はシフトに相当するため、情報は失われます。プライムを使用する利点はそれほど明確ではありませんが、伝統的です。
31というニースの特性は、乗算をシフト( §15.19 )と減算で置き換えることができることです。
31 * i == (i << 5) - i
最新のVMは、この種の最適化を自動的に行います。
このアイテムのレシピはかなり良いハッシュ関数を生成しますが、最新のハッシュ関数は生成しません。また、リリース1.6以降、Javaプラットフォームライブラリはそのようなハッシュ関数を提供しません。そのようなハッシュ関数を書くことは、数学者と理論的なコンピューター科学者に任せるのが最善の研究課題です。
おそらくプラットフォームの今後のリリースでは、平均的なプログラマーがそのようなハッシュ関数を作成できるように、クラスとユーティリティメソッドに最新のハッシュ関数が提供されるでしょう。それまでの間、この項目で説明する手法は、ほとんどのアプリケーションに適しているはずです。
むしろ単純化すると、多数の除数を持つ乗数を使用すると、より多くの ハッシュ衝突 が生じると言えます。効果的なハッシュのために、衝突の数を最小限に抑えたいため、除数の少ない乗数を使用しようとします。定義による素数には、厳密に2つの正の約数があります。
コンパイラーが乗算を5ビット左シフトして最適化し、値を減算できるように31が選択されたと聞きました。
最初に2 ^ 32を法とするハッシュ値(int
のサイズ)を計算するため、2 ^ 32に対して比較的素数のものが必要です(比較的素数とは、一般的な除数がないことを意味します)。それにはどんな奇数でもかまいません。
次に、指定されたハッシュテーブルでは、通常、インデックスはハッシュテーブルのサイズを法とするハッシュ値から計算されるため、ハッシュテーブルのサイズに比較的素なものが必要です。多くの場合、ハッシュテーブルのサイズはそのために素数として選択されます。 Javaの場合、Sunの実装は、サイズが常に2のべき乗であることを確認するため、ここでも奇数で十分です。衝突をさらに制限するために、ハッシュキーの追加のマッサージもいくつかあります。
ハッシュテーブルと乗数に共通の因子n
がある場合の悪影響は、特定の状況ではハッシュテーブルの1/nエントリのみが使用されることです。
素数が使用される理由は、データが特定のパターンを示すときの衝突を最小限に抑えるためです。
まず最初に:データがランダムな場合、素数は必要ありません。任意の数に対してmod操作を実行できます。モジュラスの各可能な値に対して同じ数の衝突が発生します。
しかし、データがランダムでない場合、奇妙なことが起こります。たとえば、常に10の倍数である数値データを考えます。
Mod 4を使用すると、次のことがわかります。
10 mod 4 = 2
20 mod 4 = 0
30 mod 4 = 2
40 mod 4 = 0
50 mod 4 = 2
したがって、モジュラスの3つの可能な値(0、1、2、3)から0と2のみが衝突します。これは悪いことです。
7のような素数を使用する場合:
10 mod 7 = 3
20 mod 7 = 6
30 mod 7 = 2
40 mod 7 = 4
50 mod 7 = 1
等
また、5は良い選択ではありませんが、5は素数であることに注意してください。理由は、すべてのキーが5の倍数であるためです。通常は十分です。
したがって、繰り返しの側では、素数が使用される理由は、ハッシュ関数の衝突の分布におけるキーのパターンの影響を中和するためです。
一般に、特に低エントロピーキーの場合、ハッシュバケット間でデータをより均等に分散するのに役立ちます。
31は、ハッシュデータ型としてintを使用するJava HashMapにも固有です。したがって、最大容量は2 ^ 32です。大きなフェルマー素数またはメルセンヌ素数を使用しても意味がありません。