ハッシュテーブル — O(1) で見つける魔法と、衝突との戦い
「この鍵はどこ?」を、探さずに計算で一発で当てる。鍵から格納先の番号を作り出すハッシュ関数を軸に、なぜ平均 O(1) で出し入れできるのか、そして避けられない衝突とどう付き合うのかを、玉を落としながら体感します。
1. 鍵から住所を計算する — ハッシュ関数
配列は「何番目?」と番号で聞けば即座に取り出せます。ならば鍵そのものから番号を計算してしまえばいい。鍵を数に変え、バケット数 M で割った余りを添字にする——これが最も素朴なハッシュ関数 h(key) = key mod M です。
下のデモでは、鍵(数字の玉)が落ちてくると h が格納先のバケット番号を計算し、そこへ直行します。バケット数 M を変えたり、鍵を追加してみてください。
2. ぶつかったら数珠つなぎ — チェイン法と探索
違う鍵でも計算結果が同じ番号になることがあります。これが衝突(コリジョン)。バケットは M 個しかないのに鍵は無限に作れるので、衝突は原理的に避けられません(鳩の巣原理)。
いちばん素直な対処がチェイン法:同じバケットに来た鍵を連結リストでつなぐだけ。探すときは h で目的のバケットへ飛び、あとはその短い鎖だけを順にたどります。下で鍵を選ぶと、探索の様子が動きます。
3. 詰め込みすぎると衝突が増える — 負荷率 α
速さを保てるかは、どれだけ混んでいるかで決まります。鍵の数 n をバケット数 M で割った値を負荷率 α = n / M と呼び、これがそのまま1バケットあたりの平均の鎖の長さになります。
下で鍵の数 n とバケット数 M を動かしてみてください。α が上がるほど鎖(=オレンジの衝突玉)がぐんぐん積み上がっていくのが分かります。
4. なぜ速い? — 配列の線形探索と競争
並べ替えていない配列から目的の鍵を探すには、先頭から1個ずつ照合するしかありません(線形探索、平均 n/2 回)。ハッシュテーブルは h で目的のバケットへ一足飛びし、短い鎖を数回見るだけ。同じ鍵を同時に探させて、歩数を比べてみましょう。
dict/HashMap/set はこれらの上に、良質なハッシュ関数とリハッシュを組み合わせて平均 O(1) を実現している。用途で使い分けもあり、改ざん検知や暗号で使う暗号学的ハッシュ(SHA-256 など)は「逆算・衝突が困難」という別の強さを持つ。速さ用と安全用でハッシュは目的が違う。
5. まとめ — ハッシュテーブルの設計図
- ハッシュ関数 h:鍵を配列の添字に変換し、探索を計算に置き換える。均一にばらまくほど良い。
- 衝突:避けられない。チェイン法なら同じバケットを連結リストでつなぎ、その鎖だけをたどる。
- 負荷率 α = n/M:混み具合=平均鎖長。低く保てば平均 O(1)、放置すれば O(n) に退化する。