Python で学ぶハッシュ探索法 - チェーン法とオープンアドレス法 -
ハッシュ法は、ソート済みリストからの探索だけでなく、データの追加や削除も効率よく行う手法です。
ソート済みリスト(List A)に新たにデータ(12)を追加する場面を考えます。この時の操作は以下のようになります。
- データを挿入すべき位置を特定する
- 挿入位置よりも後ろにあるデータを全て1つずつ後方にずらす
- データを挿入する
単に新規にデータを追加するコストは決して小さくありません。データ削除も同様です。
ハッシュ法
ハッシュ法は、データ追加や削除を効率よく行う手法になります。具体的には、データの剰余(ハッシュ値と呼ぶ)を元にリストを作成・管理する方法です。
- リストに入っているデータと挿入するデータを 10 で割った時の剰余を求めます。ある数値を 10 で割った時の剰余は 0 から 9 の 10 通りです。
- 各剰余に対応するハッシュ表を作成します(ハッシュ表の各要素をバケット(bucket)と呼びます)。
- 新規に追加するデータ(14)の剰余は 4 なので、ハッシュ表の 4 に対応するバケットに挿入します。データを追加する際には、あらかじめ用意したハッシュ表にデータを追加するだけなので、既存のデータを移動させる必要がありません。
衝突
さらに、15 を追加してみましょう。15 を 10 で割った剰余は 5 なので、5 のバケットに追加しようとしますが、そのバケットには既にデータが入っています。このように格納すべきバケットが重複することを衝突と言います。
衝突が発生したときの対処方法として、以下に示す2つの方法があります。
- チェーン法
同一のハッシュ値を持つ要素を線形リストで管理する - オープンアドレス法
空きバケットを見つけるまでハッシュを繰り返す
チェーン法(オープンハッシュ法)
チェーン法は、同じハッシュ値を持つデータを鎖(チェーン)状に線形リストで繋ぐ方法です。例えば、2, 12, 32 は同じ剰余「2」のバケットに属しています。「2」のバケットは、バケットに最初に追加された 2 のポインタが格納されています。そして、2 には 12 のポインタが後続のオブジェクトとして登録されています。
各データを表すノード(Node
)とチェーン法を実現するクラス(ChainHash
)を Python で表したものを以下に示します。ノードは、データと後続ノードのポインタを持ちます。
class Node
"""
各データを表すノードクラス
"""
def __init__(self, key, value, next):
self.key = key # データ(数値)
self.next = next # 後続ノードへのポインタ
class ChainHash
"""
チェーン法を実現するクラス
"""
def __init__(self):
self.capacity = 10 # ハッシュ表のサイズ
self.hash_table = [None] * self.capacity # ハッシュ表
def hashing(self, key):
"""
ハッシュ値(剰余)を求める
"""
return key % self.capacity
def add(self, key):
"""
データを追加する
"""
hash = self.hashing(key)
p = self.hash_table[hash]
if p is None:
# 対応するバケットに 1 つもデータが入っていない場合
node = Node(key, None)
self.hash_table[hash] = node
return True
else:
while p is not None:
if p.key == key:
# 既に同じデータが格納されている
return False
p = p.next
node = Node(key, None)
p.next = node
return True
データを追加する(add
関数)時は、まずデータの剰余を求めてハッシュ表を確認します。対応するバケットにまだデータが1つも追加されていない場合、今回追加するデータを使ってノードを作成してバケットに追加して終了します。
バケットに既に1つでもデータが格納されている場合、そのバケットの終端ノードを while 文で検索しています。終端ノードの next
を新しく追加するノードに変更することで、新しいデータがバケットの終端に結合したことになります。
データの検索
チェーン法のデータ検索について Python で実装した例を以下に示します。検索対象の剰余を求めて、対応するバケットの中身を1つずつ走査しています。
class ChainHash:
...
def search(self, key):
hash = self.hashing(key)
p = self.hash_table[hash]
while p is not None:
if p.key == key:
return p
p = p.next
return None # 検索失敗
データの削除
チェーン法のデータ削除について Python で実装した例を以下に示します。まずは、対象のノードを検索します。その後、そのノードを next
で参照しているノードに修正を加えます。具体的には、next
を削除ノードの next
に変更しています。
class ChainHash:
...
def delete(self, key):
hash = self.hashing(key)
p = self.hash_table[hash]
pre_node = None
while p is not None:
if p.key == key:
if pre_node is None:
self.hash_table[hash] = p.next
else:
pre_node.next = p.next
return True
pre_node = p
p = p.next
return False # 失敗(削除対象が存在しない)
オープンアドレス法
オープンアドレス法は、同じバケットに衝突したときに再ハッシュを行うことで、空いているバケットを探し出す手法です。クローズドハッシュ法とも呼ばれます。再ハッシュ関数は自由に設定することができます。例えば、「元の値に 1 を加えてからハッシュ計算をする」です。
15 を追加しようとしていますが、5 のバケットには既にデータが格納されているため、挿入できません。そこで、15 + 1 のハッシュ値である 6 のバケットにデータを格納します。
オープンアドレス法での、データの検索と削除は少し面倒です。単に、挿入データのハッシュ値を検索するだけでなく、データの再ハッシュについても検索する必要があるからです。
Discussion