--- comments: true --- # 6.3 ハッシュアルゴリズム 前の 2 節では、ハッシュテーブルの動作原理とハッシュ衝突の処理方法を紹介しました。しかし、オープンアドレス法であれ連鎖方式であれ、**それらが保証できるのは衝突発生時でもハッシュテーブルが正常に動作することだけであり、ハッシュ衝突そのものを減らすことはできません**。 ハッシュ衝突があまりにも頻繁に発生すると、ハッシュテーブルの性能は急激に劣化します。下図のように、連鎖方式のハッシュテーブルでは、理想的な場合にはキーと値のペアが各バケットに均等に分布し、最良の検索効率を達成します。最悪の場合には、すべてのキーと値のペアが同じバケットに格納され、時間計算量は $O(n)$ に劣化します。 { class="animation-figure" }
図 6-8 ハッシュ衝突の最良ケースと最悪ケース
**キーと値のペアの分布はハッシュ関数によって決まります**。ハッシュ関数の計算手順を思い出すと、まずハッシュ値を計算し、その後で配列長に対して剰余を取ります。 ```shell index = hash(key) % capacity ``` 上の式から分かるように、ハッシュテーブルの容量 `capacity` が固定されているとき、**出力値を決めるのはハッシュアルゴリズム `hash()` です**。したがって、それがキーと値のペアのハッシュテーブル内での分布も決定します。 これは、ハッシュ衝突の発生確率を下げるには、ハッシュアルゴリズム `hash()` の設計に注目すべきだということを意味します。 ## 6.3.1 ハッシュアルゴリズムの目標 「高速かつ安定した」ハッシュテーブルというデータ構造を実現するために、ハッシュアルゴリズムは次の特徴を備える必要があります。 - **決定性**:同じ入力に対して、ハッシュアルゴリズムは常に同じ出力を生成しなければなりません。そうして初めて、ハッシュテーブルの信頼性が保たれます。 - **高効率**:ハッシュ値の計算過程は十分に高速であるべきです。計算コストが小さいほど、ハッシュテーブルの実用性は高くなります。 - **均一分布**:ハッシュアルゴリズムは、キーと値のペアがハッシュテーブル内に均等に分布するようにすべきです。分布が均一であるほど、ハッシュ衝突の確率は低くなります。 実際には、ハッシュアルゴリズムはハッシュテーブルの実装だけでなく、ほかの多くの分野でも広く利用されています。 - **パスワード保存**:ユーザーのパスワードを保護するために、システムは通常、平文パスワードを直接保存せず、そのハッシュ値を保存します。ユーザーがパスワードを入力すると、システムは入力内容のハッシュ値を計算し、保存済みのハッシュ値と比較します。一致すれば、そのパスワードは正しいと見なされます。 - **データ完全性検査**:送信側はデータのハッシュ値を計算してデータと一緒に送信できます。受信側は受け取ったデータのハッシュ値を再計算し、受信したハッシュ値と比較できます。両者が一致すれば、そのデータは完全だと見なされます。 暗号分野の応用では、ハッシュ値から元のパスワードを推測するといった逆解析を防ぐために、ハッシュアルゴリズムにはさらに高いレベルの安全性が求められます。 - **一方向性**:ハッシュ値から入力データに関するいかなる情報も逆算できないこと。 - **耐衝突性**:異なる 2 つの入力で同じハッシュ値になるものを見つけることが、極めて困難であること。 - **アバランシェ効果**:入力のわずかな変化が、出力の大きく予測不能な変化を引き起こすこと。 注意してほしいのは、**「均一分布」と「耐衝突性」は独立した 2 つの概念である**という点です。均一分布を満たしていても、耐衝突性を満たすとは限りません。たとえば、入力 `key` がランダムである場合、ハッシュ関数 `key % 100` は均一に分布した出力を生成できます。しかし、このハッシュアルゴリズムはあまりにも単純で、下 2 桁が同じ `key` はすべて同じ出力になります。そのため、ハッシュ値から利用可能な `key` を容易に逆算でき、結果としてパスワードが破られてしまいます。 ## 6.3.2 ハッシュアルゴリズムの設計 ハッシュアルゴリズムの設計は、多くの要素を考慮しなければならない複雑な問題です。しかし、要求の高くない場面であれば、いくつかの単純なハッシュアルゴリズムを設計することもできます。 - **加算ハッシュ**:入力の各文字の ASCII コードを足し合わせ、その合計をハッシュ値とします。 - **乗算ハッシュ**:乗算の非相関性を利用し、各ラウンドで定数を掛けながら、各文字の ASCII コードをハッシュ値に累積します。 - **XOR ハッシュ**:入力データの各要素を XOR 演算で 1 つのハッシュ値に累積します。 - **回転ハッシュ**:各文字の ASCII コードを 1 つのハッシュ値に累積し、各回の累積前にハッシュ値を回転させます。 === "Python" ```python title="simple_hash.py" def add_hash(key: str) -> int: """加算ハッシュ""" hash = 0 modulus = 1000000007 for c in key: hash += ord(c) return hash % modulus def mul_hash(key: str) -> int: """乗算ハッシュ""" hash = 0 modulus = 1000000007 for c in key: hash = 31 * hash + ord(c) return hash % modulus def xor_hash(key: str) -> int: """XOR ハッシュ""" hash = 0 modulus = 1000000007 for c in key: hash ^= ord(c) return hash % modulus def rot_hash(key: str) -> int: """回転ハッシュ""" hash = 0 modulus = 1000000007 for c in key: hash = (hash << 4) ^ (hash >> 28) ^ ord(c) return hash % modulus ``` === "C++" ```cpp title="simple_hash.cpp" /* 加算ハッシュ */ int addHash(string key) { long long hash = 0; const int MODULUS = 1000000007; for (unsigned char c : key) { hash = (hash + (int)c) % MODULUS; } return (int)hash; } /* 乗算ハッシュ */ int mulHash(string key) { long long hash = 0; const int MODULUS = 1000000007; for (unsigned char c : key) { hash = (31 * hash + (int)c) % MODULUS; } return (int)hash; } /* XOR ハッシュ */ int xorHash(string key) { int hash = 0; const int MODULUS = 1000000007; for (unsigned char c : key) { hash ^= (int)c; } return hash & MODULUS; } /* 回転ハッシュ */ int rotHash(string key) { long long hash = 0; const int MODULUS = 1000000007; for (unsigned char c : key) { hash = ((hash << 4) ^ (hash >> 28) ^ (int)c) % MODULUS; } return (int)hash; } ``` === "Java" ```java title="simple_hash.java" /* 加算ハッシュ */ int addHash(String key) { long hash = 0; final int MODULUS = 1000000007; for (char c : key.toCharArray()) { hash = (hash + (int) c) % MODULUS; } return (int) hash; } /* 乗算ハッシュ */ int mulHash(String key) { long hash = 0; final int MODULUS = 1000000007; for (char c : key.toCharArray()) { hash = (31 * hash + (int) c) % MODULUS; } return (int) hash; } /* XOR ハッシュ */ int xorHash(String key) { int hash = 0; final int MODULUS = 1000000007; for (char c : key.toCharArray()) { hash ^= (int) c; } return hash & MODULUS; } /* 回転ハッシュ */ int rotHash(String key) { long hash = 0; final int MODULUS = 1000000007; for (char c : key.toCharArray()) { hash = ((hash << 4) ^ (hash >> 28) ^ (int) c) % MODULUS; } return (int) hash; } ``` === "C#" ```csharp title="simple_hash.cs" /* 加算ハッシュ */ int AddHash(string key) { long hash = 0; const int MODULUS = 1000000007; foreach (char c in key) { hash = (hash + c) % MODULUS; } return (int)hash; } /* 乗算ハッシュ */ int MulHash(string key) { long hash = 0; const int MODULUS = 1000000007; foreach (char c in key) { hash = (31 * hash + c) % MODULUS; } return (int)hash; } /* XOR ハッシュ */ int XorHash(string key) { int hash = 0; const int MODULUS = 1000000007; foreach (char c in key) { hash ^= c; } return hash & MODULUS; } /* 回転ハッシュ */ int RotHash(string key) { long hash = 0; const int MODULUS = 1000000007; foreach (char c in key) { hash = ((hash << 4) ^ (hash >> 28) ^ c) % MODULUS; } return (int)hash; } ``` === "Go" ```go title="simple_hash.go" /* 加算ハッシュ */ func addHash(key string) int { var hash int64 var modulus int64 modulus = 1000000007 for _, b := range []byte(key) { hash = (hash + int64(b)) % modulus } return int(hash) } /* 乗算ハッシュ */ func mulHash(key string) int { var hash int64 var modulus int64 modulus = 1000000007 for _, b := range []byte(key) { hash = (31*hash + int64(b)) % modulus } return int(hash) } /* XOR ハッシュ */ func xorHash(key string) int { hash := 0 modulus := 1000000007 for _, b := range []byte(key) { fmt.Println(int(b)) hash ^= int(b) hash = (31*hash + int(b)) % modulus } return hash & modulus } /* 回転ハッシュ */ func rotHash(key string) int { var hash int64 var modulus int64 modulus = 1000000007 for _, b := range []byte(key) { hash = ((hash << 4) ^ (hash >> 28) ^ int64(b)) % modulus } return int(hash) } ``` === "Swift" ```swift title="simple_hash.swift" /* 加算ハッシュ */ func addHash(key: String) -> Int { var hash = 0 let MODULUS = 1_000_000_007 for c in key { for scalar in c.unicodeScalars { hash = (hash + Int(scalar.value)) % MODULUS } } return hash } /* 乗算ハッシュ */ func mulHash(key: String) -> Int { var hash = 0 let MODULUS = 1_000_000_007 for c in key { for scalar in c.unicodeScalars { hash = (31 * hash + Int(scalar.value)) % MODULUS } } return hash } /* XOR ハッシュ */ func xorHash(key: String) -> Int { var hash = 0 let MODULUS = 1_000_000_007 for c in key { for scalar in c.unicodeScalars { hash ^= Int(scalar.value) } } return hash & MODULUS } /* 回転ハッシュ */ func rotHash(key: String) -> Int { var hash = 0 let MODULUS = 1_000_000_007 for c in key { for scalar in c.unicodeScalars { hash = ((hash << 4) ^ (hash >> 28) ^ Int(scalar.value)) % MODULUS } } return hash } ``` === "JS" ```javascript title="simple_hash.js" /* 加算ハッシュ */ function addHash(key) { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash = (hash + c.charCodeAt(0)) % MODULUS; } return hash; } /* 乗算ハッシュ */ function mulHash(key) { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash = (31 * hash + c.charCodeAt(0)) % MODULUS; } return hash; } /* XOR ハッシュ */ function xorHash(key) { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash ^= c.charCodeAt(0); } return hash % MODULUS; } /* 回転ハッシュ */ function rotHash(key) { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS; } return hash; } ``` === "TS" ```typescript title="simple_hash.ts" /* 加算ハッシュ */ function addHash(key: string): number { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash = (hash + c.charCodeAt(0)) % MODULUS; } return hash; } /* 乗算ハッシュ */ function mulHash(key: string): number { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash = (31 * hash + c.charCodeAt(0)) % MODULUS; } return hash; } /* XOR ハッシュ */ function xorHash(key: string): number { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash ^= c.charCodeAt(0); } return hash % MODULUS; } /* 回転ハッシュ */ function rotHash(key: string): number { let hash = 0; const MODULUS = 1000000007; for (const c of key) { hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS; } return hash; } ``` === "Dart" ```dart title="simple_hash.dart" /* 加算ハッシュ */ int addHash(String key) { int hash = 0; final int MODULUS = 1000000007; for (int i = 0; i < key.length; i++) { hash = (hash + key.codeUnitAt(i)) % MODULUS; } return hash; } /* 乗算ハッシュ */ int mulHash(String key) { int hash = 0; final int MODULUS = 1000000007; for (int i = 0; i < key.length; i++) { hash = (31 * hash + key.codeUnitAt(i)) % MODULUS; } return hash; } /* XOR ハッシュ */ int xorHash(String key) { int hash = 0; final int MODULUS = 1000000007; for (int i = 0; i < key.length; i++) { hash ^= key.codeUnitAt(i); } return hash & MODULUS; } /* 回転ハッシュ */ int rotHash(String key) { int hash = 0; final int MODULUS = 1000000007; for (int i = 0; i < key.length; i++) { hash = ((hash << 4) ^ (hash >> 28) ^ key.codeUnitAt(i)) % MODULUS; } return hash; } ``` === "Rust" ```rust title="simple_hash.rs" /* 加算ハッシュ */ fn add_hash(key: &str) -> i32 { let mut hash = 0_i64; const MODULUS: i64 = 1000000007; for c in key.chars() { hash = (hash + c as i64) % MODULUS; } hash as i32 } /* 乗算ハッシュ */ fn mul_hash(key: &str) -> i32 { let mut hash = 0_i64; const MODULUS: i64 = 1000000007; for c in key.chars() { hash = (31 * hash + c as i64) % MODULUS; } hash as i32 } /* XOR ハッシュ */ fn xor_hash(key: &str) -> i32 { let mut hash = 0_i64; const MODULUS: i64 = 1000000007; for c in key.chars() { hash ^= c as i64; } (hash & MODULUS) as i32 } /* 回転ハッシュ */ fn rot_hash(key: &str) -> i32 { let mut hash = 0_i64; const MODULUS: i64 = 1000000007; for c in key.chars() { hash = ((hash << 4) ^ (hash >> 28) ^ c as i64) % MODULUS; } hash as i32 } ``` === "C" ```c title="simple_hash.c" /* 加算ハッシュ */ int addHash(char *key) { long long hash = 0; const int MODULUS = 1000000007; for (int i = 0; i < strlen(key); i++) { hash = (hash + (unsigned char)key[i]) % MODULUS; } return (int)hash; } /* 乗算ハッシュ */ int mulHash(char *key) { long long hash = 0; const int MODULUS = 1000000007; for (int i = 0; i < strlen(key); i++) { hash = (31 * hash + (unsigned char)key[i]) % MODULUS; } return (int)hash; } /* XOR ハッシュ */ int xorHash(char *key) { int hash = 0; const int MODULUS = 1000000007; for (int i = 0; i < strlen(key); i++) { hash ^= (unsigned char)key[i]; } return hash & MODULUS; } /* 回転ハッシュ */ int rotHash(char *key) { long long hash = 0; const int MODULUS = 1000000007; for (int i = 0; i < strlen(key); i++) { hash = ((hash << 4) ^ (hash >> 28) ^ (unsigned char)key[i]) % MODULUS; } return (int)hash; } ``` === "Kotlin" ```kotlin title="simple_hash.kt" /* 加算ハッシュ */ fun addHash(key: String): Int { var hash = 0L val MODULUS = 1000000007 for (c in key.toCharArray()) { hash = (hash + c.code) % MODULUS } return hash.toInt() } /* 乗算ハッシュ */ fun mulHash(key: String): Int { var hash = 0L val MODULUS = 1000000007 for (c in key.toCharArray()) { hash = (31 * hash + c.code) % MODULUS } return hash.toInt() } /* XOR ハッシュ */ fun xorHash(key: String): Int { var hash = 0 val MODULUS = 1000000007 for (c in key.toCharArray()) { hash = hash xor c.code } return hash and MODULUS } /* 回転ハッシュ */ fun rotHash(key: String): Int { var hash = 0L val MODULUS = 1000000007 for (c in key.toCharArray()) { hash = ((hash shl 4) xor (hash shr 28) xor c.code.toLong()) % MODULUS } return hash.toInt() } ``` === "Ruby" ```ruby title="simple_hash.rb" ### 加算ハッシュ ### def add_hash(key) hash = 0 modulus = 1_000_000_007 key.each_char { |c| hash += c.ord } hash % modulus end ### 乗算ハッシュ ### def mul_hash(key) hash = 0 modulus = 1_000_000_007 key.each_char { |c| hash = 31 * hash + c.ord } hash % modulus end ### XOR ハッシュ ### def xor_hash(key) hash = 0 modulus = 1_000_000_007 key.each_char { |c| hash ^= c.ord } hash % modulus end ### 回転ハッシュ ### def rot_hash(key) hash = 0 modulus = 1_000_000_007 key.each_char { |c| hash = (hash << 4) ^ (hash >> 28) ^ c.ord } hash % modulus end ``` ??? pythontutor "コードの可視化" 見て分かるように、各ハッシュアルゴリズムの最後のステップでは、大きな素数 $1000000007$ で剰余を取り、ハッシュ値が適切な範囲に収まるようにしています。ここで考えてみる価値があるのは、なぜ素数での剰余を強調するのか、あるいは合成数で剰余を取ることにどんな欠点があるのか、という点です。これは興味深い問題です。 先に結論を述べると、**法として大きな素数を使うと、ハッシュ値が均一に分布することを最大限に保証できます**。素数はほかの数と公約数を持たないため、剰余演算によって生じる周期的なパターンを減らし、ハッシュ衝突を避けやすくなります。 たとえば、法として合成数 $9$ を選ぶとします。これは $3$ で割り切れるため、$3$ で割り切れるすべての `key` は、$0$、$3$、$6$ の 3 つのハッシュ値に写像されます。 $$ \begin{aligned} \text{modulus} & = 9 \newline \text{key} & = \{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \dots \} \newline \text{hash} & = \{ 0, 3, 6, 0, 3, 6, 0, 3, 6, 0, 3, 6,\dots \} \end{aligned} $$ 入力 `key` がたまたまこのような等差数列の分布をしていると、ハッシュ値に偏りが生じ、ハッシュ衝突がさらに深刻になります。そこで `modulus` を素数 $13$ に置き換えると仮定すると、`key` と `modulus` の間に公約数が存在しないため、出力されるハッシュ値の均一性は明らかに向上します。 $$ \begin{aligned} \text{modulus} & = 13 \newline \text{key} & = \{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \dots \} \newline \text{hash} & = \{ 0, 3, 6, 9, 12, 2, 5, 8, 11, 1, 4, 7, \dots \} \end{aligned} $$ 補足すると、`key` がランダムかつ均一に分布していると保証できるなら、法に素数を選んでも合成数を選んでも構いません。どちらでも均一に分布したハッシュ値を出力できます。しかし、`key` の分布に何らかの周期性がある場合、合成数で剰余を取るほうが偏りが生じやすくなります。 要するに、通常は法として素数を選び、その素数はできるだけ大きいほうが望ましいです。そうすることで周期的なパターンをできる限り取り除き、ハッシュアルゴリズムの堅牢性を高められます。 ## 6.3.3 一般的なハッシュアルゴリズム 上で紹介した単純なハッシュアルゴリズムは、どれも比較的「脆弱」であり、ハッシュアルゴリズムの設計目標にはほど遠いことが分かります。たとえば、加算と XOR は交換法則を満たすため、加算ハッシュと XOR ハッシュでは、内容が同じで順序だけ異なる文字列を区別できません。これはハッシュ衝突を悪化させ、一部の安全上の問題を引き起こす可能性があります。 実際には、MD5、SHA-1、SHA-2、SHA-3 などの標準的なハッシュアルゴリズムを用いることが一般的です。これらは任意長の入力データを、固定長のハッシュ値へ写像できます。 ここ 1 世紀近くの間、ハッシュアルゴリズムは継続的に改良と最適化が進められてきました。ある研究者たちは性能向上に取り組み、別の研究者やハッカーたちは安全性の弱点を探し続けてきました。次の表は、実際の応用でよく使われるハッシュアルゴリズムを示したものです。 - MD5 と SHA-1 は何度も攻撃に成功されているため、各種のセキュリティ用途では廃止されています。 - SHA-2 系列の SHA-256 は最も安全なハッシュアルゴリズムの 1 つであり、いまだに成功した攻撃例がないため、多くのセキュリティ用途やプロトコルで広く使われています。 - SHA-3 は SHA-2 と比べて実装コストが低く、計算効率も高い一方で、現時点での普及度は SHA-2 系列に及びません。表 6-2 一般的なハッシュアルゴリズム