Menu
Coddy logo textTech

ハッシュテーブル

最終更新

ハッシュテーブルは項目をバケットの配列に格納します。キーを配置または検索するには、キーをハッシュ関数に通し、その結果をバケット数で割った余りを取ります。これにより O(1) でバケットのインデックスが得られます。上の再生を押すと、各キーがバケットにハッシュされて格納され、検索が正しいバケットへ直接ジャンプする様子を見られます。

2つのキーが同じバケットに入ると(衝突)、この可視化は分離連鎖法を使います。各バケットは小さなリストを保持し、衝突したキーはそこに追加されます。テーブルが満杯になりすぎず(負荷率が低く)、ハッシュがキーをうまく分散させている限り、連鎖は短いままで、挿入・検索・削除は平均 O(1) になります。

時間計算量

操作平均最悪の場合
挿入O(1)O(n)(すべてのキーが衝突)
検索O(1)O(n)
削除O(1)O(n)
空間O(n)O(n)

主要な概念

用語意味
ハッシュ関数キーをバケットのインデックスに対応づける
衝突2つのキーが同じバケットに入る
分離連鎖法各バケットが衝突したエントリのリストを保持する
開番地法代替案: 次の空きスロットを探索する
負荷率エントリ数 / バケット数 - リサイズを決める

具体例

hash(k) = k % 7 を使い、キー 2034913 を7バケットのテーブルに挿入:

ステップ構造動作
20 を挿入バケット6: [20]20 % 7 = 6、バケット6は空、20 を格納
34 を挿入バケット6: [20, 34]34 % 7 = 620 と衝突、34 を連鎖に追加
9 を挿入バケット2: [9]9 % 7 = 2、バケット2は空、9 を格納
13 を挿入バケット6: [20, 34, 13]13 % 7 = 6、衝突、13 をバケット6の連鎖に追加
34 を検索バケット6: [20, 34, 13]34 % 7 = 6、連鎖を走査: 20 は違う、34 が一致 - 発見

ハッシュテーブルを使うべき場面

使うべき場面避けるべき場面
キーによる挿入・検索・削除を平均 O(1) で行いたいキーをソート順に保つ必要がある - 平衡BSTを使う
キーに有用な順序がなく、完全一致の存在確認だけを行う範囲クエリや最も近いキーの検索が必要
バケットと低い負荷率のために追加のメモリを割けるメモリが極端に厳しく、1バイトも惜しい
キーの型に対して良いハッシュ関数が存在する最悪ケースのレイテンシに上限が必要 - 衝突により O(n) になる

Hash Tableのコード

Python, JavaScript, Java, C++, Cによるクリーンで実行可能なHash Tableの実装です。言語を選んでコードをコピーするか、Coddyプレイグラウンドに読み込んだ状態で開けます。

PythonでのHash Tableのコード

Python
1class HashTable:2    def __init__(self, size=8):3        self.size = size4        self.buckets = [[] for _ in range(size)]5
6    def _index(self, key):7        # Hash the key to a bucket; different keys can collide8        return sum(ord(ch) for ch in key) % self.size9
10    def set(self, key, value):11        bucket = self.buckets[self._index(key)]12        for i, (k, _) in enumerate(bucket):13            if k == key:14                bucket[i] = (key, value)  # update existing key15                return16        bucket.append((key, value))  # chain on collision17
18    def get(self, key):19        for k, v in self.buckets[self._index(key)]:20            if k == key:21                return v22        raise KeyError(key)23
24    def delete(self, key):25        bucket = self.buckets[self._index(key)]26        for i, (k, _) in enumerate(bucket):27            if k == key:28                del bucket[i]29                return30        raise KeyError(key)31
32
33table = HashTable()34table.set("apple", 3)35table.set("banana", 7)36table.set("cherry", 5)37
38print("apple  ->", table.get("apple"))39print("banana ->", table.get("banana"))40table.set("apple", 10)41print("apple  ->", table.get("apple"))42table.delete("banana")43print("bucket sizes:", [len(b) for b in table.buckets])
このコードをPythonプレイグラウンドで実行

ハッシュテーブル よくある質問

ハッシュ衝突とは何で、どう解決しますか?
衝突は、2つの異なるキーが同じバケットにハッシュされたときに起こります。一般的な2つの解決策は、分離連鎖法(各バケットがリストを保持し、衝突したキーを追加する - ここで示している方法)と開番地法(配列内の次の空きスロットを探索する)です。どちらも検索の正しさを保ちますが、メモリレイアウトと高負荷時の性能が異なります。
ハッシュテーブルの時間計算量は?
ハッシュ関数がキーを均等に分散させ、負荷率が低く保たれている場合、挿入・検索・削除は平均 O(1) です。最悪の場合 - すべてのキーが1つのバケットに衝突する場合 - それらは O(n) に劣化します。だからこそ良いハッシュ関数とリサイズが重要です。
負荷率とは何ですか?
負荷率は、格納されているエントリ数をバケット数で割ったものです。これが大きくなると連鎖が長くなり操作が遅くなるため、ほとんどのハッシュテーブルは 0.75 のようなしきい値を超えるとリサイズ(より大きな配列へ再ハッシュ)します。
二分探索木の代わりにハッシュテーブルを使うべきなのはいつですか?
完全一致の検索だけが必要で、順序の要件なしに平均 O(1) の速度が欲しいときはハッシュテーブルを使います。ソートされたキー、範囲クエリ、前者/後者の検索が必要なときは平衡二分探索木を使います。これらはハッシュテーブルでは効率的に行えません。BSTは保証された O(log n) 操作を提供する一方、ハッシュテーブルはその保証をより速い平均性能と引き換えにします。
分離連鎖法と開番地法の違いは何ですか?
分離連鎖法は衝突したキーをバケットごとのリストに格納するため、1つのバケットが多くのエントリを保持でき、テーブルが本当に満杯になることはありません。開番地法はすべてを配列自体に保持し、衝突時に次の空きスロットを探索します。これはキャッシュに優しい一方、負荷率が1に近づくと急激に劣化し、削除の慎重な扱いが必要です。連鎖法はより高い負荷率を許容し、開番地法はメモリをよりコンパクトに使います。
ハッシュテーブルがキーを挿入順やソート順に保つことに頼れないのはなぜですか?
ハッシュ関数はクラスタリングを避けるために意図的にキーをバケット全体に散らすため、反復順序は挿入順やソート順ではなくバケットの配置を反映します。順序が必要なら、順序付きマップ/木、または挿入順を保持するマップのような構造を使ってください(例えばPythonの dict は挿入順を保持しますが、それは言語の保証であり、ハッシュテーブル本来の性質ではありません)。言語が明示的に約束しない限り、反復順序がキーを挿入した順と一致すると決して仮定しないでください。
Coddy programming languages illustration

Coddy でアルゴリズムをマスターしよう

始める