平方探测法の実装と注意点:ハッシュテーブル衝突解決の深層

平方探测法の実装と注意点:ハッシュテーブル衝突解決の深層

ハッシュテーブルにおける衝突処理手法として、平方探査法はその特異な動作特性から多くの場面で採用されています。この手法は単なる線形探索とは異なり、特定の数列パターンを用いたジャンプ型検索を行うことで、データ構造の効率的な運用を実現します。しかし、実装時にはいくつかの落とし穴に注意が必要です。

平方探査法の数学的基盤

平方探査法はオープンアドレッシング方式の一種であり、衝突発生時に固定間隔ではなく、平方数に基づく可変間隔で空き位置を探します。探査シーケンスは以下のように表現されます:

d_i = 0, +1, -1, +4, -4, +9, -9, ..., +k², -k²

ここでkは探査回数、mはハッシュテーブルのサイズです。この数列の特徴は、初期位置を中心に左右対称に探査範囲を広げることにあります。

探査位置の計算式は以下の通りです:

H_i = (H(key) + d_i) mod m

負の値が発生する場合、適切な処理が必要になります。以下はその実装例です:

def calculate_probe_position(initial_hash, iteration, table_size):
    """探査位置を計算する関数"""
    square_value = ((iteration + 1) // 2) ** 2
    if iteration % 2 == 1:
        square_value = -square_value
    
    position = (initial_hash + square_value) % table_size
    # 負数の場合は正規化
    if position < 0:
        position += table_size
    return position

Pythonによる完全実装

以下は平方探査法を用いたハッシュテーブルの完全な実装です:

class SquareProbeHashTable:
    """平方探査法を使用したハッシュテーブル"""
    
    def __init__(self, capacity=13):
        """
        コンストラクタ
        Args:
            capacity: テーブル容量(素数推奨)
        """
        self.capacity = capacity
        self.storage = [None] * capacity
        self.removed_flags = [False] * capacity  # 削除済みフラグ
        self.element_count = 0
    
    def hash_function(self, key):
        """基本的なハッシュ関数"""
        return key % self.capacity
    
    def locate_position(self, target_key):
        """
        キーに対応する位置を検索
        Returns: (位置, 発見フラグ)
        """
        base_hash = self.hash_function(target_key)
        
        for attempt in range(self.capacity):
            offset = self.square_offset(attempt)
            current_pos = (base_hash + offset) % self.capacity
            
            if self.storage[current_pos] is None or self.removed_flags[current_pos]:
                if not self.removed_flags[current_pos]:
                    return current_pos, False
                else:
                    continue  # 削除済み位置をスキップ
            
            if self.storage[current_pos] == target_key and not self.removed_flags[current_pos]:
                return current_pos, True
        
        return -1, False  # テーブル満杯
    
    def square_offset(self, attempt):
        """平方オフセットを計算"""
        squared_num = ((attempt + 1) // 2) ** 2
        return squared_num if attempt % 2 == 0 else -squared_num
    
    def add_element(self, key):
        """要素追加メソッド"""
        load_factor = self.element_count / self.capacity
        if load_factor > 0.6:
            self.resize_table()
        
        pos, exists = self.locate_position(key)
        
        if pos == -1:
            raise OverflowError("テーブル容量超過")
        
        if not exists:
            self.storage[pos] = key
            self.removed_flags[pos] = False
            self.element_count += 1
            return True
        return False  # 既存要素
    
    def find_element(self, key):
        """要素検索"""
        _, found = self.locate_position(key)
        return found
    
    def remove_element(self, key):
        """要素削除(論理削除)"""
        pos, exists = self.locate_position(key)
        
        if exists:
            self.removed_flags[pos] = True
            self.element_count -= 1
            return True
        return False
    
    def resize_table(self):
        """テーブル再構成"""
        previous_storage = self.storage[:]
        previous_flags = self.removed_flags[:]
        
        new_capacity = self.find_next_prime(self.capacity * 2)
        self.capacity = new_capacity
        self.storage = [None] * new_capacity
        self.removed_flags = [False] * new_capacity
        self.element_count = 0
        
        for idx in range(len(previous_storage)):
            if previous_storage[idx] is not None and not previous_flags[idx]:
                self.add_element(previous_storage[idx])
    
    def find_next_prime(self, number):
        """次の素数を探索"""
        def check_prime(value):
            if value < 2:
                return False
            for divisor in range(2, int(value ** 0.5) + 1):
                if value % divisor == 0:
                    return False
            return True
        
        while not check_prime(number):
            number += 1
        return number

実装上の重要な考慮事項

この実装では以下の点に特に注意しています:

  • 削除フラグ管理:物理的な削除を行うと探査パスが破壊されるため、削除済みフラグで論理削除を実現しています。
  • 負荷率制御:負荷率が0.6を超えると再構成を実行し、性能維持を図ります。
  • テーブルサイズ選択:4k+3形式の素数を選択することで、すべての位置が探査可能になることを保証します。

二次クラスタリング問題

平方探査法は線形クラスタリングを軽減する一方で、異なるキーが同じ探査シーケンスを持つことによる二次クラスタリングという新たな課題を抱えています。これは異なる初期位置を持つキーが特定の位置で交差し、結果としてこれらの位置が新たなクラスタとなる現象です。

例えば、異なるハッシュ値を持つ複数のキーが同じ位置で衝突すると、それ以降の探査パスが一致する可能性があります。このような場合、効率的なデータ配置が困難になり、パフォーマンスが低下する可能性があります。

タグ: hash-table quadratic-probing collision-resolution data-structures Algorithms

6月18日 19:35 投稿