ハッシュとはぐしゃぐしゃに混ぜること
今回は,
まずはハッシュ関数の用途を見てみましょう。大きく2つの用途に分かれます。
- a.データの効率的検索
- b.データの改竄検出
無関係というより正反対に見えます。それではハッシュ関数とはどんな関数なのでしょう?
- あるデータを整数値に対応
- データが同じなら,
必ず同じ数値を返す - データが異なっている場合は,
なるべく異なる数値を返す
これだけです。この定義どおりだとすると,
func identity<T:Numeric>(x:T)->T {
return x
}
つまり受け取った数値をそのまま返す関数もハッシュ関数ということになります。とくに3.に関してはなるべくどころか必ず異なる数値を返すという意味で完璧です。しかし全然ハッシュしてないのにハッシュ関数とはこれいかに?
あまり混ぜない 「.hashValue」
しかしこれが,
SwiftにはHashable
というプロトコルが用意されています。このプロトコルに準拠した型は,
var hashValue:Int
というComputed Property,
42.hashValue // 42 Double.pi.hashValue // 4614256656552045848unsafeBitCast(Double.pi, to:Int.self) // 4614256656552045848
見てのとおり,
それはとにかく,Dictionary
と呼ばれています。しかしその内部実装がハッシュテーブルであることは,Hashable
プロトコルが如実に示しています。
ということは,
リスト1 オレオレハッシュテーブル
struct NaiveHT<K:Hashable, V> {
var table:[(K,V)?]
init(count:Int) {
table = [(K,V)?](repeating:nil, count:count)
}
func index(_ key:K) -> Int {
return key.hashValue % table.count
}
subscript(key:K) -> V? {
get {
return table[index(key)]?.1
}
set {
table[index(key)] = (key, newValue!)
}
}
}
名前からわかるとおり,Dictionary
っぽく動いています。
var h = NaiveHT<Int,Int>(count: 3) h[0] // nil h[0] = 1 // 1 h[0] // 1
しかし,
h[1] = 3 h[2] = 5 h[3] = 7 h[0] // 7
h[0]
には1を代入したはずなのに,index()
を見てみましょう。.hashValue
をtable
の要素数で割っていますが,0 % 3 == 1 % 3
なのでh[3] = 7
で上書きされちゃったわけです。要は衝突
リスト2 オレオレハッシュテーブル
func collides(key:K)->Bool {
if let kv = table[index(key)] {
return kv.0 != key
}
return false
}
mutating func rehash() {
var newHT = NaiveHT<K,V>(
count:table.count * 2 + 1
)
for kv in table {
if kv != nil {
newHT[kv!.0] = kv!.1
}
}
self = newHT
}
subscript(key:K) -> V? {
get {
return table[index(key)]?.1
}
set {
if self.collides(key:key) { rehash() }
table[index(key)] = (key, newValue!)
}
}
あるいははじめから衝突があり得ないぐらい大きな配列を用意しとくか。しかしそれではメモリがいくらあっても足りなくなってしまいます。しかも今回は添字がInt
だからいいものの,String
だとしたらindex()
ではなく.hashValue
そのものが同じ値を返すことも想定しなければなりません。Int
はたかだか8バイト。それより長い文字列には必ず同じ.hashValue
を返す別の文字列が存在することは鳩ノ巣原理で明らかなのですから。
幸い再発明をしなくても,Dictionary
が組み込まれていますし,Hashable
でなければならないのはキーだけで値はなんでもありなので,.description
を実装してから.description.
を返すという逃げもあります。