docs: add Japanese translate documents (#1812)
* docs: add Japanese documents (`ja/docs`) * docs: add Japanese documents (`ja/codes`) * docs: add Japanese documents * Remove pythontutor blocks in ja/ * Add an empty at the end of each markdown file. * Add the missing figures (use the English version temporarily). * Add index.md for Japanese version. * Add index.html for Japanese version. * Add missing index.assets * Fix backtracking_algorithm.md for Japanese version. * Add avatar_eltociear.jpg. Fix image links on the Japanese landing page. * Add the Japanese banner. --------- Co-authored-by: krahets <krahets@163.com>
|
After Width: | Height: | Size: 28 KiB |
362
ja/docs/chapter_hashing/hash_algorithm.md
Normal file
@ -0,0 +1,362 @@
|
||||
# ハッシュアルゴリズム
|
||||
|
||||
前の2つの節では、ハッシュ表の動作原理とハッシュ衝突を処理する方法を紹介しました。しかし、オープンアドレス法と連鎖法はどちらも**衝突が発生した際にハッシュ表が正常に機能することのみを保証でき、ハッシュ衝突の発生頻度を減らすことはできません**。
|
||||
|
||||
ハッシュ衝突があまりにも頻繁に発生すると、ハッシュ表の性能は劇的に悪化します。下図に示すように、連鎖法ハッシュ表では、理想的なケースではキー値ペアがバケット間に均等に分散され、最適なクエリ効率を実現します。最悪のケースでは、すべてのキー値ペアが同じバケットに格納され、時間計算量が$O(n)$に悪化します。
|
||||
|
||||

|
||||
|
||||
**キー値ペアの分布はハッシュ関数によって決定されます**。ハッシュ関数の計算ステップを思い出すと、まずハッシュ値を計算し、次に配列長で剰余を取ります:
|
||||
|
||||
```shell
|
||||
index = hash(key) % capacity
|
||||
```
|
||||
|
||||
上記の式を観察すると、ハッシュ表の容量`capacity`が固定されている場合、**ハッシュアルゴリズム`hash()`が出力値を決定し**、それによってハッシュ表におけるキー値ペアの分布を決定します。
|
||||
|
||||
これは、ハッシュ衝突の確率を減らすために、ハッシュアルゴリズム`hash()`の設計に焦点を当てるべきであることを意味します。
|
||||
|
||||
## ハッシュアルゴリズムの目標
|
||||
|
||||
「高速で安定した」ハッシュ表データ構造を実現するために、ハッシュアルゴリズムは以下の特性を持つべきです:
|
||||
|
||||
- **決定性**: 同じ入力に対して、ハッシュアルゴリズムは常に同じ出力を生成するべきです。そうでなければハッシュ表は信頼できません。
|
||||
- **高効率**: ハッシュ値を計算するプロセスは十分に高速である必要があります。計算オーバーヘッドが小さいほど、ハッシュ表はより実用的になります。
|
||||
- **均等分散**: ハッシュアルゴリズムはキー値ペアがハッシュ表に均等に分散されることを保証するべきです。分散が均等であるほど、ハッシュ衝突の確率は低くなります。
|
||||
|
||||
実際、ハッシュアルゴリズムはハッシュ表の実装だけでなく、他の分野でも広く応用されています。
|
||||
|
||||
- **パスワード保存**: ユーザーパスワードのセキュリティを保護するために、システムは通常平文パスワードを保存せず、パスワードのハッシュ値を保存します。ユーザーがパスワードを入力すると、システムは入力のハッシュ値を計算し、保存されているハッシュ値と比較します。一致すれば、パスワードは正しいと見なされます。
|
||||
- **データ整合性チェック**: データ送信者はデータのハッシュ値を計算して一緒に送信できます。受信者は受信したデータのハッシュ値を再計算し、受信したハッシュ値と比較できます。一致すれば、データは完全であると見なされます。
|
||||
|
||||
暗号化アプリケーションでは、ハッシュ値から元のパスワードを推測するなどの逆行分析を防ぐために、ハッシュアルゴリズムはより高いレベルのセキュリティ機能が必要です。
|
||||
|
||||
- **一方向性**: ハッシュ値から入力データに関する情報を推測することは不可能であるべきです。
|
||||
- **衝突耐性**: 同じハッシュ値を生成する2つの異なる入力を見つけることは極めて困難であるべきです。
|
||||
- **雪崩効果**: 入力の小さな変更は、出力に大きく予測不可能な変化をもたらすべきです。
|
||||
|
||||
**「均等分散」と「衝突耐性」は2つの別々の概念**であることに注意してください。均等分散を満たしても、必ずしも衝突耐性があるとは限りません。例えば、ランダムな入力`key`の下で、ハッシュ関数`key % 100`は均等に分散された出力を生成できます。しかし、このハッシュアルゴリズムは過度にシンプルで、下二桁が同じすべての`key`は同じ出力を持つため、ハッシュ値から使用可能な`key`を簡単に推測でき、パスワードを破ることができます。
|
||||
|
||||
## ハッシュアルゴリズムの設計
|
||||
|
||||
ハッシュアルゴリズムの設計は多くの要因を考慮する必要がある複雑な問題です。しかし、要求が少ない一部のシナリオでは、いくつかの簡単なハッシュアルゴリズムを設計することもできます。
|
||||
|
||||
- **加算ハッシュ**: 入力の各文字のASCIIコードを合計し、合計をハッシュ値として使用します。
|
||||
- **乗算ハッシュ**: 乗算の非相関性を利用し、各ラウンドで定数を乗算し、各文字のASCIIコードをハッシュ値に累積します。
|
||||
- **XORハッシュ**: 入力データの各要素をXORすることでハッシュ値を累積します。
|
||||
- **回転ハッシュ**: 各文字のASCIIコードをハッシュ値に累積し、各累積前にハッシュ値に回転操作を実行します。
|
||||
|
||||
```src
|
||||
[file]{simple_hash}-[class]{}-[func]{rot_hash}
|
||||
```
|
||||
|
||||
各ハッシュアルゴリズムの最後のステップが大きな素数$1000000007$の剰余を取ることで、ハッシュ値が適切な範囲内にあることを保証していることが観察されます。なぜ素数の剰余を取ることが強調されるのか、または合成数の剰余を取ることの欠点は何かを考える価値があります。これは興味深い質問です。
|
||||
|
||||
結論として:**大きな素数を剰余として使用することで、ハッシュ値の均等分散を最大化できます**。素数は他の数と共通因子を持たないため、剰余演算によって引き起こされる周期的パターンを減らし、ハッシュ衝突を回避できます。
|
||||
|
||||
例えば、合成数$9$を剰余として選択するとします。これは$3$で割り切れるため、$3$で割り切れるすべての`key`はハッシュ値$0$、$3$、$6$にマッピングされます。
|
||||
|
||||
$$
|
||||
\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`の分布にある種の周期性がある場合、合成数の剰余はクラスタリングを引き起こしやすくなります。
|
||||
|
||||
要約すると、通常は素数を剰余として選択し、この素数は周期的パターンを可能な限り排除し、ハッシュアルゴリズムの堅牢性を向上させるために十分大きくある必要があります。
|
||||
|
||||
## 一般的なハッシュアルゴリズム
|
||||
|
||||
上記で言及した簡単なハッシュアルゴリズムはかなり「脆弱」で、ハッシュアルゴリズムの設計目標から程遠いことは難しくありません。例えば、加算とXORは交換法則に従うため、加算ハッシュとXORハッシュは同じ内容だが順序が異なる文字列を区別できず、ハッシュ衝突を悪化させ、セキュリティ問題を引き起こす可能性があります。
|
||||
|
||||
実際には、通常MD5、SHA-1、SHA-2、SHA-3などの標準ハッシュアルゴリズムを使用します。これらは任意の長さの入力データを固定長のハッシュ値にマッピングできます。
|
||||
|
||||
過去1世紀にわたって、ハッシュアルゴリズムは継続的なアップグレードと最適化のプロセスにありました。一部の研究者はハッシュアルゴリズムの性能向上に努め、ハッカーを含む他の人々はハッシュアルゴリズムのセキュリティ問題を見つけることに専念しています。以下の表は、実用的なアプリケーションで一般的に使用されるハッシュアルゴリズムを示しています。
|
||||
|
||||
- MD5とSHA-1は複数回攻撃に成功しており、さまざまなセキュリティアプリケーションで放棄されています。
|
||||
- SHA-2シリーズ、特にSHA-256は、現在最も安全なハッシュアルゴリズムの1つで、成功した攻撃は報告されておらず、さまざまなセキュリティアプリケーションとプロトコルで一般的に使用されています。
|
||||
- SHA-3はSHA-2と比較して実装コストが低く、計算効率が高いですが、現在の使用範囲はSHA-2シリーズほど広範囲ではありません。
|
||||
|
||||
<p align="center"> 表 <id> 一般的なハッシュアルゴリズム </p>
|
||||
|
||||
| | MD5 | SHA-1 | SHA-2 | SHA-3 |
|
||||
| --------------- | ----------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------- | ---------------------------- |
|
||||
| リリース年 | 1992 | 1995 | 2002 | 2008 |
|
||||
| 出力長 | 128 bit | 160 bit | 256/512 bit | 224/256/384/512 bit |
|
||||
| ハッシュ衝突 | 頻繁 | 頻繁 | まれ | まれ |
|
||||
| セキュリティレベル | 低、攻撃に成功している | 低、攻撃に成功している | 高 | 高 |
|
||||
| アプリケーション | 放棄、データ整合性チェックにまだ使用 | 放棄 | 暗号通貨取引検証、デジタル署名など | SHA-2の代替として使用可能 |
|
||||
|
||||
# データ構造におけるハッシュ値
|
||||
|
||||
ハッシュ表のキーは整数、小数、文字列などのさまざまなデータ型にできることを知っています。プログラミング言語は通常、これらのデータ型に対して組み込みのハッシュアルゴリズムを提供し、ハッシュ表のバケットインデックスを計算します。Pythonを例に取ると、`hash()`関数を使用してさまざまなデータ型のハッシュ値を計算できます。
|
||||
|
||||
- 整数とブール値のハッシュ値は、それら自身の値です。
|
||||
- 浮動小数点数と文字列のハッシュ値の計算はより複雑で、興味のある読者は自分で研究することをお勧めします。
|
||||
- タプルのハッシュ値は、その各要素のハッシュ値の組み合わせで、単一のハッシュ値になります。
|
||||
- オブジェクトのハッシュ値は、そのメモリアドレスに基づいて生成されます。オブジェクトのハッシュメソッドをオーバーライドすることで、内容に基づいてハッシュ値を生成できます。
|
||||
|
||||
!!! tip
|
||||
|
||||
異なるプログラミング言語における組み込みハッシュ値計算関数の定義と方法は異なることに注意してください。
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="built_in_hash.py"
|
||||
num = 3
|
||||
hash_num = hash(num)
|
||||
# 整数3のハッシュ値は3
|
||||
|
||||
bol = True
|
||||
hash_bol = hash(bol)
|
||||
# ブール値Trueのハッシュ値は1
|
||||
|
||||
dec = 3.14159
|
||||
hash_dec = hash(dec)
|
||||
# 小数3.14159のハッシュ値は326484311674566659
|
||||
|
||||
str = "Hello 算法"
|
||||
hash_str = hash(str)
|
||||
# 文字列"Hello 算法"のハッシュ値は4617003410720528961
|
||||
|
||||
tup = (12836, "小哈")
|
||||
hash_tup = hash(tup)
|
||||
# タプル(12836, '小哈')のハッシュ値は1029005403108185979
|
||||
|
||||
obj = ListNode(0)
|
||||
hash_obj = hash(obj)
|
||||
# ListNodeオブジェクト0x1058fd810のハッシュ値は274267521
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="built_in_hash.cpp"
|
||||
int num = 3;
|
||||
size_t hashNum = hash<int>()(num);
|
||||
// 整数3のハッシュ値は3
|
||||
|
||||
bool bol = true;
|
||||
size_t hashBol = hash<bool>()(bol);
|
||||
// ブール値1のハッシュ値は1
|
||||
|
||||
double dec = 3.14159;
|
||||
size_t hashDec = hash<double>()(dec);
|
||||
// 小数3.14159のハッシュ値は4614256650576692846
|
||||
|
||||
string str = "Hello 算法";
|
||||
size_t hashStr = hash<string>()(str);
|
||||
// 文字列"Hello 算法"のハッシュ値は15466937326284535026
|
||||
|
||||
// C++では、組み込みstd::hash()は基本データ型のハッシュ値のみを提供
|
||||
// 配列とオブジェクトのハッシュ値は別途実装が必要
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="built_in_hash.java"
|
||||
int num = 3;
|
||||
int hashNum = Integer.hashCode(num);
|
||||
// 整数3のハッシュ値は3
|
||||
|
||||
boolean bol = true;
|
||||
int hashBol = Boolean.hashCode(bol);
|
||||
// ブール値trueのハッシュ値は1231
|
||||
|
||||
double dec = 3.14159;
|
||||
int hashDec = Double.hashCode(dec);
|
||||
// 小数3.14159のハッシュ値は-1340954729
|
||||
|
||||
String str = "Hello 算法";
|
||||
int hashStr = str.hashCode();
|
||||
// 文字列"Hello 算法"のハッシュ値は-727081396
|
||||
|
||||
Object[] arr = { 12836, "小哈" };
|
||||
int hashTup = Arrays.hashCode(arr);
|
||||
// 配列[12836, 小哈]のハッシュ値は1151158
|
||||
|
||||
ListNode obj = new ListNode(0);
|
||||
int hashObj = obj.hashCode();
|
||||
// ListNodeオブジェクトutils.ListNode@7dc5e7b4のハッシュ値は2110121908
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="built_in_hash.cs"
|
||||
int num = 3;
|
||||
int hashNum = num.GetHashCode();
|
||||
// 整数3のハッシュ値は3;
|
||||
|
||||
bool bol = true;
|
||||
int hashBol = bol.GetHashCode();
|
||||
// ブール値trueのハッシュ値は1;
|
||||
|
||||
double dec = 3.14159;
|
||||
int hashDec = dec.GetHashCode();
|
||||
// 小数3.14159のハッシュ値は-1340954729;
|
||||
|
||||
string str = "Hello 算法";
|
||||
int hashStr = str.GetHashCode();
|
||||
// 文字列"Hello 算法"のハッシュ値は-586107568;
|
||||
|
||||
object[] arr = [12836, "小哈"];
|
||||
int hashTup = arr.GetHashCode();
|
||||
// 配列[12836, 小哈]のハッシュ値は42931033;
|
||||
|
||||
ListNode obj = new(0);
|
||||
int hashObj = obj.GetHashCode();
|
||||
// ListNodeオブジェクト0のハッシュ値は39053774;
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="built_in_hash.go"
|
||||
// Goには組み込みのハッシュコード関数が提供されていません
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="built_in_hash.swift"
|
||||
let num = 3
|
||||
let hashNum = num.hashValue
|
||||
// 整数3のハッシュ値は9047044699613009734
|
||||
|
||||
let bol = true
|
||||
let hashBol = bol.hashValue
|
||||
// ブール値trueのハッシュ値は-4431640247352757451
|
||||
|
||||
let dec = 3.14159
|
||||
let hashDec = dec.hashValue
|
||||
// 小数3.14159のハッシュ値は-2465384235396674631
|
||||
|
||||
let str = "Hello 算法"
|
||||
let hashStr = str.hashValue
|
||||
// 文字列"Hello 算法"のハッシュ値は-7850626797806988787
|
||||
|
||||
let arr = [AnyHashable(12836), AnyHashable("小哈")]
|
||||
let hashTup = arr.hashValue
|
||||
// 配列[AnyHashable(12836), AnyHashable("小哈")]のハッシュ値は-2308633508154532996
|
||||
|
||||
let obj = ListNode(x: 0)
|
||||
let hashObj = obj.hashValue
|
||||
// ListNodeオブジェクトutils.ListNodeのハッシュ値は-2434780518035996159
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="built_in_hash.js"
|
||||
// JavaScriptには組み込みのハッシュコード関数が提供されていません
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="built_in_hash.ts"
|
||||
// TypeScriptには組み込みのハッシュコード関数が提供されていません
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="built_in_hash.dart"
|
||||
int num = 3;
|
||||
int hashNum = num.hashCode;
|
||||
// 整数3のハッシュ値は34803
|
||||
|
||||
bool bol = true;
|
||||
int hashBol = bol.hashCode;
|
||||
// ブール値trueのハッシュ値は1231
|
||||
|
||||
double dec = 3.14159;
|
||||
int hashDec = dec.hashCode;
|
||||
// 小数3.14159のハッシュ値は2570631074981783
|
||||
|
||||
String str = "Hello 算法";
|
||||
int hashStr = str.hashCode;
|
||||
// 文字列"Hello 算法"のハッシュ値は468167534
|
||||
|
||||
List arr = [12836, "小哈"];
|
||||
int hashArr = arr.hashCode;
|
||||
// 配列[12836, 小哈]のハッシュ値は976512528
|
||||
|
||||
ListNode obj = new ListNode(0);
|
||||
int hashObj = obj.hashCode;
|
||||
// ListNodeオブジェクトInstance of 'ListNode'のハッシュ値は1033450432
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="built_in_hash.rs"
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
let num = 3;
|
||||
let mut num_hasher = DefaultHasher::new();
|
||||
num.hash(&mut num_hasher);
|
||||
let hash_num = num_hasher.finish();
|
||||
// 整数3のハッシュ値は568126464209439262
|
||||
|
||||
let bol = true;
|
||||
let mut bol_hasher = DefaultHasher::new();
|
||||
bol.hash(&mut bol_hasher);
|
||||
let hash_bol = bol_hasher.finish();
|
||||
// ブール値trueのハッシュ値は4952851536318644461
|
||||
|
||||
let dec: f32 = 3.14159;
|
||||
let mut dec_hasher = DefaultHasher::new();
|
||||
dec.to_bits().hash(&mut dec_hasher);
|
||||
let hash_dec = dec_hasher.finish();
|
||||
// 小数3.14159のハッシュ値は2566941990314602357
|
||||
|
||||
let str = "Hello 算法";
|
||||
let mut str_hasher = DefaultHasher::new();
|
||||
str.hash(&mut str_hasher);
|
||||
let hash_str = str_hasher.finish();
|
||||
// 文字列"Hello 算法"のハッシュ値は16092673739211250988
|
||||
|
||||
let arr = (&12836, &"小哈");
|
||||
let mut tup_hasher = DefaultHasher::new();
|
||||
arr.hash(&mut tup_hasher);
|
||||
let hash_tup = tup_hasher.finish();
|
||||
// タプル(12836, "小哈")のハッシュ値は1885128010422702749
|
||||
|
||||
let node = ListNode::new(42);
|
||||
let mut hasher = DefaultHasher::new();
|
||||
node.borrow().val.hash(&mut hasher);
|
||||
let hash = hasher.finish();
|
||||
// ListNodeオブジェクトRefCell { value: ListNode { val: 42, next: None } }のハッシュ値は15387811073369036852
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="built_in_hash.c"
|
||||
// Cには組み込みのハッシュコード関数が提供されていません
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="built_in_hash.kt"
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="built_in_hash.zig"
|
||||
|
||||
```
|
||||
|
||||
多くのプログラミング言語では、**不変オブジェクトのみがハッシュ表の`key`として機能できます**。リスト(動的配列)を`key`として使用する場合、リストの内容が変更されると、そのハッシュ値も変更され、ハッシュ表で元の`value`を見つけることができなくなります。
|
||||
|
||||
カスタムオブジェクト(連結リストノードなど)のメンバー変数は可変ですが、ハッシュ可能です。**これは、オブジェクトのハッシュ値が通常そのメモリアドレスに基づいて生成されるためです**。オブジェクトの内容が変更されても、メモリアドレスは同じままなので、ハッシュ値は変更されません。
|
||||
|
||||
異なるコンソールで出力されるハッシュ値が異なることに気づいたかもしれません。**これは、Pythonインタープリターが起動するたびに文字列ハッシュ関数にランダムソルトを追加するためです**。このアプローチはHashDoS攻撃を効果的に防ぎ、ハッシュアルゴリズムのセキュリティを向上させます。
|
||||
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 18 KiB |
108
ja/docs/chapter_hashing/hash_collision.md
Normal file
@ -0,0 +1,108 @@
|
||||
# ハッシュ衝突
|
||||
|
||||
前節で述べたように、**ほとんどの場合、ハッシュ関数の入力空間は出力空間よりもはるかに大きい**ため、理論的にはハッシュ衝突は避けられません。例えば、入力空間がすべての整数で、出力空間が配列容量のサイズの場合、複数の整数が必然的に同じバケットインデックスにマッピングされます。
|
||||
|
||||
ハッシュ衝突は誤ったクエリ結果につながり、ハッシュ表の使いやすさに深刻な影響を与える可能性があります。この問題に対処するために、ハッシュ衝突が発生するたびに、衝突が消えるまでハッシュ表のリサイズを実行します。このアプローチは非常にシンプルで直接的であり、うまく機能します。しかし、テーブルの拡張には大量のデータ移行とハッシュコードの再計算が含まれ、これらは高コストであるため、非常に非効率的に見えます。効率を向上させるために、以下の戦略を採用できます:
|
||||
|
||||
1. **ハッシュ衝突が発生した場合でも、ターゲット要素の検索が適切に機能する**ようにハッシュ表のデータ構造を改善する。
|
||||
2. 深刻な衝突が観察され、必要になる前に、拡張は最後の手段とする。
|
||||
|
||||
ハッシュ表の構造を改善する主な方法は2つあります:「連鎖法」と「オープンアドレス法」です。
|
||||
|
||||
## 連鎖法
|
||||
|
||||
元のハッシュ表では、各バケットは1つのキー値ペアのみを格納できます。<u>連鎖法</u>は単一の要素を連結リストに変換し、キー値ペアをリストノードとして扱い、衝突するすべてのキー値ペアを同じ連結リストに格納します。下図は連鎖法を使用したハッシュ表の例を示しています。
|
||||
|
||||

|
||||
|
||||
連鎖法で実装されたハッシュ表の操作は以下のように変更されます:
|
||||
|
||||
- **要素のクエリ**: `key`を入力し、ハッシュ関数を通してバケットインデックスを取得し、連結リストのヘッドノードにアクセスします。連結リストを走査してキーを比較し、ターゲットキー値ペアを見つけます。
|
||||
- **要素の追加**: ハッシュ関数を通して連結リストのヘッドノードにアクセスし、ノード(キー値ペア)をリストに追加します。
|
||||
- **要素の削除**: ハッシュ関数の結果に基づいて連結リストのヘッドにアクセスし、連結リストを走査してターゲットノードを見つけて削除します。
|
||||
|
||||
連鎖法には以下の制限があります:
|
||||
|
||||
- **空間使用量の増加**: 連結リストにはノードポインタが含まれており、配列よりも多くのメモリ空間を消費します。
|
||||
- **クエリ効率の低下**: 対応する要素を見つけるために連結リストの線形走査が必要になるためです。
|
||||
|
||||
以下のコードは連鎖法ハッシュ表の簡単な実装を提供し、注意すべき2つの点があります:
|
||||
|
||||
- 簡単にするために、連結リストの代わりにリスト(動的配列)を使用します。この設定では、ハッシュ表(配列)は複数のバケットを含み、各バケットはリストです。
|
||||
- この実装にはハッシュ表のリサイズメソッドが含まれています。負荷率が$\frac{2}{3}$を超えると、ハッシュ表を元のサイズの2倍に拡張します。
|
||||
|
||||
```src
|
||||
[file]{hash_map_chaining}-[class]{hash_map_chaining}-[func]{}
|
||||
```
|
||||
|
||||
連結リストが非常に長い場合、クエリ効率$O(n)$が悪いことは注目に値します。**この場合、リストを「AVL木」または「赤黒木」に変換して**、クエリ操作の時間計算量を$O(\log n)$に最適化できます。
|
||||
|
||||
## オープンアドレス法
|
||||
|
||||
<u>オープンアドレス法</u>は追加のデータ構造を導入せず、代わりに「複数回プローブ」を通してハッシュ衝突を処理します。プローブ方法には主に線形プローブ、二次プローブ、二重ハッシュがあります。
|
||||
|
||||
線形プローブを例にして、オープンアドレス法ハッシュ表のメカニズムを紹介しましょう。
|
||||
|
||||
### 線形プローブ
|
||||
|
||||
線形プローブは固定ステップの線形検索をプローブに使用し、通常のハッシュ表とは異なります。
|
||||
|
||||
- **要素の挿入**: ハッシュ関数を使用してバケットインデックスを計算します。バケットに既に要素が含まれている場合、衝突位置から線形に前方に走査し(通常ステップサイズは$1$)、空のバケットが見つかるまで進み、要素を挿入します。
|
||||
- **要素の検索**: ハッシュ衝突に遭遇した場合、同じステップサイズを使用して線形に前方に走査し、対応する要素が見つかったら`value`を返します。空のバケットに遭遇した場合、ターゲット要素がハッシュ表にないことを意味するため、`None`を返します。
|
||||
|
||||
下図はオープンアドレス法(線形プローブ)ハッシュ表におけるキー値ペアの分布を示しています。このハッシュ関数によると、下二桁が同じキーは同じバケットにマッピングされます。線形プローブを通して、それらはそのバケットとその下のバケットに順次格納されます。
|
||||
|
||||

|
||||
|
||||
しかし、**線形プローブは「クラスタリング」を作りやすい傾向があります**。具体的には、配列内の連続的に占有された位置が長いほど、これらの連続した位置でハッシュ衝突が発生する確率が高くなり、その位置でのクラスタリングの成長をさらに促進し、悪循環を形成し、最終的に挿入、削除、クエリ、更新操作の効率低下につながります。
|
||||
|
||||
**オープンアドレス法ハッシュ表では要素を直接削除できない**ことに注意することが重要です。要素を削除すると、配列に空のバケット`None`が作成されます。要素を検索する際、線形プローブがこの空のバケットに遭遇すると戻ってしまい、このバケットの下の要素にアクセスできなくなります。プログラムはこれらの要素が存在しないと誤って仮定する可能性があります。下図に示すとおりです。
|
||||
|
||||

|
||||
|
||||
この問題を解決するために、<u>遅延削除</u>メカニズムを採用できます:ハッシュ表から要素を直接削除する代わりに、**定数`TOMBSTONE`を使用してバケットをマークします**。このメカニズムでは、`None`と`TOMBSTONE`の両方が空のバケットを表し、キー値ペアを保持できます。ただし、線形プローブが`TOMBSTONE`に遭遇した場合、その下にまだキー値ペアがある可能性があるため、走査を続ける必要があります。
|
||||
|
||||
しかし、**遅延削除はハッシュ表の性能劣化を加速する可能性があります**。削除操作のたびに削除マークが生成され、`TOMBSTONE`が増加すると、線形プローブがターゲット要素を見つけるために複数の`TOMBSTONE`をスキップする必要がある可能性があるため、検索時間も増加します。
|
||||
|
||||
これに対処するために、線形プローブ中に最初に遭遇した`TOMBSTONE`のインデックスを記録し、検索されたターゲット要素とその`TOMBSTONE`の位置を交換することを検討してください。これを行う利点は、要素がクエリまたは追加されるたびに、要素がその理想的な位置(プローブの開始点)により近いバケットに移動され、クエリ効率が最適化されることです。
|
||||
|
||||
以下のコードは、遅延削除を使用したオープンアドレス法(線形プローブ)ハッシュ表を実装しています。ハッシュ表の空間をより有効に活用するために、ハッシュ表を「循環配列」として扱います。配列の終わりを超えると、最初に戻って走査を続けます。
|
||||
|
||||
```src
|
||||
[file]{hash_map_open_addressing}-[class]{hash_map_open_addressing}-[func]{}
|
||||
```
|
||||
|
||||
### 二次プローブ
|
||||
|
||||
二次プローブは線形プローブに似ており、オープンアドレス法の一般的な戦略の1つです。衝突が発生した場合、二次プローブは単純に固定ステップ数をスキップするのではなく、「プローブ回数の二乗」に等しいステップ数、つまり$1, 4, 9, \dots$ステップをスキップします。
|
||||
|
||||
二次プローブには以下の利点があります:
|
||||
|
||||
- 二次プローブは、プローブ回数の二乗の距離をスキップすることで、線形プローブのクラスタリング効果を軽減しようとします。
|
||||
- 二次プローブはより大きな距離をスキップして空の位置を見つけ、データをより均等に分散するのに役立ちます。
|
||||
|
||||
しかし、二次プローブは完璧ではありません:
|
||||
|
||||
- クラスタリングは依然として存在し、つまり一部の位置は他の位置よりも占有される可能性が高いです。
|
||||
- 二乗の成長により、二次プローブはハッシュ表全体をプローブできない可能性があり、ハッシュ表に空のバケットがあっても、二次プローブがアクセスできない可能性があります。
|
||||
|
||||
### 二重ハッシュ
|
||||
|
||||
名前が示すように、二重ハッシュ法は複数のハッシュ関数$f_1(x)$、$f_2(x)$、$f_3(x)$、$\dots$をプローブに使用します。
|
||||
|
||||
- **要素の挿入**: ハッシュ関数$f_1(x)$が衝突に遭遇した場合、$f_2(x)$を試し、以下同様に、空の位置が見つかって要素が挿入されるまで続けます。
|
||||
- **要素の検索**: 同じハッシュ関数の順序で検索し、ターゲット要素が見つかって返されるまで、または空の位置に遭遇するかすべてのハッシュ関数が試されるまで続け、要素がハッシュ表にないことを示し、`None`を返します。
|
||||
|
||||
線形プローブと比較して、二重ハッシュ法はクラスタリングが起こりにくいですが、複数のハッシュ関数は追加の計算オーバーヘッドを導入します。
|
||||
|
||||
!!! tip
|
||||
|
||||
オープンアドレス法(線形プローブ、二次プローブ、二重ハッシュ)ハッシュ表はすべて「要素を直接削除できない」という問題があることに注意してください。
|
||||
|
||||
## プログラミング言語の選択
|
||||
|
||||
異なるプログラミング言語は異なるハッシュ表実装戦略を採用しています。以下にいくつかの例を示します:
|
||||
|
||||
- Pythonはオープンアドレス法を使用します。`dict`辞書はプローブに疑似乱数を使用します。
|
||||
- Javaは連鎖法を使用します。JDK 1.8以降、`HashMap`の配列長が64に達し、連結リストの長さが8に達すると、連結リストは検索性能を向上させるために赤黒木に変換されます。
|
||||
- Goは連鎖法を使用します。Goは各バケットが最大8つのキー値ペアを格納できることを規定し、容量を超えた場合はオーバーフローバケットが連結されます。オーバーフローバケットが多すぎる場合、性能を確保するために特別な等容量リサイズ操作が実行されます。
|
||||
BIN
ja/docs/chapter_hashing/hash_map.assets/hash_collision.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
ja/docs/chapter_hashing/hash_map.assets/hash_function.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
ja/docs/chapter_hashing/hash_map.assets/hash_table_lookup.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
ja/docs/chapter_hashing/hash_map.assets/hash_table_reshash.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
529
ja/docs/chapter_hashing/hash_map.md
Normal file
@ -0,0 +1,529 @@
|
||||
# ハッシュ表
|
||||
|
||||
<u>ハッシュ表</u>は<u>ハッシュマップ</u>とも呼ばれ、キーと値の間のマッピングを確立し、効率的な要素の取得を可能にするデータ構造です。具体的には、ハッシュ表に`key`を入力すると、$O(1)$の時間計算量で対応する`value`を取得できます。
|
||||
|
||||
下図に示すように、$n$人の学生がいて、各学生には「名前」と「学籍番号」の2つのデータフィールドがあるとします。学籍番号を入力として対応する名前を返すクエリ機能を実装したい場合、下図に示すハッシュ表を使用できます。
|
||||
|
||||

|
||||
|
||||
ハッシュ表に加えて、配列や連結リストもクエリ機能の実装に使用できますが、時間計算量が異なります。効率は以下の表で比較されています:
|
||||
|
||||
- **要素の挿入**: 配列(または連結リスト)の末尾に要素を追加するだけです。この操作の時間計算量は$O(1)$です。
|
||||
- **要素の検索**: 配列(または連結リスト)がソートされていないため、要素を検索するにはすべての要素を走査する必要があります。この操作の時間計算量は$O(n)$です。
|
||||
- **要素の削除**: 要素を削除するには、まずその要素を見つけてから、配列(または連結リスト)から削除します。この操作の時間計算量は$O(n)$です。
|
||||
|
||||
<p align="center"> 表 <id> 一般的な操作の時間効率の比較 </p>
|
||||
|
||||
| | 配列 | 連結リスト | ハッシュ表 |
|
||||
| -------------- | ------ | ----------- | ---------- |
|
||||
| 要素の検索 | $O(n)$ | $O(n)$ | $O(1)$ |
|
||||
| 要素の挿入 | $O(1)$ | $O(1)$ | $O(1)$ |
|
||||
| 要素の削除 | $O(n)$ | $O(n)$ | $O(1)$ |
|
||||
|
||||
観察されるように、**ハッシュ表における操作(挿入、削除、検索、変更)の時間計算量は$O(1)$**で、非常に効率的です。
|
||||
|
||||
## ハッシュ表の一般的な操作
|
||||
|
||||
ハッシュ表の一般的な操作には、初期化、クエリ、キー値ペアの追加、キー値ペアの削除があります。以下はコード例です:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="hash_map.py"
|
||||
# ハッシュ表を初期化
|
||||
hmap: dict = {}
|
||||
|
||||
# 追加操作
|
||||
# ハッシュ表にキー値ペア (key, value) を追加
|
||||
hmap[12836] = "小哈"
|
||||
hmap[15937] = "小啰"
|
||||
hmap[16750] = "小算"
|
||||
hmap[13276] = "小法"
|
||||
hmap[10583] = "小鸭"
|
||||
|
||||
# クエリ操作
|
||||
# ハッシュ表にキーを入力し、値を取得
|
||||
name: str = hmap[15937]
|
||||
|
||||
# 削除操作
|
||||
# ハッシュ表からキー値ペア (key, value) を削除
|
||||
hmap.pop(10583)
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="hash_map.cpp"
|
||||
/* ハッシュ表を初期化 */
|
||||
unordered_map<int, string> map;
|
||||
|
||||
/* 追加操作 */
|
||||
// ハッシュ表にキー値ペア (key, value) を追加
|
||||
map[12836] = "小哈";
|
||||
map[15937] = "小啰";
|
||||
map[16750] = "小算";
|
||||
map[13276] = "小法";
|
||||
map[10583] = "小鸭";
|
||||
|
||||
/* クエリ操作 */
|
||||
// ハッシュ表にキーを入力し、値を取得
|
||||
string name = map[15937];
|
||||
|
||||
/* 削除操作 */
|
||||
// ハッシュ表からキー値ペア (key, value) を削除
|
||||
map.erase(10583);
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="hash_map.java"
|
||||
/* ハッシュ表を初期化 */
|
||||
Map<Integer, String> map = new HashMap<>();
|
||||
|
||||
/* 追加操作 */
|
||||
// ハッシュ表にキー値ペア (key, value) を追加
|
||||
map.put(12836, "小哈");
|
||||
map.put(15937, "小啰");
|
||||
map.put(16750, "小算");
|
||||
map.put(13276, "小法");
|
||||
map.put(10583, "小鸭");
|
||||
|
||||
/* クエリ操作 */
|
||||
// ハッシュ表にキーを入力し、値を取得
|
||||
String name = map.get(15937);
|
||||
|
||||
/* 削除操作 */
|
||||
// ハッシュ表からキー値ペア (key, value) を削除
|
||||
map.remove(10583);
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="hash_map.cs"
|
||||
/* ハッシュ表を初期化 */
|
||||
Dictionary<int, string> map = new() {
|
||||
/* 追加操作 */
|
||||
// ハッシュ表にキー値ペア (key, value) を追加
|
||||
{ 12836, "小哈" },
|
||||
{ 15937, "小啰" },
|
||||
{ 16750, "小算" },
|
||||
{ 13276, "小法" },
|
||||
{ 10583, "小鸭" }
|
||||
};
|
||||
|
||||
/* クエリ操作 */
|
||||
// ハッシュ表にキーを入力し、値を取得
|
||||
string name = map[15937];
|
||||
|
||||
/* 削除操作 */
|
||||
// ハッシュ表からキー値ペア (key, value) を削除
|
||||
map.Remove(10583);
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="hash_map_test.go"
|
||||
/* ハッシュ表を初期化 */
|
||||
hmap := make(map[int]string)
|
||||
|
||||
/* 追加操作 */
|
||||
// ハッシュ表にキー値ペア (key, value) を追加
|
||||
hmap[12836] = "小哈"
|
||||
hmap[15937] = "小啰"
|
||||
hmap[16750] = "小算"
|
||||
hmap[13276] = "小法"
|
||||
hmap[10583] = "小鸭"
|
||||
|
||||
/* クエリ操作 */
|
||||
// ハッシュ表にキーを入力し、値を取得
|
||||
name := hmap[15937]
|
||||
|
||||
/* 削除操作 */
|
||||
// ハッシュ表からキー値ペア (key, value) を削除
|
||||
delete(hmap, 10583)
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="hash_map.swift"
|
||||
/* ハッシュ表を初期化 */
|
||||
var map: [Int: String] = [:]
|
||||
|
||||
/* 追加操作 */
|
||||
// ハッシュ表にキー値ペア (key, value) を追加
|
||||
map[12836] = "小哈"
|
||||
map[15937] = "小啰"
|
||||
map[16750] = "小算"
|
||||
map[13276] = "小法"
|
||||
map[10583] = "小鸭"
|
||||
|
||||
/* クエリ操作 */
|
||||
// ハッシュ表にキーを入力し、値を取得
|
||||
let name = map[15937]!
|
||||
|
||||
/* 削除操作 */
|
||||
// ハッシュ表からキー値ペア (key, value) を削除
|
||||
map.removeValue(forKey: 10583)
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="hash_map.js"
|
||||
/* ハッシュ表を初期化 */
|
||||
const map = new Map();
|
||||
/* 追加操作 */
|
||||
// ハッシュ表にキー値ペア (key, value) を追加
|
||||
map.set(12836, '小哈');
|
||||
map.set(15937, '小啰');
|
||||
map.set(16750, '小算');
|
||||
map.set(13276, '小法');
|
||||
map.set(10583, '小鸭');
|
||||
|
||||
/* クエリ操作 */
|
||||
// ハッシュ表にキーを入力し、値を取得
|
||||
let name = map.get(15937);
|
||||
|
||||
/* 削除操作 */
|
||||
// ハッシュ表からキー値ペア (key, value) を削除
|
||||
map.delete(10583);
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="hash_map.ts"
|
||||
/* ハッシュ表を初期化 */
|
||||
const map = new Map<number, string>();
|
||||
/* 追加操作 */
|
||||
// ハッシュ表にキー値ペア (key, value) を追加
|
||||
map.set(12836, '小哈');
|
||||
map.set(15937, '小啰');
|
||||
map.set(16750, '小算');
|
||||
map.set(13276, '小法');
|
||||
map.set(10583, '小鸭');
|
||||
console.info('\n追加後、ハッシュ表は\nKey -> Value');
|
||||
console.info(map);
|
||||
|
||||
/* クエリ操作 */
|
||||
// ハッシュ表にキーを入力し、値を取得
|
||||
let name = map.get(15937);
|
||||
console.info('\n学籍番号15937を入力、名前を問い合わせ ' + name);
|
||||
|
||||
/* 削除操作 */
|
||||
// ハッシュ表からキー値ペア (key, value) を削除
|
||||
map.delete(10583);
|
||||
console.info('\n10583を削除後、ハッシュ表は\nKey -> Value');
|
||||
console.info(map);
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="hash_map.dart"
|
||||
/* ハッシュ表を初期化 */
|
||||
Map<int, String> map = {};
|
||||
|
||||
/* 追加操作 */
|
||||
// ハッシュ表にキー値ペア (key, value) を追加
|
||||
map[12836] = "小哈";
|
||||
map[15937] = "小啰";
|
||||
map[16750] = "小算";
|
||||
map[13276] = "小法";
|
||||
map[10583] = "小鸭";
|
||||
|
||||
/* クエリ操作 */
|
||||
// ハッシュ表にキーを入力し、値を取得
|
||||
String name = map[15937];
|
||||
|
||||
/* 削除操作 */
|
||||
// ハッシュ表からキー値ペア (key, value) を削除
|
||||
map.remove(10583);
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="hash_map.rs"
|
||||
use std::collections::HashMap;
|
||||
|
||||
/* ハッシュ表を初期化 */
|
||||
let mut map: HashMap<i32, String> = HashMap::new();
|
||||
|
||||
/* 追加操作 */
|
||||
// ハッシュ表にキー値ペア (key, value) を追加
|
||||
map.insert(12836, "小哈".to_string());
|
||||
map.insert(15937, "小啰".to_string());
|
||||
map.insert(16750, "小算".to_string());
|
||||
map.insert(13279, "小法".to_string());
|
||||
map.insert(10583, "小鸭".to_string());
|
||||
|
||||
/* クエリ操作 */
|
||||
// ハッシュ表にキーを入力し、値を取得
|
||||
let _name: Option<&String> = map.get(&15937);
|
||||
|
||||
/* 削除操作 */
|
||||
// ハッシュ表からキー値ペア (key, value) を削除
|
||||
let _removed_value: Option<String> = map.remove(&10583);
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="hash_map.c"
|
||||
// Cには組み込みのハッシュ表が提供されていません
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="hash_map.kt"
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="hash_map.zig"
|
||||
|
||||
```
|
||||
|
||||
ハッシュ表を走査する一般的な方法は3つあります:キー値ペアの走査、キーの走査、値の走査。以下はコード例です:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="hash_map.py"
|
||||
# ハッシュ表を走査
|
||||
# キー値ペア key->value を走査
|
||||
for key, value in hmap.items():
|
||||
print(key, "->", value)
|
||||
# キーのみを走査
|
||||
for key in hmap.keys():
|
||||
print(key)
|
||||
# 値のみを走査
|
||||
for value in hmap.values():
|
||||
print(value)
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="hash_map.cpp"
|
||||
/* ハッシュ表を走査 */
|
||||
// キー値ペア key->value を走査
|
||||
for (auto kv: map) {
|
||||
cout << kv.first << " -> " << kv.second << endl;
|
||||
}
|
||||
// イテレータを使用してキー値ペア key->value を走査
|
||||
for (auto iter = map.begin(); iter != map.end(); iter++) {
|
||||
cout << iter->first << "->" << iter->second << endl;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="hash_map.java"
|
||||
/* ハッシュ表を走査 */
|
||||
// キー値ペア key->value を走査
|
||||
for (Map.Entry<Integer, String> kv: map.entrySet()) {
|
||||
System.out.println(kv.getKey() + " -> " + kv.getValue());
|
||||
}
|
||||
// キーのみを走査
|
||||
for (int key: map.keySet()) {
|
||||
System.out.println(key);
|
||||
}
|
||||
// 値のみを走査
|
||||
for (String val: map.values()) {
|
||||
System.out.println(val);
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="hash_map.cs"
|
||||
/* ハッシュ表を走査 */
|
||||
// キー値ペア Key->Value を走査
|
||||
foreach (var kv in map) {
|
||||
Console.WriteLine(kv.Key + " -> " + kv.Value);
|
||||
}
|
||||
// キーのみを走査
|
||||
foreach (int key in map.Keys) {
|
||||
Console.WriteLine(key);
|
||||
}
|
||||
// 値のみを走査
|
||||
foreach (string val in map.Values) {
|
||||
Console.WriteLine(val);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="hash_map_test.go"
|
||||
/* ハッシュ表を走査 */
|
||||
// キー値ペア key->value を走査
|
||||
for key, value := range hmap {
|
||||
fmt.Println(key, "->", value)
|
||||
}
|
||||
// キーのみを走査
|
||||
for key := range hmap {
|
||||
fmt.Println(key)
|
||||
}
|
||||
// 値のみを走査
|
||||
for _, value := range hmap {
|
||||
fmt.Println(value)
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="hash_map.swift"
|
||||
/* ハッシュ表を走査 */
|
||||
// キー値ペア Key->Value を走査
|
||||
for (key, value) in map {
|
||||
print("\(key) -> \(value)")
|
||||
}
|
||||
// キーのみを走査
|
||||
for key in map.keys {
|
||||
print(key)
|
||||
}
|
||||
// 値のみを走査
|
||||
for value in map.values {
|
||||
print(value)
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="hash_map.js"
|
||||
/* ハッシュ表を走査 */
|
||||
console.info('\nキー値ペア Key->Value を走査');
|
||||
for (const [k, v] of map.entries()) {
|
||||
console.info(k + ' -> ' + v);
|
||||
}
|
||||
console.info('\nキーのみを走査 Key');
|
||||
for (const k of map.keys()) {
|
||||
console.info(k);
|
||||
}
|
||||
console.info('\n値のみを走査 Value');
|
||||
for (const v of map.values()) {
|
||||
console.info(v);
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="hash_map.ts"
|
||||
/* ハッシュ表を走査 */
|
||||
console.info('\nキー値ペア Key->Value を走査');
|
||||
for (const [k, v] of map.entries()) {
|
||||
console.info(k + ' -> ' + v);
|
||||
}
|
||||
console.info('\nキーのみを走査 Key');
|
||||
for (const k of map.keys()) {
|
||||
console.info(k);
|
||||
}
|
||||
console.info('\n値のみを走査 Value');
|
||||
for (const v of map.values()) {
|
||||
console.info(v);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="hash_map.dart"
|
||||
/* ハッシュ表を走査 */
|
||||
// キー値ペア Key->Value を走査
|
||||
map.forEach((key, value) {
|
||||
print('$key -> $value');
|
||||
});
|
||||
|
||||
// キーのみを走査 Key
|
||||
map.keys.forEach((key) {
|
||||
print(key);
|
||||
});
|
||||
|
||||
// 値のみを走査 Value
|
||||
map.values.forEach((value) {
|
||||
print(value);
|
||||
});
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="hash_map.rs"
|
||||
/* ハッシュ表を走査 */
|
||||
// キー値ペア Key->Value を走査
|
||||
for (key, value) in &map {
|
||||
println!("{key} -> {value}");
|
||||
}
|
||||
|
||||
// キーのみを走査 Key
|
||||
for key in map.keys() {
|
||||
println!("{key}");
|
||||
}
|
||||
|
||||
// 値のみを走査 Value
|
||||
for value in map.values() {
|
||||
println!("{value}");
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="hash_map.c"
|
||||
// Cには組み込みのハッシュ表が提供されていません
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="hash_map.kt"
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="hash_map.zig"
|
||||
// Zigの例は提供されていません
|
||||
```
|
||||
|
||||
## ハッシュ表の簡単な実装
|
||||
|
||||
まず、最も簡単なケースを考えてみましょう:**配列のみを使ってハッシュ表を実装すること**。ハッシュ表において、配列の各空きスロットは<u>バケット</u>と呼ばれ、各バケットはキー値ペアを格納できます。したがって、クエリ操作は`key`に対応するバケットを見つけ、そこから`value`を取得することになります。
|
||||
|
||||
では、`key`に基づいて対応するバケットをどのように特定するのでしょうか?これは<u>ハッシュ関数</u>によって実現されます。ハッシュ関数の役割は、より大きな入力空間をより小さな出力空間にマッピングすることです。ハッシュ表では、入力空間はすべてのキーで構成され、出力空間はすべてのバケット(配列インデックス)で構成されます。つまり、`key`が与えられた場合、**ハッシュ関数を使用して対応するキー値ペアの配列内の格納位置を決定できます**。
|
||||
|
||||
与えられた`key`に対して、ハッシュ関数の計算は2つのステップで構成されます:
|
||||
|
||||
1. 特定のハッシュアルゴリズム`hash()`を使用してハッシュ値を計算します。
|
||||
2. ハッシュ値をバケット数(配列長)`capacity`で剰余を取り、キーに対応する配列`index`を取得します。
|
||||
|
||||
```shell
|
||||
index = hash(key) % capacity
|
||||
```
|
||||
|
||||
その後、`index`を使用してハッシュ表内の対応するバケットにアクセスし、`value`を取得できます。
|
||||
|
||||
配列長が`capacity = 100`で、ハッシュアルゴリズムが`hash(key) = key`として定義されているとします。したがって、ハッシュ関数は`key % 100`として表現できます。以下の図は、`key`を学籍番号、`value`を名前として、ハッシュ関数の動作原理を示しています。
|
||||
|
||||

|
||||
|
||||
以下のコードは簡単なハッシュ表を実装しています。ここでは、`key`と`value`を`Pair`クラスにカプセル化してキー値ペアを表現しています。
|
||||
|
||||
```src
|
||||
[file]{array_hash_map}-[class]{array_hash_map}-[func]{}
|
||||
```
|
||||
|
||||
## ハッシュ衝突とリサイズ
|
||||
|
||||
本質的に、ハッシュ関数の役割は、すべてのキーの入力空間全体を、すべての配列インデックスの出力空間にマッピングすることです。しかし、入力空間は出力空間よりもはるかに大きいことがよくあります。したがって、**理論的には、「複数の入力が同じ出力に対応する」ケースが常に存在します**。
|
||||
|
||||
上記の例では、与えられたハッシュ関数で、入力`key`の下二桁が同じ場合、ハッシュ関数は同じ出力を生成します。例えば、学籍番号12836と20336の2人の学生をクエリすると、以下のことがわかります:
|
||||
|
||||
```shell
|
||||
12836 % 100 = 36
|
||||
20336 % 100 = 36
|
||||
```
|
||||
|
||||
下図に示すように、両方の学籍番号が同じ名前を指しており、これは明らかに間違っています。この複数の入力が同じ出力に対応する状況を<u>ハッシュ衝突</u>と呼びます。
|
||||
|
||||

|
||||
|
||||
ハッシュ表の容量$n$が増加するにつれて、複数のキーが同じバケットに割り当てられる確率が減少し、衝突が少なくなることは理解しやすいです。したがって、**ハッシュ表をリサイズすることでハッシュ衝突を減らすことができます**。
|
||||
|
||||
下図に示すように、リサイズ前は、キー値ペア`(136, A)`と`(236, D)`が衝突していました。しかし、リサイズ後は衝突が解決されています。
|
||||
|
||||

|
||||
|
||||
配列の拡張と同様に、ハッシュ表のリサイズにはすべてのキー値ペアを元のハッシュ表から新しいものに移行する必要があり、時間がかかります。さらに、ハッシュ表の`capacity`が変更されるため、ハッシュ関数を使用してすべてのキー値ペアの格納位置を再計算する必要があり、リサイズプロセスの計算オーバーヘッドがさらに増加します。したがって、プログラミング言語は頻繁なリサイズを防ぐために、ハッシュ表に十分大きな容量を割り当てることがよくあります。
|
||||
|
||||
<u>負荷率</u>はハッシュ表の重要な概念です。ハッシュ表内の要素数とバケット数の比率として定義されます。ハッシュ衝突の深刻度を測定するために使用され、**しばしばハッシュ表のリサイズのトリガーとしても機能します**。例えば、Javaでは、負荷率が$0.75$を超えると、システムはハッシュ表を元のサイズの2倍にリサイズします。
|
||||
9
ja/docs/chapter_hashing/index.md
Normal file
@ -0,0 +1,9 @@
|
||||
# ハッシュ表
|
||||
|
||||

|
||||
|
||||
!!! abstract
|
||||
|
||||
コンピューティングの世界において、ハッシュ表は賢い司書のようなものです。
|
||||
|
||||
インデックス番号の計算方法を理解し、目的の本を迅速に取得することを可能にします。
|
||||
47
ja/docs/chapter_hashing/summary.md
Normal file
@ -0,0 +1,47 @@
|
||||
# まとめ
|
||||
|
||||
### 重要なポイント
|
||||
|
||||
- 入力`key`が与えられると、ハッシュ表は$O(1)$の時間で対応する`value`を取得でき、非常に効率的です。
|
||||
- 一般的なハッシュ表の操作には、クエリ、キー値ペアの追加、キー値ペアの削除、ハッシュ表の走査があります。
|
||||
- ハッシュ関数は`key`を配列インデックスにマッピングし、対応するバケットにアクセスして`value`を取得できるようにします。
|
||||
- 2つの異なるキーがハッシュ化後に同じ配列インデックスになる場合があり、誤ったクエリ結果につながります。この現象はハッシュ衝突として知られています。
|
||||
- ハッシュ表の容量が大きいほど、ハッシュ衝突の確率は低くなります。したがって、ハッシュ表のリサイズはハッシュ衝突を緩和できます。配列のリサイズと同様に、ハッシュ表のリサイズはコストが高いです。
|
||||
- 要素数をバケット数で割った負荷率は、ハッシュ衝突の深刻度を反映し、しばしばハッシュ表リサイズのトリガー条件として使用されます。
|
||||
- 連鎖法は各要素を連結リストに変換し、衝突するすべての要素を同じリストに格納することでハッシュ衝突に対処します。ただし、過度に長いリストはクエリ効率を低下させる可能性があり、リストを赤黒木に変換することで改善できます。
|
||||
- オープンアドレス法は複数回のプローブを通してハッシュ衝突を処理します。線形プローブは固定ステップサイズを使用しますが、要素を削除できず、クラスタリングを起こしやすい傾向があります。多重ハッシュはプローブに複数のハッシュ関数を使用し、線形プローブと比較してクラスタリングを減らしますが、計算オーバーヘッドが増加します。
|
||||
- 異なるプログラミング言語はさまざまなハッシュ表実装を採用しています。例えば、Javaの`HashMap`は連鎖法を使用し、Pythonの`dict`はオープンアドレス法を採用しています。
|
||||
- ハッシュ表では、決定性、高効率、均等分散を持つハッシュアルゴリズムが望まれます。暗号化では、ハッシュアルゴリズムは衝突耐性と雪崩効果も持つべきです。
|
||||
- ハッシュアルゴリズムは通常、ハッシュ値の均等分散を保証し、ハッシュ衝突を減らすために、大きな素数を剰余として使用します。
|
||||
- 一般的なハッシュアルゴリズムには、MD5、SHA-1、SHA-2、SHA-3があります。MD5はファイル整合性チェックによく使用され、SHA-2は安全なアプリケーションとプロトコルで一般的に使用されます。
|
||||
- プログラミング言語は通常、ハッシュ表のバケットインデックスを計算するために、データ型に対して組み込みのハッシュアルゴリズムを提供します。一般的に、不変オブジェクトのみがハッシュ可能です。
|
||||
|
||||
### Q & A
|
||||
|
||||
**Q**: ハッシュ表の時間計算量が$O(n)$に悪化するのはいつですか?
|
||||
|
||||
ハッシュ表の時間計算量は、ハッシュ衝突が深刻な場合に$O(n)$に悪化する可能性があります。ハッシュ関数が適切に設計され、容量が適切に設定され、衝突が均等に分散されている場合、時間計算量は$O(1)$です。プログラミング言語の組み込みハッシュ表を使用する場合、通常は時間計算量を$O(1)$と考えます。
|
||||
|
||||
**Q**: なぜハッシュ関数$f(x) = x$を使用しないのですか?これなら衝突を排除できます。
|
||||
|
||||
ハッシュ関数$f(x) = x$では、各要素が一意のバケットインデックスに対応し、これは配列と同等です。しかし、入力空間は通常出力空間(配列長)よりもはるかに大きいため、ハッシュ関数の最後のステップは配列長の剰余を取ることがよくあります。言い換えると、ハッシュ表の目標は、$O(1)$のクエリ効率を提供しながら、より大きな状態空間をより小さなものにマッピングすることです。
|
||||
|
||||
**Q**: ハッシュ表がこれらの構造を使って実装されているにもかかわらず、なぜ配列、連結リスト、二分木よりも効率的になれるのですか?
|
||||
|
||||
まず、ハッシュ表は時間効率が高いですが、空間効率は低いです。ハッシュ表のメモリの大部分は未使用のままです。
|
||||
|
||||
次に、ハッシュ表は特定のユースケースでのみ時間効率が高いです。配列や連結リストを使用して同じ時間計算量で機能を実装できる場合、通常はハッシュ表を使用するよりも高速です。これは、ハッシュ関数の計算がオーバーヘッドを発生させ、時間計算量の定数因子が大きくなるためです。
|
||||
|
||||
最後に、ハッシュ表の時間計算量は悪化する可能性があります。例えば、連鎖法では、連結リストや赤黒木で検索操作を実行し、これは依然として$O(n)$時間に悪化するリスクがあります。
|
||||
|
||||
**Q**: 多重ハッシュにも要素を直接削除できないという欠陥がありますか?削除としてマークされた空間は再利用できますか?
|
||||
|
||||
多重ハッシュはオープンアドレス法の一形態であり、すべてのオープンアドレス法には要素を直接削除できないという欠点があります。要素を削除済みとしてマークする必要があります。マークされた空間は再利用できます。ハッシュ表に新しい要素を挿入する際、ハッシュ関数が削除済みとしてマークされた位置を指している場合、その位置は新しい要素によって使用できます。これにより、ハッシュ表のプローブシーケンスを維持しながら、空間の効率的な使用が保証されます。
|
||||
|
||||
**Q**: なぜ線形プローブの検索プロセス中にハッシュ衝突が発生するのですか?
|
||||
|
||||
検索プロセス中、ハッシュ関数は対応するバケットとキー値ペアを指します。`key`が一致しない場合、ハッシュ衝突を示します。したがって、線形プローブは正しいキー値ペアが見つかるか検索が失敗するまで、事前に決められたステップサイズで下方向に検索します。
|
||||
|
||||
**Q**: なぜハッシュ表のリサイズがハッシュ衝突を緩和できるのですか?
|
||||
|
||||
ハッシュ関数の最後のステップは、出力を配列インデックス範囲内に保つために、配列長$n$の剰余を取ることがよくあります。リサイズ時、配列長$n$が変化し、キーに対応するインデックスも変化する可能性があります。以前に同じバケットにマッピングされていたキーが、リサイズ後に複数のバケットに分散される可能性があり、それによってハッシュ衝突が緩和されます。
|
||||