web-dev-qa-db-ja.com

ハッシュテーブルの拡張が通常サイズを2倍にすることで行われるのはなぜですか?

私はハッシュテーブルについて少し調査しましたが、特定の数のエントリがある場合(最大または75%などの負荷係数を介して)、ハッシュテーブルを拡張する必要があるという経験則を繰り返し実行します。

ほとんどの場合、ハッシュテーブルのサイズを2倍(または2倍プラス1、つまり2n + 1)にすることをお勧めします。しかし、私はこれの正当な理由を見つけることができませんでした。

たとえば、サイズを25%増やしたり、次の素数や次のk個の素数(3など)のサイズに増やしたりするのではなく、サイズを2倍にするのはなぜですか?

少なくともハッシュ関数がユニバーサルハッシュなどのモジュラスを使用している場合は、素数である初期ハッシュテーブルサイズを選択することをお勧めすることはすでに知っています。そのため、通常は2nではなく2n + 1を実行することをお勧めします(例: http://www.concentric.net/~Ttwang/tech/hashsize.htm

しかし、私が言ったように、新しいハッシュテーブルのサイズを選択する他の方法よりも、なぜダブリングまたはダブリングプラスワンが実際に良い選択であるのかについての実際の説明は見ていません。

(はい、ハッシュテーブルに関するウィキペディアの記事を読みました:) http://en.wikipedia.org/wiki/Hash_table

38
Chirael

たとえば、サイズ変更が一定の増分で行われた場合、ハッシュテーブルは「償却された一定時間の挿入」を要求できませんでした。その場合、サイズ変更のコスト(ハッシュテーブルのサイズとともに増加します)により、挿入する要素の総数に対して1回の挿入のコストが線形になります。サイズ変更はテーブルのサイズとともにますます高価になるため、挿入の償却コストを一定に保つために「ますます頻繁に」行わなければなりません。

ほとんどの実装では、サイズ変更の前に事前に固定された境界まで(すべて許容値である0.5から3の間の任意の場所)、平均バケット占有率を増やすことができます。この規則では、サイズを変更した直後に、バケットの平均占有率はその範囲の半分になります。倍増によるサイズ変更により、バケットの平均占有率が幅* 2の帯域に保たれます。

サブノート:統計的クラスタリングのため、多くのバケットに最大で1つの要素(キャッシュサイズの複雑な影響を無視して見つけるための最大速度)を持たせたい場合は、平均バケット占有率を0.5から低くする必要があります。 3(無駄なスペースに対応する)空のバケットの最小数が必要な場合。

36
Pascal Cuoq

私はこのサイトで成長戦略に関する非常に興味深い議論を読んだことがあります...それを再び見つけることはできません。

_2_が一般的に使用されますが、それが最良の値ではないことが実証されています。よく引用される問題の1つは、アロケータースキーム(2ブロックの累乗を割り当てることが多い)にうまく対応できないことです。これは、常に再割り当てが必要であるのに対し、実際には同じブロックに少数が再割り当てされる可能性があるためです(インプレース成長をシミュレート)。したがって、より高速になります。

したがって、たとえば、_VC++_標準ライブラリは、メーリングリストでの広範な議論の後、_1.5_の成長係数を使用します(最適なメモリ割り当て戦略が使用されている場合は、黄金数であることが理想的です)。 。理由は説明されています ここ

他のベクター実装が2以外の成長因子を使用しているかどうかに興味があり、VC7が1.5または2を使用しているかどうかも知りたいです(ここにはそのコンパイラがないため)。

1.5から2を優先する技術的な理由があります。具体的には、1+sqrt(5)/2未満の値を優先します。

ファーストフィットメモリアロケータを使用していて、ベクトルに徐々に追加しているとします。次に、再割り当てするたびに、新しいメモリを割り当て、要素をコピーしてから、古いメモリを解放します。それはギャップを残し、最終的にそのメモリを使用できるようになるといいでしょう。ベクトルの成長が速すぎると、使用可能なメモリに対して常に大きすぎます。

成長因子が>= 1+sqrt(5)/2の場合、新しいメモリは常に、残された穴に対して大きすぎます。 < 1+sqrt(5)/2の場合、新しいメモリは最終的には収まります。したがって、1.5はメモリをリサイクルできるほど十分に小さいです。

確かに、成長因子が_>= 2_の場合、新しいメモリは、これまでに残された穴に対して常に大きすぎます。 _< 2_の場合、新しいメモリは最終的には収まります。おそらく_(1+sqrt(5))/2_の理由は...

  • 初期割り当てはsです。
  • 最初のサイズ変更は_k*s_です。
  • 2番目のサイズ変更は_k*k*s_で、これは_k*k*s <= k*s+s_の場合、つまりk <= (1+sqrt(5))/2の場合に穴に適合します。

...穴はできるだけ早くリサイクルできます。

以前のサイズを保存することで、フィボナシーに成長する可能性があります。

もちろん、メモリ割り当て戦略に合わせて調整する必要があります。

8
Matthieu M.

ハッシュコンテナに固有のサイズを2倍にする理由のひとつは、コンテナの容量が常に2の累乗である場合、ハッシュをオフセットに変換するために汎用モジュロを使用する代わりに、ビットシフトで同じ結果を達成できるためです。モジュロは、整数の除算が遅いのと同じ理由で遅い演算です。 (プログラムで他に何が起こっているのかという文脈で整数除算が「遅い」かどうかはもちろんケースに依存しますが、他の基本的な整数演算よりも確かに遅いです。)

4
Praxeolitic

いくつあるかわからない場合objects最終的に使用することになります(たとえばN)、
ログを記録するスペースを2倍にする2最大でN個の再割り当て。

proper initial "n"を選択すると、オッズが上がると思います
2 * n + 1は、後続の再割り当てで素数を生成します。

3

Vector/ArrayList実装の場合と同じ理由が、サイズを2倍にする場合にも当てはまります。 この回答 を参照してください。

3
Pete Kirkham

あらゆるタイプのコレクションを拡張するときにメモリを2倍にすることは、メモリの断片化を防ぎ、頻繁に再割り当てする必要がないようにするためによく使用される戦略です。あなたが指摘するように、要素の素数を持つ理由があるかもしれません。アプリケーションとデータを知っていると、要素数の増加を予測して、2倍にするよりも別の(大きいまたは小さい)増加係数を選択できる場合もあります。

ライブラリにある一般的な実装は、まさに次のとおりです。一般的な実装。彼らは、さまざまな異なる状況で合理的な選択であることに焦点を当てる必要があります。コンテキストを知っている場合、ほとんどの場合、より専門的で効率的な実装を作成することが可能です。

3
Anders Abel