データ構造とアルゴリズム
第九回
(2010年12月3日)
ハッシュ関数とハッシュ法
http://www.sw.it.aoyama.ac.jp/2010/DA/lecture9.html
Martin J. Dürst
© 2008-10 Martin
J. Dürst 青山学院大学
目次
- B+木の大きさと効率
- 辞書の更なる効率化
- ハッシュ法の概要
- ハッシュ関数
- 激突の対処
- ハッシュの評価
- Ruby でのハッシュ
- まとめ
B 木
(B-tree)
- 2-3-4 木の変形と考えられる
- 1ページを1ノードとして使用
- ページごとのキーの容量を最大化
- ページごとの最低のキーの数は容量の半分程度
- B+木ではキー以外のデータを一番下の層だけに配置
- それによって、内部節のキーの数、子供の数を増やし、高さを最低限に抑える
B+木関連の変数の定義
- n: データ項目の総数
- Lp: ページの大きさ
- Lk: キーの大きさ
- Ld: (一項目の) データの大きさ
(キーを抜く)
- Lpp: ページ番号 (ポインタ)
の大きさ
- αmin: 最低占有率 (普通は
0.5)
B+木: ページごとの項目数
(以下、整数に丸める部分は省略)
- dmax =
Lp / (Lk
+ Ld)
(葉の最大項目数)
- dmin =
dmax
αmin
(葉の最低項目数)
- kmax =
Lp / (Lk
+ Lpp)
(内部節の最大の子供の数)
- kmin =
kmax
αmin
(内部節の最低の子供の数)
B+木のノードの数
- Ndmax = n /
dmin
(葉の最大数)
- Ndmin = n /
dmax
(葉の最低数)
- Nkmax =
Ndmax / kmin
+ Ndmax /
kmin2+
...
(内部節の最大の数)
- Nkmin = Ndmin
/ kmax +
Ndmin /
kmax2 + ...
(内部節の最低の数)
辞書のこれまでの実装
方法 |
探索 |
挿入 |
削除 |
整列済み配列 |
O(log n) |
O(n) |
O(n) |
整列無し配列・連結リスト |
O(n) |
O(1) |
O(1) |
平衡木 |
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)
問題点: 配列の大きさ
ハッシュ法の概要
(hashing, scatter storage technique, 挽き混ぜ法など)
- キーの値をハッシュ関数 hf()
でコンパクトな空間に変換
- キーの代わりに hf(キー)
を直接アドレス表と同様に使用
- 配列は「ハッシュ表」(hash table) という
- 探索は
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. に注目
ハッシュ関数の注意点
- キーの全部分を考慮
反例: 文字列の第 3, 4 文字だけ →
分散が悪い可能性が高い
- キー以外のデータは関数に使わない
データの変動する属性 (例: 合計点、値段など)
を含むと検索できなくなる
- キーの同値性を考慮
例:
大文字・小文字の同値、囲碁の定石の番面の左右対象、白黒対象
特殊なハッシュ関数
- 万能ハッシュ法 (universal hashing):
- プログラムを実行するたびに乱数で別のハッシュ関数を作成
- 完全ハッシュ関数 (perfect hash function):
- あらかじめ決まったデータの集合を対処に
- 激突が起こらないような特別なハッシュ関数
- 究極なものは占有率 1
- 使用例: プログラム言語の予約語
- 実装例: gnu gperf
(日本語)
暗号技術的ハッシュ関数
(cryptographic hash function)
- 電子署名などに使用
- 一般のハッシュ関数との相違:
- 別途の入力で同じ結果を再現するのはほぼ不可能
- 計算時間は多少長くても問題なし
激突の問題と対応
- 二つのキー k1 と k2 で
hf(k1) =
hf(k2) の可能性 (激突)
- 同じ場所を複数のデータで使うのは不可能、特別な対応が必要
- チェイン法:
激突する項目を連結リストに格納
- 開番地法:
激突の場合、更なるハッシュ関数で別の場所を探す
チェイン法
(chaining, 連鎖法)
- データの数を n、ハッシュ表の大きさを
m とすると、α = n/m を占有率という
- 連結リストが短い時に探索・挿入・削除が速い
- ハッシュ表の各ビン (bin) に連結リストをつなぐ
- 平均の連結リストの長さは α
- ハッシュ関数がいいとリストの長さのばらつきが少ない
- 操作は全て二段階:
- キーからハッシュ関数を使ってビンを算出
- キーで連結リスト内を操作
開番地法
(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
- 問題点: 削除が対応しにくい
ハッシュ表の拡大・縮小
- ハッシュ法の効率は表の大きさによる
- 項目が増えるとハッシュ表の拡大が不可欠
- 項目が減るとハッシュ表の縮小が好ましい
- 拡大・縮小は 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の倍数より次に大きい素数
(
primes[]
)
- 削除が多くても縮小は行わない
- Ruby の実装内の使用:
- クラスなどグローバルな識別子の探索
- クラスごとのメソッドの探索
- オブジェクトごとのインスタンス変数の探索
今回のまとめ
- ハッシュ法はハッシュ関数による辞書の実装
- 良い効率
- 幅広い応用