データ構造とアルゴリズム
第十回 (2014年11月
28日)
ハッシュ関数とハッシュ法
http://www.sw.it.aoyama.ac.jp/2014/DA/lecture10.html
Martin J. Dürst
© 2009-14 Martin
J. Dürst 青山学院大学
目次
- 辞書の更なる効率化
- ハッシュ法の概要
- ハッシュ関数
- 激突の対処
- ハッシュの評価
- Ruby でのハッシュ
- まとめ
辞書のこれまでの実装の計算量
方法 |
探索 |
挿入 |
削除 |
整列済み配列 |
O(log n) |
O(n) |
O(n) |
整列無し配列・連結リスト |
O(n) |
O(1) |
O(n) |
平衡木 |
O(log n) |
O(log n) |
O(log n) |
直接アドレス表
(direct addressing)
- キーの値の数だけ大きい配列を用意
- 探索は
value = array[key]
で O(1)
- 挿入・置換は
array[key] = value
で O(1)
- 削除は
array[key] = nil
で
O(1)
- 応用例:
students = []
students[15812000] = "Hanako Aoyama"
問題点: 配列の大きさ
解決案: キーの変換
ハッシュ法の概要
(hashing, scatter storage technique, 挽き混ぜ法など)
- キーの値をハッシュ関数 hf
でコンパクトな空間に変換
- キーの代わりに hf(キー)
を直接アドレス表と同様に使用
- 配列は「ハッシュ表」(hash table) という
- ハッシュ関数は O(1) と想定
- 探索は
value = table[hf(key)]
で O(1)
- 挿入・置換は
table[hf(key)] =
value
で O(1)
- 削除は
table[hf(key)] = nil
で O(1)
問題点 1: ハッシュ関数の設計
問題点 2: 激突対策
ハッシュ関数の概要
(hash function)
- 目的:
- 任意のキーからできるだけ均等に分散された指数を算出
- 値を表の大きさに合わせる
- 段階:
- キーから大きい整数 (C の
int
など)
を算出
- 大きい整数から剰余演算でハッシュ表の大きさに合わせる
2. は単純なので 1. に注目
(1.だけを「ハッシュ関数」という場合が多い)
ハッシュ関数の具体例 1
(SDBM
ハッシュ関数)
int sdbm_hash(char key[])
{
int hash = 0;
while (*key) {
hash = *key++ + hash<<6 + hash<<16 - hash;
}
}
ハッシュ関数の具体例 2
(実例ではない; MurmurHash3
参照; 32ビット用)
#define ROTL32(n,by) (((n)<<(by)) | ((n)>>(32-(by))))
int too_simple_hash(int key[], int length)
{
int h = 0;
for (int i=0; i<length; i++) {
int k = key[i] * C1; // C1 は定数
h ^= ROTL32(k, R1); // R1 は定数
}
h ^= h >> 13;
h *= 0xc2b2ae35;
return h;
}
ハッシュ関数の評価
ハッシュ関数の注意点
- キーの全部分を考慮
反例: 文字列の第 3, 4 文字目のみ → 分散が悪化
- キー以外のデータは使用禁止
データの属性 (例: 合計点、値段など)
の変動により、検索が不可能
- キーの同値性を考慮
例:
大文字・小文字の同値、囲碁の定石の番面の左右対称、白黒対称
特殊なハッシュ関数
- 万能ハッシュ法 (universal hashing):
- プログラムを実行するたびに乱数で別のハッシュ関数を作成
- 実装: ハッシュ関数の定数を変更
- DOS (denial-of-service) 攻撃への対策
(参考文献)
- 完全ハッシュ関数 (perfect hash function):
- あらかじめ決まったデータの集合を対処に
- 激突が起こらないような特別なハッシュ関数
- 究極なものは占有率 1
- 使用例: プログラム言語の予約語
- 実装例: gnu gperf
(日本語)
暗号技術的ハッシュ関数
(cryptographic hash function)
- 電子署名などに使用
- 一般のハッシュ関数との相違:
- 別途の入力で同じ結果を再現するのは (ほぼ)
不可能
- そのため、出力は長い
- 計算時間は多少長くても問題なし
激突の問題と対応
- 二つのキー k1 ≠ k2 で
hf(k1) =
hf(k2) の可能性 (激突, conflict)
- 特別な対応が必要
- 主な対策:
激突対策の用語・変数
- ハッシュ表のコマ: ビン (bin)
- ビンの数が m
(剰余演算付きハッシュ関数の値と同数)
- データ項目の数が n
- 占有率 α: ビン毎の平均のデータ項目の数
(α = n/m)
- 良い (乱数に近い)
ハッシュ関数の場合、各ビンのデータ項目の数のばらつきが少ない
(ポアソン分布)
チェイン法
(chaining, 連鎖法)
- 激突する項目を連結リストに格納
- ハッシュ表の各ビン (bin) に連結リストをつなぐ
- 連結リストが短いと探索・挿入・削除が速い
- 平均の連結リストの長さは α
- 操作は全て二段階:
- hf(キー) でビンを算出
- キーで連結リスト内を操作
チェイン法の実装
開番地法
(open addressing, オープン法)
- ハッシュ表に参照だけでなく、キーとデータも格納
- 激突の場合、次々別のビンをチェック
- i 番目のチェックの時のハッシュ関数が
ohf(key, i)
- 線形探査法 (linear probing) は ohf(key,
i) = hf(key) + i
- 二次関数探査法 (quadratic probing) は
ohf(key, i) =
hf(key) + c1 i +
c2 i2
- その他、様々な方法
- 占有率は 0 と 1 の間; 1 に近い場合、激突が急増
- 問題点: 削除が困難
ハッシュ表の探索・挿入・削除の計算量
チェイン法の場合、平均
- ハッシュ関数の計算
- キーの長さに依存
- キーの長さが一定の場合、O(1)
- ビン内の探索
- 占有率に依存
- 占有率が一定以下の場合、O(1)
- 最悪は O(n)
が、ハッシュ関数の選択により回避可能
ハッシュ表の拡大・縮小
- ハッシュ法の効率は表の占有率に依存
- 項目が増える場合、ハッシュ表の拡大が必要
- 項目が減るとハッシュ表の縮小が好ましい
- 拡大・縮小は別のハッシュ表ヘの再挿入で実現
(剰余演算の法を変更)
- 拡大・縮小は O(n)
で重い
拡大の計算量の分析
- 挿入・削除ごとに拡大・縮小すると効率が悪い
- 少数で大胆な拡大:
- データ数が二倍になった時点で表を二倍に
- n 個 (n=2x)
の項目の挿入の場合、計算時間は
2 + 4 + 8 + ... + n/2 + n < 2n =
O(n)
- 一項目当たりの挿入の計算量は
O(n)/n = O(1)
(償却分析 (amortized analysis) の簡単な一例)
ハッシュ法の評価
利点:
- (平均に) 一定時間で探索・挿入・削除が可能
- 良い効率
- キーに順序 (大小) が不要
- 幅広い応用
問題点:
- 整列は別途必要
- 近似探索は不可能
- ハッシュ表の拡大・縮小の時に時間がかかる
辞書の実装の比較
方法 |
探索 |
挿入 |
削除 |
整列 |
整列済み配列 |
O(log n) |
O(n) |
O(n) |
O(n) |
整列無し配列・連結リスト |
O(n) |
O(1) |
O(1) |
O(n log n) |
平衡木 |
O(log n) |
O(log n) |
O(log n) |
O(n) |
ハッシュ表 |
O(1) |
O(1) |
O(1) |
O(n log n) |
Ruby の Hash
クラス
(Perl: hash
; Java: HashMap
; Python:
dict
)
- 辞書の実装にハッシュ表を使用するため、
多くのプログラム言語で辞書をハッシュという
- 新規作成:
hash = {}
または hash =
Hash.new
- 初期化:
months = {'January' => 31, 'February' => 28,
'March' => 31, ... }
- 挿入・変更:
months['February'] = 29
- 探索:
this_month_length = months[this_month]
- 多言語より便利な機能が多い (発表参照)
Ruby でのハッシュの実装
- University of California Berkeley の Peter Moore による (1989年)
- ソース: st.c
- チェイン法使用
- 占有率 5.0 を上限におおよそ二倍に拡大
(
ST_DEFAULT_MAX_DENSITY
)
- ハッシュ表の大きさは2の倍数 (
next_pow2
)
(以前は素数)
- 削除が多くても縮小は行わない
- Ruby 内部にも使用:
- クラスなどグローバルな識別子の管理
- クラスごとのメソッドの管理
- オブジェクトごとのインスタンス変数の管理
今回のまとめ
- ハッシュ法はハッシュ関数による辞書の実装
- 要点: ハッシュ関数の選択と激突対策の方法
- 良い効率
- 幅広い応用