Re-translate the Japanese version (#1871)

* Retranslate Japanese docs with GPT-5.4

* Retranslate Japanese code with GPT-5.4
This commit is contained in:
Yudong Jin
2026-03-30 07:30:15 +08:00
committed by GitHub
parent fe6443235b
commit d7b2277d2b
1444 changed files with 83312 additions and 8363 deletions
+165 -111
View File
@@ -1,60 +1,60 @@
# ハッシュアルゴリズム
前の2つの節では、ハッシュの動作原理とハッシュ衝突処理する方法を紹介しました。しかし、オープンアドレス法と連鎖法はどちらも**衝突発生した際にハッシュ表が正常に機能することのみを保証でき、ハッシュ衝突の発生頻度を減らすことはできません**。
前の 2 節では、ハッシュテーブルの動作原理とハッシュ衝突処理方法を紹介しました。しかし、オープンアドレス法であれ連鎖方式であれ、**それらが保証できるのは衝突発生時でもハッシュテーブルが正常に動作することだけであり、ハッシュ衝突そのものを減らすことはできません**。
ハッシュ衝突があまりにも頻繁に発生すると、ハッシュ表の性能は劇的に悪化します。下図に示すように、連鎖ハッシュでは、理想的なケースではキー値ペアがバケットに均等に分散され、最適なクエリ効率を実現します。最悪のケースでは、すべてのキーペアが同じバケットに格納され、時間計算量$O(n)$に悪化します。
ハッシュ衝突があまりにも頻繁に発生すると、ハッシュテーブルの性能は急激に劣化します。下図ように、連鎖方式のハッシュテーブルでは、理想的な場合にはキーと値のペアがバケットに均等に分布し、最良の検索効率を達成します。最悪の場合には、すべてのキーと値のペアが同じバケットに格納され、時間計算量$O(n)$ に劣化します。
![ハッシュ衝突の理想的および最悪ケース](hash_algorithm.assets/hash_collision_best_worst_condition.png)
![ハッシュ衝突の最良ケースと最悪ケース](hash_algorithm.assets/hash_collision_best_worst_condition.png)
**キーペアの分布はハッシュ関数によって決定されます**。ハッシュ関数の計算ステップを思い出すと、まずハッシュ値を計算し、次に配列長で剰余を取ります
**キーと値のペアの分布はハッシュ関数によって決まります**。ハッシュ関数の計算手順を思い出すと、まずハッシュ値を計算し、その後で配列長に対して剰余を取ります
```shell
index = hash(key) % capacity
```
の式を観察すると、ハッシュの容量`capacity`が固定されている場合、**ハッシュアルゴリズム`hash()`が出力値を決定し**、それによってハッシュ表におけるキー値ペアの分布決定します。
上の式から分かるように、ハッシュテーブルの容量 `capacity` が固定されているとき、**出力値を決めるのはハッシュアルゴリズム `hash()` です**。したがって、それがキーと値のペアのハッシュテーブル内での分布決定します。
これは、ハッシュ衝突の確率を減らすために、ハッシュアルゴリズム`hash()`の設計に焦点を当てるべきであることを意味します。
これは、ハッシュ衝突の発生確率を下げるには、ハッシュアルゴリズム `hash()` の設計に注目すべきだということを意味します。
## ハッシュアルゴリズムの目標
「高速安定した」ハッシュデータ構造を実現するために、ハッシュアルゴリズムは以下の特性を持つべきです:
「高速かつ安定した」ハッシュテーブルというデータ構造を実現するために、ハッシュアルゴリズムは次の特徴を備える必要があります。
- **決定性**: 同じ入力に対して、ハッシュアルゴリズムは常に同じ出力を生成するべきです。そうでなければハッシュ表は信頼できません
- **高効率**: ハッシュ値計算するプロセスは十分に高速である必要があります。計算オーバーヘッドが小さいほど、ハッシュ表はより実用的になります。
- **均等分散**: ハッシュアルゴリズムはキーペアがハッシュ表に均等に分散されることを保証するべきです。分が均であるほど、ハッシュ衝突の確率は低くなります。
- **決定性**同じ入力に対して、ハッシュアルゴリズムは常に同じ出力を生成しなければなりません。そうして初めて、ハッシュテーブルの信頼性が保たれます
- **高効率**ハッシュ値計算過程は十分に高速であるべきです。計算コストが小さいほど、ハッシュテーブルの実用性は高くなります。
- **均一分布**ハッシュアルゴリズムはキーと値のペアがハッシュテーブル内に均等に分布するようにすべきです。分が均であるほど、ハッシュ衝突の確率は低くなります。
実際、ハッシュアルゴリズムはハッシュの実装だけでなく、の分野でも広く用されています。
実際には、ハッシュアルゴリズムはハッシュテーブルの実装だけでなく、ほかの多くの分野でも広く用されています。
- **パスワード保存**: ユーザーパスワードのセキュリティを保護するために、システムは通常平文パスワードを保存せず、パスワードのハッシュ値を保存します。ユーザーがパスワードを入力すると、システムは入力のハッシュ値を計算し、保存されているハッシュ値と比較します。一致すれば、パスワードは正しいと見なされます。
- **データ整合性チェック**: データ送信はデータのハッシュ値を計算して一緒に送信できます。受信は受信したデータのハッシュ値を再計算し、受信したハッシュ値と比較できます。一致すれば、データは完全であると見なされます。
- **パスワード保存**ユーザーパスワードを保護するために、システムは通常平文パスワードを直接保存せず、のハッシュ値を保存します。ユーザーがパスワードを入力すると、システムは入力内容のハッシュ値を計算し、保存済みのハッシュ値と比較します。一致すれば、そのパスワードは正しいと見なされます。
- **データ完全性検査**送信はデータのハッシュ値を計算してデータと一緒に送信できます。受信は受け取ったデータのハッシュ値を再計算し、受信したハッシュ値と比較できます。両者が一致すれば、そのデータは完全と見なされます。
暗号化アプリケーションでは、ハッシュ値から元のパスワードを推測するなどの逆行分析を防ぐために、ハッシュアルゴリズムはより高いレベルのセキュリティ機能が必要です。
暗号分野の応用では、ハッシュ値から元のパスワードを推測するといった逆解析を防ぐために、ハッシュアルゴリズムにはさらに高いレベルの安全性が求められます。
- **一方向性**: ハッシュ値から入力データに関する情報を推測することは不可能であるべきです
- **衝突性**: 同じハッシュ値を生成する2つの異なる入力を見つけること極めて困難であるべきです
- **雪崩効果**: 入力の小さな変更は、出力大きく予測不能な変化をもたらすべきです
- **一方向性**ハッシュ値から入力データに関するいかなる情報も逆算できないこと
- **衝突性**:異なる 2 つの入力で同じハッシュ値になるものを見つけることが、極めて困難であること
- **アバランシェ効果**入力のわずかな変化が、出力大きく予測不能な変化を引き起こすこと
**「均等分散」と「衝突性」は2つの別々の概念**であることに注意してください。均等分散を満たしても、必ずしも衝突耐性があるとは限りません。えば、ランダムな入力`key`の下で、ハッシュ関数`key % 100`は均に分散された出力を生成できます。しかし、このハッシュアルゴリズムは過度にシンプルで、下二桁が同じすべての`key`は同じ出力を持つため、ハッシュ値から使用可能な`key`を簡単に推測でき、パスワードを破ることができます。
注意してほしいのは、**「均一分布」と「衝突性」は独立した 2 つの概念である**という点です。均一分布を満たしていても、耐衝突性を満たすとは限りません。たとえば、入力 `key` がランダムである場合、ハッシュ関数 `key % 100` は均に分布した出力を生成できます。しかし、このハッシュアルゴリズムはあまりにも単純で、下 2 桁が同じ `key` はすべて同じ出力になります。そのため、ハッシュ値から用可能な `key` を容易に逆算でき、結果としてパスワードが破られてしまいます。
## ハッシュアルゴリズムの設計
ハッシュアルゴリズムの設計は多くの要を考慮する必要がある複雑な問題です。しかし、要求が少ない一部のシナリオでは、いくつかの単なハッシュアルゴリズムを設計することもできます。
ハッシュアルゴリズムの設計は多くの要を考慮しなければならない複雑な問題です。しかし、要求の高くない場面であれば、いくつかの単なハッシュアルゴリズムを設計することもできます。
- **加算ハッシュ**: 入力の各文字のASCIIコードを合計し、合計をハッシュ値として使用します。
- **乗算ハッシュ**: 乗算の非相関性を利用し、各ラウンドで定数を乗算し、各文字のASCIIコードをハッシュ値に累積します。
- **XORハッシュ**: 入力データの各要素をXORすることでハッシュ値累積します。
- **回転ハッシュ**: 各文字のASCIIコードをハッシュ値に累積し、各累積前にハッシュ値回転操作を実行します。
- **加算ハッシュ**入力の各文字の ASCII コードを足し合わせ、その合計をハッシュ値とします。
- **乗算ハッシュ**乗算の非相関性を利用し、各ラウンドで定数を掛けながら、各文字の ASCII コードをハッシュ値に累積します。
- **XOR ハッシュ**入力データの各要素を XOR 演算で 1 つのハッシュ値累積します。
- **回転ハッシュ**各文字の ASCII コードを 1 つのハッシュ値に累積し、各回の累積前にハッシュ値回転させます。
```src
[file]{simple_hash}-[class]{}-[func]{rot_hash}
```
各ハッシュアルゴリズムの最後のステップ大きな素数$1000000007$剰余を取ることで、ハッシュ値が適切な範囲内にあることを保証していることが観察されます。なぜ素数の剰余を取ることが強調されるのか、または合成数剰余を取ることの欠点は何かを考える価値があります。これは興味深い問です。
見て分かるように、各ハッシュアルゴリズムの最後のステップでは、大きな素数 $1000000007$剰余を取、ハッシュ値が適切な範囲に収まるようにしています。ここで考えてみる価値があるのは、なぜ素数の剰余を強調するのか、あるいは合成数剰余を取ることにどんな欠点があるのか、という点です。これは興味深い問です。
結論として:**大きな素数を剰余として使用することで、ハッシュ値の均等分散を最大化できます**。素数はの数と共通因子を持たないため、剰余演算によって引き起こされる周期的パターンを減らし、ハッシュ衝突を回避できます。
先に結論を述べると、**法として大きな素数を使うと、ハッシュ値が均一に分布することを最大限に保証できます**。素数はほかの数と公約数を持たないため、剰余演算によって生じる周期的パターンを減らし、ハッシュ衝突を避けやすくなります。
えば、合成数$9$を剰余として選択するとします。これは$3$で割り切れるため、$3$で割り切れるすべての`key`はハッシュ値$0$、$3$、$6$にマッピングされます。
たとえば、法として合成数 $9$ を選ぶとします。これは $3$ で割り切れるため、$3$ で割り切れるすべての `key` は、$0$、$3$、$6$ の 3 つのハッシュ値に写像されます。
$$
\begin{aligned}
@@ -64,7 +64,7 @@ $$
\end{aligned}
$$
入力`key`がたまたまこの種の等差数列分布を持つ場合、ハッシュ値がクラスターし、ハッシュ衝突を悪化させます。今度は`modulus`を素数$13$に置き換えるとします。`key``modulus`の間に共通因子がないため、出力ハッシュ値の均等性が大幅に改善されます。
入力 `key` がたまたまこのような等差数列分布をしていると、ハッシュ値に偏りが生じ、ハッシュ衝突がさらに深刻になります。そこで `modulus` を素数 $13$ に置き換えると仮定すると、`key``modulus` の間に公約数が存在しないため、出力されるハッシュ値の均一性は明らかに向上します。
$$
\begin{aligned}
@@ -74,71 +74,71 @@ $$
\end{aligned}
$$
`key`がランダムで均等に分散されることが保証されている場合、剰余として素数または合成数を選択しても、両方とも均に分散されたハッシュ値を生成できることは注目に値します。しかし、`key`の分布にある種の周期性がある場合、合成数剰余はクラスタリングを引き起こしやすくなります。
補足すると、`key` がランダムかつ均一に分布していると保証できるなら、法に素数を選んでも合成数を選んでも構いません。どちらでも均に分布したハッシュ値を出力できます。しかし、`key` の分布に何らかの周期性がある場合、合成数剰余を取るほうが偏りが生じやすくなります。
する、通常は素数を剰余として選択し、この素数は周期的パターンを可能な限り排除し、ハッシュアルゴリズムの堅牢性を向上させるために十分大きくある必要があります。
要する、通常は法として素数を選び、その素数はできるだけ大きいほうが望ましいです。そうすることで周期的パターンをできる限り取り除き、ハッシュアルゴリズムの堅牢性を高められます。
## 一般的なハッシュアルゴリズム
記で言及した簡単なハッシュアルゴリズムはかなり「脆弱」で、ハッシュアルゴリズムの設計目標から程遠いことは難しくありません。例えば、加算とXORは交換法則に従うため、加算ハッシュとXORハッシュは同じ内容だが順序が異なる文字列を区別できず、ハッシュ衝突を悪化させ、セキュリティ問題を引き起こす可能性があります。
で紹介した単純なハッシュアルゴリズムは、どれも比較的「脆弱」であり、ハッシュアルゴリズムの設計目標にはほど遠いことが分かります。たとえば、加算と XOR は交換法則を満たすため、加算ハッシュと XOR ハッシュでは、内容が同じで順序だけ異なる文字列を区別できません。これはハッシュ衝突を悪化させ、一部の安全上の問題を引き起こす可能性があります。
実際には、通常MD5、SHA-1、SHA-2、SHA-3などの標準ハッシュアルゴリズムを使用します。これらは任意の長さの入力データを固定長のハッシュ値にマッピングできます。
実際には、MD5、SHA-1、SHA-2、SHA-3 などの標準的なハッシュアルゴリズムを用いることが一般的です。これらは任意の入力データを固定長のハッシュ値へ写像できます。
過去1世紀にわたって、ハッシュアルゴリズムは継続的なアップグレードと最適化のプロセスにありました。一部の研究者はハッシュアルゴリズムの性能向上に努め、ハッカーを含む他の人々はハッシュアルゴリズムのセキュリティ問題を見つけることに専念しています。以下の表は、実用的なアプリケーションで一般的に使用されるハッシュアルゴリズムを示しています。
ここ 1 世紀近くの間、ハッシュアルゴリズムは継続的に改良と最適化が進められてきました。ある研究者たちは性能向上に取り組み、別の研究者やハッカーたちは安全性の弱点を探し続けてきました。次の表は、実際の応用でよく使われるハッシュアルゴリズムを示したものです。
- MD5SHA-1は複数回攻撃に成功しており、さまざまなセキュリティアプリケーションで放棄されています。
- SHA-2シリーズ、特にSHA-256は、現在最も安全なハッシュアルゴリズムの1つで、成功した攻撃は報告されておらず、さまざまなセキュリティアプリケーションとプロトコルで一般的に使用されています。
- SHA-3SHA-2と比較して実装コストが低く、計算効率高いですが、現在の使用範囲はSHA-2シリーズほど広範囲ではありません。
- MD5SHA-1 は何度も攻撃に成功されているため、各種のセキュリティ用途では廃止されています。
- SHA-2 系列の SHA-256 は最も安全なハッシュアルゴリズムの 1 つであり、いまだに成功した攻撃例がないため、多くのセキュリティ用途やプロトコルで広く使われています。
- SHA-3SHA-2 と比て実装コストが低く、計算効率高い一方で、現時点での普及度は SHA-2 系列に及びません。
<p align="center"> 表 <id> &nbsp; 一般的なハッシュアルゴリズム </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の代替として使用可能 |
| | 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()`関数を使用してさまざまなデータ型のハッシュ値を計算できます。
ご存じのように、ハッシュテーブルの `key` には整数、小数、文字列などのデータ型を使えます。プログラミング言語は通常、これらのデータ型に対して組み込みのハッシュアルゴリズムを提供し、ハッシュテーブル内のバケットインデックス計算に利用します。Python を例にると、`hash()` 関数を呼び出して各種データ型のハッシュ値を計算できます。
- 整数とブール値のハッシュ値は、それら自身の値です。
- 浮動小数点数と文字列のハッシュ値の計算はより複雑で、興味ある読者は自分で研究することをお勧めします
- タプルのハッシュ値は、その各要素のハッシュ値の組み合わせで、単一のハッシュ値になります。
- オブジェクトのハッシュ値は、そのメモリアドレスに基づいて生成されます。オブジェクトのハッシュメソッドをオーバーライドすることで、内容に基づいてハッシュ値を生成できます。
- 整数と真理値のハッシュ値は、その値自身です。
- 浮動小数点数と文字列のハッシュ値の計算はやや複雑なので、興味ある読者は自分で調べてみてください
- タプルのハッシュ値は、各要素のハッシュ値を求めてから、それらを組み合わせて 1 つのハッシュ値にしたものです。
- オブジェクトのハッシュ値は、そのメモリアドレスに基づいて生成されます。オブジェクトのハッシュメソッドをオーバーライドすれば、内容に基づハッシュ値を実装できます。
!!! tip
異なるプログラミング言語における組み込みハッシュ値計算関数の定義方法は異なることに注意してください
注意してください。組み込みハッシュ値計算関数の定義方法は、プログラミング言語ごとに異なります
=== "Python"
```python title="built_in_hash.py"
num = 3
hash_num = hash(num)
# 整数3のハッシュ値は3
# 整数 3 のハッシュ値は 3
bol = True
hash_bol = hash(bol)
# ブール値Trueのハッシュ値は1
# 真理値 True のハッシュ値は 1
dec = 3.14159
hash_dec = hash(dec)
# 小数3.14159のハッシュ値は326484311674566659
# 小数 3.14159 のハッシュ値は 326484311674566659
str = "Hello 算法"
str = "Hello アルゴリズム"
hash_str = hash(str)
# 文字列"Hello 算法"のハッシュ値は4617003410720528961
# 文字列Hello アルゴリズム」のハッシュ値は 4617003410720528961
tup = (12836, "小哈")
tup = (12836, "シャオハ")
hash_tup = hash(tup)
# タプル(12836, '小哈')のハッシュ値は1029005403108185979
# タプル (12836, 'シャオハ') のハッシュ値は 1029005403108185979
obj = ListNode(0)
hash_obj = hash(obj)
# ListNodeオブジェクト0x1058fd810のハッシュ値は274267521
# ノードオブジェクト <ListNode object at 0x1058fd810> のハッシュ値は 274267521
```
=== "C++"
@@ -146,22 +146,22 @@ $$
```cpp title="built_in_hash.cpp"
int num = 3;
size_t hashNum = hash<int>()(num);
// 整数3のハッシュ値は3
// 整数 3 のハッシュ値は 3
bool bol = true;
size_t hashBol = hash<bool>()(bol);
// ブール値1のハッシュ値は1
// 真理値 1 のハッシュ値は 1
double dec = 3.14159;
size_t hashDec = hash<double>()(dec);
// 小数3.14159のハッシュ値は4614256650576692846
// 小数 3.14159 のハッシュ値は 4614256650576692846
string str = "Hello 算法";
string str = "Hello アルゴリズム";
size_t hashStr = hash<string>()(str);
// 文字列"Hello 算法"のハッシュ値は15466937326284535026
// 文字列Hello アルゴリズム」のハッシュ値は 15466937326284535026
// C++では、組み込みstd::hash()は基本データ型のハッシュ値のみを提供
// 配列オブジェクトのハッシュ値は別途実装が必要
// C++ では、組み込みstd:hash() は基本データ型のハッシュ値計算のみを提供する
// 配列オブジェクトのハッシュ値計算は自分で実装する必要がある
```
=== "Java"
@@ -169,27 +169,27 @@ $$
```java title="built_in_hash.java"
int num = 3;
int hashNum = Integer.hashCode(num);
// 整数3のハッシュ値は3
// 整数 3 のハッシュ値は 3
boolean bol = true;
int hashBol = Boolean.hashCode(bol);
// ブール値trueのハッシュ値は1231
// 真理値 true のハッシュ値は 1231
double dec = 3.14159;
int hashDec = Double.hashCode(dec);
// 小数3.14159のハッシュ値は-1340954729
// 小数 3.14159 のハッシュ値は -1340954729
String str = "Hello 算法";
String str = "Hello アルゴリズム";
int hashStr = str.hashCode();
// 文字列"Hello 算法"のハッシュ値は-727081396
// 文字列Hello アルゴリズム」のハッシュ値は -727081396
Object[] arr = { 12836, "小哈" };
Object[] arr = { 12836, "シャオハ" };
int hashTup = Arrays.hashCode(arr);
// 配列[12836, 小哈]のハッシュ値は1151158
// 配列 [12836, シャオハ] のハッシュ値は 1151158
ListNode obj = new ListNode(0);
int hashObj = obj.hashCode();
// ListNodeオブジェクトutils.ListNode@7dc5e7b4のハッシュ値は2110121908
// ノードオブジェクト utils.ListNode@7dc5e7b4 のハッシュ値は 2110121908
```
=== "C#"
@@ -197,33 +197,33 @@ $$
```csharp title="built_in_hash.cs"
int num = 3;
int hashNum = num.GetHashCode();
// 整数3のハッシュ値は3;
// 整数 3 のハッシュ値は 3;
bool bol = true;
int hashBol = bol.GetHashCode();
// ブール値trueのハッシュ値は1;
// 真理値 true のハッシュ値は 1;
double dec = 3.14159;
int hashDec = dec.GetHashCode();
// 小数3.14159のハッシュ値は-1340954729;
// 小数 3.14159 のハッシュ値は -1340954729;
string str = "Hello 算法";
string str = "Hello アルゴリズム";
int hashStr = str.GetHashCode();
// 文字列"Hello 算法"のハッシュ値は-586107568;
// 文字列Hello アルゴリズム」のハッシュ値は -586107568;
object[] arr = [12836, "小哈"];
object[] arr = [12836, "シャオハ"];
int hashTup = arr.GetHashCode();
// 配列[12836, 小哈]のハッシュ値は42931033;
// 配列 [12836, シャオハ] のハッシュ値は 42931033;
ListNode obj = new(0);
int hashObj = obj.GetHashCode();
// ListNodeオブジェクト0のハッシュ値は39053774;
// ノードオブジェクト 0 のハッシュ値は 39053774;
```
=== "Go"
```go title="built_in_hash.go"
// Goは組み込みのハッシュコード関数提供されていません
// Go は組み込みの hash code 関数提供していない
```
=== "Swift"
@@ -231,39 +231,39 @@ $$
```swift title="built_in_hash.swift"
let num = 3
let hashNum = num.hashValue
// 整数3のハッシュ値は9047044699613009734
// 整数 3 のハッシュ値は 9047044699613009734
let bol = true
let hashBol = bol.hashValue
// ブール値trueのハッシュ値は-4431640247352757451
// 真理値 true のハッシュ値は -4431640247352757451
let dec = 3.14159
let hashDec = dec.hashValue
// 小数3.14159のハッシュ値は-2465384235396674631
// 小数 3.14159 のハッシュ値は -2465384235396674631
let str = "Hello 算法"
let str = "Hello アルゴリズム"
let hashStr = str.hashValue
// 文字列"Hello 算法"のハッシュ値は-7850626797806988787
// 文字列Hello アルゴリズム」のハッシュ値は -7850626797806988787
let arr = [AnyHashable(12836), AnyHashable("小哈")]
let arr = [AnyHashable(12836), AnyHashable("シャオハ")]
let hashTup = arr.hashValue
// 配列[AnyHashable(12836), AnyHashable("小哈")]のハッシュ値は-2308633508154532996
// 配列 [AnyHashable(12836), AnyHashable("シャオハ")] のハッシュ値は -2308633508154532996
let obj = ListNode(x: 0)
let hashObj = obj.hashValue
// ListNodeオブジェクトutils.ListNodeのハッシュ値は-2434780518035996159
// ノードオブジェクト utils.ListNode のハッシュ値は -2434780518035996159
```
=== "JS"
```javascript title="built_in_hash.js"
// JavaScriptは組み込みのハッシュコード関数提供されていません
// JavaScript は組み込みの hash code 関数提供していない
```
=== "TS"
```typescript title="built_in_hash.ts"
// TypeScriptは組み込みのハッシュコード関数提供されていません
// TypeScript は組み込みの hash code 関数提供していない
```
=== "Dart"
@@ -271,27 +271,27 @@ $$
```dart title="built_in_hash.dart"
int num = 3;
int hashNum = num.hashCode;
// 整数3のハッシュ値は34803
// 整数 3 のハッシュ値は 34803
bool bol = true;
int hashBol = bol.hashCode;
// ブール値trueのハッシュ値は1231
// 真理値 true のハッシュ値は 1231
double dec = 3.14159;
int hashDec = dec.hashCode;
// 小数3.14159のハッシュ値は2570631074981783
// 小数 3.14159 のハッシュ値は 2570631074981783
String str = "Hello 算法";
String str = "Hello アルゴリズム";
int hashStr = str.hashCode;
// 文字列"Hello 算法"のハッシュ値は468167534
// 文字列Hello アルゴリズム」のハッシュ値は 468167534
List arr = [12836, "小哈"];
List arr = [12836, "シャオハ"];
int hashArr = arr.hashCode;
// 配列[12836, 小哈]のハッシュ値は976512528
// 配列 [12836, シャオハ] のハッシュ値は 976512528
ListNode obj = new ListNode(0);
int hashObj = obj.hashCode;
// ListNodeオブジェクトInstance of 'ListNode'のハッシュ値は1033450432
// ノードオブジェクト Instance of 'ListNode' のハッシュ値は 1033450432
```
=== "Rust"
@@ -304,53 +304,107 @@ $$
let mut num_hasher = DefaultHasher::new();
num.hash(&mut num_hasher);
let hash_num = num_hasher.finish();
// 整数3のハッシュ値は568126464209439262
// 整数 3 のハッシュ値は 568126464209439262
let bol = true;
let mut bol_hasher = DefaultHasher::new();
bol.hash(&mut bol_hasher);
let hash_bol = bol_hasher.finish();
// ブール値trueのハッシュ値は4952851536318644461
// 真理値 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
// 小数 3.14159 のハッシュ値は 2566941990314602357
let str = "Hello 算法";
let str = "Hello アルゴリズム";
let mut str_hasher = DefaultHasher::new();
str.hash(&mut str_hasher);
let hash_str = str_hasher.finish();
// 文字列"Hello 算法"のハッシュ値は16092673739211250988
// 文字列Hello アルゴリズム」のハッシュ値は 16092673739211250988
let arr = (&12836, &"小哈");
let arr = (&12836, &"シャオハ");
let mut tup_hasher = DefaultHasher::new();
arr.hash(&mut tup_hasher);
let hash_tup = tup_hasher.finish();
// タプル(12836, "小哈")のハッシュ値は1885128010422702749
// タプル (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
// ノードオブジェクト RefCell { value: ListNode { val: 42, next: None } } のハッシュ値は15387811073369036852
```
=== "C"
```c title="built_in_hash.c"
// Cは組み込みのハッシュコード関数提供されていません
// C は組み込みの hash code 関数提供していない
```
=== "Kotlin"
```kotlin title="built_in_hash.kt"
val num = 3
val hashNum = num.hashCode()
// 整数 3 のハッシュ値は 3
val bol = true
val hashBol = bol.hashCode()
// 真理値 true のハッシュ値は 1231
val dec = 3.14159
val hashDec = dec.hashCode()
// 小数 3.14159 のハッシュ値は -1340954729
val str = "Hello アルゴリズム"
val hashStr = str.hashCode()
// 文字列「Hello アルゴリズム」のハッシュ値は -727081396
val arr = arrayOf<Any>(12836, "シャオハ")
val hashTup = arr.hashCode()
// 配列 [12836, シャオハ] のハッシュ値は 189568618
val obj = ListNode(0)
val hashObj = obj.hashCode()
// ノードオブジェクト utils.ListNode@1d81eb93 のハッシュ値は 495053715
```
多くのプログラミング言語では、**不変オブジェクトのみがハッシュ表の`key`として機能できます**。リスト(動的配列)を`key`として使用する場合、リストの内容が変更されると、そのハッシュ値も変更され、ハッシュ表で元の`value`を見つけることができなくなります。
=== "Ruby"
カスタムオブジェクト(連結リストノードなど)のメンバー変数は可変ですが、ハッシュ可能です。**これは、オブジェクトのハッシュ値が通常そのメモリアドレスに基づいて生成されるためです**。オブジェクトの内容が変更されても、メモリアドレスは同じままなので、ハッシュ値は変更されません。
```ruby title="built_in_hash.rb"
num = 3
hash_num = num.hash
# 整数 3 のハッシュ値は -4385856518450339636
異なるコンソールで出力されるハッシュ値が異なることに気づいたかもしれません。**これは、Pythonインタープリターが起動するたびに文字列ハッシュ関数にランダムソルトを追加するためです**。このアプローチはHashDoS攻撃を効果的に防ぎ、ハッシュアルゴリズムのセキュリティを向上させます。
bol = true
hash_bol = bol.hash
# 真理値 true のハッシュ値は -1617938112149317027
dec = 3.14159
hash_dec = dec.hash
# 小数 3.14159 のハッシュ値は -1479186995943067893
str = "Hello アルゴリズム"
hash_str = str.hash
# 文字列「Hello アルゴリズム」のハッシュ値は -4075943250025831763
tup = [12836, 'シャオハ']
hash_tup = tup.hash
# タプル (12836, 'シャオハ') のハッシュ値は 1999544809202288822
obj = ListNode.new(0)
hash_obj = obj.hash
# ノードオブジェクト #<ListNode:0x000078133140ab70> のハッシュ値は 4302940560806366381
```
??? pythontutor "可視化実行"
https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20num%20%3D%203%0A%20%20%20%20hash_num%20%3D%20hash%28num%29%0A%20%20%20%20%23%20%E6%95%B4%E6%95%B0%203%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%203%0A%0A%20%20%20%20bol%20%3D%20True%0A%20%20%20%20hash_bol%20%3D%20hash%28bol%29%0A%20%20%20%20%23%20%E5%B8%83%E5%B0%94%E9%87%8F%20True%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201%0A%0A%20%20%20%20dec%20%3D%203.14159%0A%20%20%20%20hash_dec%20%3D%20hash%28dec%29%0A%20%20%20%20%23%20%E5%B0%8F%E6%95%B0%203.14159%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%80%9CHello%20%E7%AE%97%E6%B3%95%E2%80%9D%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836,%20%22%E5%B0%8F%E5%93%88%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%E5%85%83%E7%BB%84%20%2812836,%20'%E5%B0%8F%E5%93%88'%29%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201029005403108185979%0A%0A%20%20%20%20obj%20%3D%20ListNode%280%29%0A%20%20%20%20hash_obj%20%3D%20hash%28obj%29%0A%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%AF%B9%E8%B1%A1%20%3CListNode%20object%20at%200x1058fd810%3E%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20274267521&cumulative=false&curInstr=19&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
多くのプログラミング言語では、**不変オブジェクトだけがハッシュテーブルの `key` として使えます**。仮にリスト(動的配列)を `key` とすると、その内容が変化したときにハッシュ値も変わってしまうため、もとの `value` をハッシュテーブルから検索できなくなります。
カスタムオブジェクト(たとえば連結リストのノード)のメンバ変数は可変ですが、それでもハッシュ可能です。**これは、オブジェクトのハッシュ値が通常はメモリアドレスに基づいて生成されるためです**。オブジェクトの内容が変化しても、メモリアドレスが変わらなければ、ハッシュ値も変わりません。
注意深い人なら、異なるコンソールでプログラムを実行したときに、出力されるハッシュ値が異なることに気づくかもしれません。**これは、Python インタプリタが起動のたびに文字列ハッシュ関数へランダムな salt 値を追加しているためです**。この方法によって HashDoS 攻撃を効果的に防ぎ、ハッシュアルゴリズムの安全性を高めています。
+53 -53
View File
@@ -1,108 +1,108 @@
# ハッシュ衝突
前節で述べたように、**ほとんどの場合、ハッシュ関数の入力空間は出力空間よりもはるかに大きい**ため、理論的にはハッシュ衝突は避けられません。例えば、入力空間がすべての整数で、出力空間が配列容量サイズの場合、複数の整数が必然的に同じバケットインデックスにマッピングされます。
前節で述べたように、**通常、ハッシュ関数の入力空間は出力空間よりもはるかに大きい**ため、理論ハッシュ衝突は避けられません。例えば、入力空間がすべての整数で、出力空間が配列容量サイズである場合、必然的に複数の整数が同じバケットインデックスに写像されます。
ハッシュ衝突は誤ったクエリ結果につながり、ハッシュ表の使いやすさに深刻な影響を与える可能性があります。この問題に対処するために、ハッシュ衝突が発生するたびに、衝突が消えるまでハッシュ表のリサイズを実行します。このアプローチは非常にシンプルで直接的であり、うまく機能します。しかし、テーブルの拡張には大量のデータ移とハッシュコードの再計算が含まれ、これらは高コストであるため、非常に非効率的に見えます。効率を向上させるために、以下の戦略を採用できます
ハッシュ衝突は検索結果の誤りを招き、ハッシュテーブルの利用可能性に深刻な影響を与えます。この問題を解決するために、ハッシュ衝突が発生するたびにハッシュテーブルを拡張し、衝突が消えるまで続けることが考えられます。この方法は単純で効果的ですが、効率が低すぎます。なぜなら、ハッシュテーブルの拡張には大量のデータ移とハッシュ値の計算が必要だからです。効率を高めるために、の戦略を採用できます
1. **ハッシュ衝突が発生した場合でも、ターゲット要素の検索が適切に機能する**ようにハッシュ表のデータ構造を改善する
2. 深刻な衝突が観察され、必要になる前に、拡張は最後の手段とする。
1. ハッシュテーブルのデータ構造を改良し、**ハッシュ衝突が発生してもハッシュテーブルが正常に動作できるようにする**
2. 必要な場合、すなわちハッシュ衝突が比較的深刻なときにのみ、拡張操作を実行する。
ハッシュの構造改善する主な方法は2つあります:「連鎖法」と「オープンアドレス法」です。
ハッシュテーブルの構造改善方法には、主に「チェイン法」と「オープンアドレッシング」があります。
## 連鎖
## チェイン
元のハッシュでは、各バケットは1つのキー値ペアのみを格納できま。<u>連鎖法</u>単一要素を連結リストに変換し、キー値ペアをリストノードとして扱い、衝突するすべてのキーペアを同じ連結リストに格納します。下図は連鎖法を使用したハッシュ表の例を示しています。
元のハッシュテーブルでは、各バケットには 1 つのキーと値のペアしか格納できません。<u>チェイン法(separate chaining</u>では、単一要素を連結リストに置き換え、キーと値のペアを連結リストノードとして扱い、衝突したすべてのキーと値のペアを同じ連結リストに格納します。下図はチェイン法によるハッシュテーブルの例を示しています。
![連鎖法ハッシュ](hash_collision.assets/hash_table_chaining.png)
![チェイン法ハッシュテーブル](hash_collision.assets/hash_table_chaining.png)
連鎖法で実装されたハッシュ表の操作は以下のように変更されます
チェイン法で実装されたハッシュテーブルでは、操作方法が次のように変わります
- **要素のクエリ**: `key`入力し、ハッシュ関数通してバケットインデックスを取得し、連結リストのヘッドノードにアクセスます。連結リストを走査してキーを比較し、ターゲットキー値ペアを見つけます。
- **要素の追加**: ハッシュ関数を通して連結リストのヘッドノードにアクセスし、ノード(キーペア)をリストに追加します。
- **要素の削除**: ハッシュ関数の結果に基づいて連結リストのヘッドにアクセスし、連結リストを走査してターゲットノードを見つけて削除します。
- **要素の検索**:入力 `key` をハッシュ関数通してバケットインデックスを得ると、連結リストの先頭ノードにアクセスできます。その後、連結リストを走査して `key` を比較し、目的のキーと値のペアを探します。
- **要素の追加**:まずハッシュ関数連結リストの先頭ノードにアクセスし、その後ノード(キーと値のペア)を連結リストに追加します。
- **要素の削除**ハッシュ関数の結果に基づいて連結リストの先頭にアクセスし、続いて連結リストを走査して対象ノードを探し、削除します。
連鎖法には以下の制があります
チェイン法にはの制があります
- **空間使用量の増加**: 連結リストにはノードポインタが含まれており、配列よりも多くのメモリ空間を消費します。
- **クエリ効率の低下**: 対応する要素を見つけるために連結リスト線形走査が必要になるためです。
- **使用メモリの増加**連結リストにはノードポインタが含まれるため、配列よりも多くのメモリを消費します。
- **検索効率の低下**対応する要素を見つけるために連結リスト線形走査する必要があるためです。
以下のコードは連鎖法ハッシュの簡単な実装を提供し、注意すべき2つの点があります
以下のコードはチェイン法ハッシュテーブルの簡単な実装を示しています。注意すべき点は 2 つあります
- 簡単にするために、連結リストの代わりにリスト(動的配列)を使用します。この設定では、ハッシュ(配列)は複数のバケットを含み、各バケットはリストです。
- の実装にはハッシュ表のリサイズメソッドが含まれています。負荷率が$\frac{2}{3}$を超えると、ハッシュ表を元のサイズの2倍に拡張します。
- 連結リストの代わりにリスト(動的配列)を使って、コードを簡潔にしています。この設定では、ハッシュテーブル(配列)は複数のバケットを含み、各バケットは 1 つのリストです。
- 以下の実装にはハッシュテーブルの拡張メソッドが含まれています。負荷率が $\frac{2}{3}$ を超えたとき、ハッシュテーブルを元の $2$ 倍に拡張します。
```src
[file]{hash_map_chaining}-[class]{hash_map_chaining}-[func]{}
```
連結リストが非常に長い場合、クエリ効率$O(n)$が悪いことは注目に値します。**この場合、リストを「AVL木」または「赤黒木」に変換して**、クエリ操作の時間計算量を$O(\log n)$に最適化できます。
注意すべきなのは、連結リストが長い場合、検索効率 $O(n)$ は非常に低いことです。**このとき、連結リストを「AVL 木」または「赤黒木」に変換することで**、検索操作の時間計算量を $O(\log n)$ に最適化できます。
## オープンアドレス法
## オープンアドレッシング
<u>オープンアドレス法</u>は追加のデータ構造を導入せず、代わりに「複数回プローブ」を通してハッシュ衝突を処理します。プローブ方法には主に線形プローブ、二次プローブ、二重ハッシュがあります。
<u>オープンアドレッシング(open addressing</u>は追加のデータ構造を導入せず、「複数回の探索」によってハッシュ衝突を処理します。探索方法には主に線形探索、二次探索、多重ハッシュなどがあります。
線形プローブを例にして、オープンアドレス法ハッシュ表のメカニズムを紹介しましょう
以下では線形探索を例に、オープンアドレッシングハッシュテーブルの動作の仕組みを説明します
### 線形プローブ
### 線形探索
線形プローブは固定ステップの線形検索をプローブに使用し、通常のハッシュとは異なります。
線形探索では、固定ステップの線形探索によって探索を行います。その操作方法は通常のハッシュテーブルとは異なります。
- **要素の挿入**: ハッシュ関数を使用してバケットインデックスを計算します。バケットに既に要素が含まれている場合、衝突位置から線形に前方に走査し(通常ステップサイズは$1$)、空のバケットが見つかるまで進み、要素を挿入します。
- **要素の検索**: ハッシュ衝突に遭遇した場合、同じステップサイズを使用して線形に前方に走査し、対応する要素が見つかったら`value`を返します。空のバケットに遭遇した場合、ターゲット要素がハッシュ表にないことを意味するため、`None`を返します。
- **要素の挿入**ハッシュ関数によってバケットインデックスを計算しバケット内にすでに要素がある場合、衝突位置から後方へ線形に走査し(ステップ長は通常 $1$ )、空のバケットが見つかるまで進み、その中に要素を挿入します。
- **要素の検索**ハッシュ衝突が見つかった場合、同じステップ長で後方へ線形走査を行い、対応する要素が見つかるまで続け、 `value` を返します。空のバケットに遭遇した場合は、対象要素がハッシュテーブル内に存在しないことを意味するため、 `None` を返します。
下図はオープンアドレス法(線形プローブ)ハッシュにおけるキーペアの分布を示しています。このハッシュ関数によると、下二桁が同じキーは同じバケットにマッピングされます。線形プローブを通して、それらはそのバケットとそののバケットに順格納されます。
下図はオープンアドレッシング(線形探索)ハッシュテーブルにおけるキーと値のペアの分布を示しています。このハッシュ関数では、末尾 2 桁が同じ `key` はすべて同じバケットに写像されます。線形探索によって、それらはそのバケットとその後続のバケットに順格納されます。
![オープンアドレス法(線形プローブ)ハッシュにおけるキーペアの分布](hash_collision.assets/hash_table_linear_probing.png)
![オープンアドレッシング(線形探索)ハッシュテーブルにおけるキーと値のペアの分布](hash_collision.assets/hash_table_linear_probing.png)
しかし、**線形プローブは「クラスタリング」を作りやすい傾向があります**。具体的には、配列内連続的に占有された位置が長いほど、れらの連続した位置でハッシュ衝突が発生する確率が高くなり、その位置でのクラスタリングの成長をさらに促進し、悪循環を形成し、最終的に挿入、削除、クエリ、更新操作の効率低下につながります。
しかし、**線形探索では「クラスタリング現象」が起こりやすい**です。具体的には、配列内連続して占有された位置が長いほど、れらの連続位置でハッシュ衝突が発生する可能性が高くなり、さらにその位置の集積成長を促して悪循環を生み、最終的には追加・削除・検索・更新操作の効率低下を招きます。
**オープンアドレス法ハッシュ表では要素を直接削除できない**ことに注意することが重要です。要素を削除すると配列に空バケット`None`が作成されます。要素を検索する際、線形プローブがこの空バケットに遭遇すると戻ってしまい、このバケットの下の要素にアクセスできなくなります。プログラムはこれらの要素存在しないと誤って仮定する可能性があります。下図に示すとおりです。
注意すべきなのは、**オープンアドレッシングハッシュテーブルでは要素を直接削除できない**ことです。これは、要素を削除すると配列に空バケット `None` が生じ、要素を検索するときに線形探索がその空バケットに到達した時点で返ってしまうため、その空バケットより後ろの要素には二度とアクセスできなくなるからです。結果として、プログラムがそれらの要素存在しないと誤定する可能性があります。下図とおりです。
![オープンアドレス法での削除によるクエリ問題](hash_collision.assets/hash_table_open_addressing_deletion.png)
![オープンアドレッシングで要素を削除したことによる検索問題](hash_collision.assets/hash_table_open_addressing_deletion.png)
この問題を解決するために、<u>遅延削除</u>メカニズムを採用できます:ハッシュ表から要素を直接削除する代わりに、**定数`TOMBSTONE`を使用してバケットをマークします**。このメカニズムでは、`None``TOMBSTONE`の両方が空のバケットを表し、キー値ペアを保持できます。ただし、線形プローブが`TOMBSTONE`に遭遇した場合、その下にまだキー値ペアがある可能性があるため、走査を続ける必要があります。
この問題を解決するために、<u>遅延削除lazy deletion</u>の仕組みを採用できます。これは要素をハッシュテーブルから直接取り除かず、**代わりに定数 `TOMBSTONE` を使ってこのバケットをマークします**。この仕組みでは、`None``TOMBSTONE` はどちらも空バケットを表し、どちらにもキーと値のペアを配置できます。ただし異なるのは、線形探索が `TOMBSTONE` に到達した場合、その先にキーと値のペアが存在する可能性があるため、探索を続けるべきだという点です。
しかし、**遅延削除はハッシュの性能劣化を加速る可能性があります**。削除操作のたびに削除マークが生成され、`TOMBSTONE`が増加すると、線形プローブがターゲット要素を見つけるために複数の`TOMBSTONE`をスキップする必要がある可能性があるため、検索時間も増加します
しかし、**遅延削除はハッシュテーブルの性能劣化を加速させる可能性があります**。これは、削除操作のたびに削除マークが 1 つ生成され、`TOMBSTONE` が増えるにつれて探索時間も増加するためです。線形探索では、対象要素を見つけるまでに複数の `TOMBSTONE` を飛び越える必要があるかもしれません
これに対処するため、線形プローブ中に最初に遭遇した`TOMBSTONE`のインデックスを記録し、検索されたターゲット要素とその`TOMBSTONE`の位置を交換することを検討してください。これを行う利点は、要素がクエリまたは追加されるたびに、要素がその理想的な位置(プローブの開始点)により近いバケット移動され、クエリ効率が最適化されることです。
そのため、線形探索では、遭遇した最初の `TOMBSTONE` のインデックスを記録し、見つかった対象要素とその `TOMBSTONE` を交換することを考えます。こうする利点は、要素を検索または追加るたびに、要素が理想位置(探索開始点)により近いバケット移動し、検索効率が向上することです。
以下のコードは、遅延削除を使用したオープンアドレス法(線形プローブ)ハッシュ表を実装しています。ハッシュの空間をより有効に活用するために、ハッシュ表を「循環配列」として扱います。配列の終わりを超えると、最初に戻って走査を続けます。
以下のコードは、遅延削除を含むオープンアドレッシング(線形探索)ハッシュテーブルを実装したものです。ハッシュテーブルの空間をより十分に活用するために、ハッシュテーブルを「環状配列」とみなし、配列末尾を越えたら先頭に戻って探索を続けます。
```src
[file]{hash_map_open_addressing}-[class]{hash_map_open_addressing}-[func]{}
```
### 二次プローブ
### 二次探索
二次プローブは線形プローブに似ており、オープンアドレス法の一般的な戦略の1つです。衝突が発生した場合、二次プローブは単純に固定ステップ数をスキップするのではなく、「プローブ回数の二乗」に等しいステップ数、つまり$1, 4, 9, \dots$ステップをスキップします。
二次探索は線形探索に似ており、オープンアドレッシングの一般的な戦略の 1 つです。衝突が発生したとき、二次探索では単純に固定歩数を飛ばすのではなく、「探索回数の二乗」に相当する歩数、すなわち $1, 4, 9, \dots$ 歩を飛ばします。
二次プローブには以下の利点があります
二次探索には主に次の利点があります
- 二次プローブは、プローブ回数の二乗の距離をスキップすることで、線形プローブのクラスタリング効果を軽減しようとします。
- 二次プローブはより大きな距離をスキップして空位置を見つけ、データをより均等に分散するのに役立ちます。
- 二次探索は、探索回数の二乗の距離を飛ばすことで、線形探索のクラスタリング効果を緩和しようとします。
- 二次探索はより大きな距離を飛ばして空位置を探すため、データ分布がより均一になるのに役立ちます。
しかし、二次プローブは完璧ではありません
しかし、二次探索は完璧ではありません
- クラスタリングは依然として存在し、つまり一部の位置他の位置より占有される可能性が高いです。
- 二乗の成長により、二次プローブはハッシュ表全体をプローブできない可能性があり、ハッシュに空バケットがあっても、二次プローブがアクセスできない可能性があります。
- 依然としてクラスタリング現象は存在し、ある位置他の位置より占有されやすいことがあります。
- 二乗の増加により、二次探索はハッシュテーブル全体を探索できない可能性があります。これは、ハッシュテーブルに空バケットがあっても、二次探索ではそこに到達できないことがあることを意味します。
### 重ハッシュ
### 重ハッシュ
名前が示すように、二重ハッシュ法は複数のハッシュ関数$f_1(x)$、$f_2(x)$、$f_3(x)$、$\dots$をプローブに使用します。
その名のとおり、多重ハッシュ法は複数のハッシュ関数 $f_1(x)$、$f_2(x)$、$f_3(x)$、$\dots$ を使って探索を行います。
- **要素の挿入**: ハッシュ関数$f_1(x)$が衝突に遭遇した場合、$f_2(x)$を試し、以下同様に、空位置が見つかって要素挿入されるまで続けます。
- **要素の検索**: 同じハッシュ関数の順序で索し、ターゲット要素が見つかって返されるまで、または空の位置に遭遇するかすべてのハッシュ関数が試されるまで続け、要素がハッシュ表にないことを示し、`None`を返します。
- **要素の挿入**ハッシュ関数 $f_1(x)$ で衝突が発生した場合、$f_2(x)$ を試し、以下同様に、空位置が見つかるまで続けてから要素挿入ます。
- **要素の検索**同じハッシュ関数の順序で索し、対象要素が見つかった時点で返します。空き位置に遭遇するかすべてのハッシュ関数を試しても見つからない場合は、ハッシュテーブル内にその要素は存在しないため、 `None` を返します。
線形プローブと比較して、二重ハッシュ法はクラスタリング起こにくいですが、複数のハッシュ関数追加の計算オーバーヘッドを導入します。
線形探索と比べると、多重ハッシュ法はクラスタリング起こにくい一方で、複数のハッシュ関数により追加の計算量が発生します。
!!! tip
オープンアドレス法(線形プローブ、二次プローブ、二重ハッシュ)ハッシュ表はすべて「要素を直接削除できない」という問題があることに注意してください
注意してください。オープンアドレッシング(線形探索、二次探索、多重ハッシュ)ハッシュテーブルには、いずれも「要素を直接削除できない」という問題があります
## プログラミング言語の選択
異なるプログラミング言語は異なるハッシュ実装戦略を採用しています。以下にいくつか例を示します
各種プログラミング言語は異なるハッシュテーブル実装戦略を採用しています。以下にいくつか例を挙げます
- Pythonはオープンアドレス法を使用します。`dict`辞書はプローブに疑似乱数を使用します。
- Javaは連鎖法を使用します。JDK 1.8以降、`HashMap`の配列長が64に達し、連結リストの長さが8に達すると、連結リストは検索性能を向上させるため赤黒木に変換されます。
- Goは連鎖法を使用します。Goは各バケット最大8つのキー値ペアを格納できることを規定し、容量を超えた場合はオーバーフローバケット連結されます。オーバーフローバケットが多すぎる場合、性能を確保するために特な等容量リサイズ操作実行されます。
- Python はオープンアドレッシングを採用しています。辞書 `dict` は疑似乱数を用いて探索します。
- Java はチェイン法を用しています。JDK 1.8 以降、`HashMap`の配列長が 64 に達し、かつ連結リスト長が 8 に達すると、連結リストは検索性能を高めるため赤黒木に変換されます。
- Go はチェイン法を用しています。Goは各バケット最大 8 個のキーと値のペアを格納でき、容量を超えるとオーバーフローバケット連結ます。オーバーフローバケットが多すぎる場合、性能を確保するために特な等量拡張操作実行ます。
+259 -185
View File
@@ -1,238 +1,238 @@
# ハッシュ
# ハッシュテーブル
<u>ハッシュ</u>は<u>ハッシュマップ</u>とも呼ばれ、キーと値の間のマッピングを確立し、効率な要素の取得を可能にするデータ構造です。具体的には、ハッシュ表に`key`を入力すると、$O(1)$の時間計算量で対応する`value`取得できます。
<u>ハッシュテーブル(hash table</u>は<u>散列表</u>とも呼ばれ、キー `key` と値 `value` の対応関係を構築することで、高効率な要素検索を実現します。具体的には、ハッシュテーブルにキー `key` を入力すると、対応する`value` を $O(1)$ 時間で取得できます。
図に示すように、$n$人の学生がい、各学生は「名前」と「学籍番号」の2つのデータフィールドがあるとします。学籍番号を入力として対応する名前を返すクエリ機能を実したい場合、下図に示すハッシュ表を使用できます。
以下の図に示すように、$n$ 人の学生がいるとし、各学生は「名前」と「学籍番号」の 2 つの情報を持っています。もし「学籍番号を入力すると対応する名前を返す」という検索機能を実したいなら、下図のようなハッシュテーブルを用いることができます。
![ハッシュの抽象的な表現](hash_map.assets/hash_table_lookup.png)
![ハッシュテーブルの抽象表現](hash_map.assets/hash_table_lookup.png)
ハッシュ表に加えて、配列や連結リストもクエリ機能の実装に使用できますが、時間計算量が異なります。効率は以下の表で比較されています
ハッシュテーブルのほかに、配列や連結リストでも検索機能を実現できます。それらの効率比較を次の表に示します
- **要素の挿入**: 配列(または連結リスト)の末尾に要素を追加するだけです。この操作の時間計算量は$O(1)$です。
- **要素の検索**: 配列(または連結リスト)がソートされていないため、要素を検索するにはすべての要素を走査する必要があります。この操作の時間計算量は$O(n)$です。
- **要素の削除**: 要素を削除するには、まずその要素を見つけてから配列(または連結リスト)から削除します。この操作の時間計算量は$O(n)$です。
- **要素の追加**:要素を配列(連結リスト)の末尾に追加するだけでよく、$O(1)$ 時間です。
- **要素の検索**配列(連結リスト)は無秩序なので、すべての要素を走査する必要があり、$O(n)$ 時間かかります。
- **要素の削除**:先に要素を検索してから配列(連結リスト)から削除する必要があり、$O(n)$ 時間かかります。
<p align="center"> 表 <id> &nbsp; 一般的な操作の時間効率の比較 </p>
<p align="center"> 表 <id> &nbsp; 要素検索効率の比較 </p>
| | 配列 | 連結リスト | ハッシュ |
| -------------- | ------ | ----------- | ---------- |
| 要素の検索 | $O(n)$ | $O(n)$ | $O(1)$ |
| 要素の挿入 | $O(1)$ | $O(1)$ | $O(1)$ |
| 要素の削除 | $O(n)$ | $O(n)$ | $O(1)$ |
| | 配列 | 連結リスト | ハッシュテーブル |
| -------- | ------ | ------ | ------ |
| 要素の検索 | $O(n)$ | $O(n)$ | $O(1)$ |
| 要素の追加 | $O(1)$ | $O(1)$ | $O(1)$ |
| 要素の削除 | $O(n)$ | $O(n)$ | $O(1)$ |
観察されるように、**ハッシュ表における操作(挿入、削除検索、変更)の時間計算量は$O(1)$**、非常に効率です。
以上から分かるように、**ハッシュテーブルにおける追加・削除検索・更新の時間計算量はいずれも $O(1)$** であり、非常に効率です。
## ハッシュ表の一般的な操作
## ハッシュテーブルの基本操作
ハッシュの一般的な操作には、初期化、クエリ、キー値ペアの追加、キーペアの削除があります。以下はコード例です:
ハッシュテーブルの一般的な操作には、初期化、検索、キーと値のペアの追加、キーと値のペアの削除などがあります。コード例は以下のとおりです:
=== "Python"
```python title="hash_map.py"
# ハッシュを初期化
# ハッシュテーブルを初期化
hmap: dict = {}
# 追加操作
# ハッシュ表にキー値ペア (key, value) を追加
hmap[12836] = "小哈"
hmap[15937] = "小啰"
hmap[16750] = "小算"
hmap[13276] = "小法"
hmap[10583] = "小鸭"
# ハッシュテーブルにキーと値のペア (key, value) を追加
hmap[12836] = "シャオハ"
hmap[15937] = "シャオルオ"
hmap[16750] = "シャオスワン"
hmap[13276] = "シャオファ"
hmap[10583] = "シャオヤー"
# クエリ操作
# ハッシュ表にキーを入力し、値を取得
# 検索操作
# ハッシュテーブルにキー key を入力し、値 value を取得
name: str = hmap[15937]
# 削除操作
# ハッシュからキーペア (key, value) を削除
# ハッシュテーブルからキーと値のペア (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] = "小鸭";
// ハッシュテーブルにキーと値のペア (key, value) を追加
map[12836] = "シャオハ";
map[15937] = "シャオルオ";
map[16750] = "シャオスワン";
map[13276] = "シャオファ";
map[10583] = "シャオヤー";
/* クエリ操作 */
// ハッシュ表にキーを入力し、値を取得
/* 検索操作 */
// ハッシュテーブルにキー key を入力し、値 value を取得
string name = map[15937];
/* 削除操作 */
// ハッシュからキーペア (key, value) を削除
// ハッシュテーブルからキーと値のペア (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, "小鸭");
// ハッシュテーブルにキーと値のペア (key, value) を追加
map.put(12836, "シャオハ");
map.put(15937, "シャオルオ");
map.put(16750, "シャオスワン");
map.put(13276, "シャオファ");
map.put(10583, "シャオヤー");
/* クエリ操作 */
// ハッシュ表にキーを入力し、値を取得
/* 検索操作 */
// ハッシュテーブルにキー key を入力し、値 value を取得
String name = map.get(15937);
/* 削除操作 */
// ハッシュからキーペア (key, value) を削除
// ハッシュテーブルからキーと値のペア (key, value) を削除
map.remove(10583);
```
=== "C#"
```csharp title="hash_map.cs"
/* ハッシュを初期化 */
/* ハッシュテーブルを初期化 */
Dictionary<int, string> map = new() {
/* 追加操作 */
// ハッシュ表にキー値ペア (key, value) を追加
{ 12836, "小哈" },
{ 15937, "小啰" },
{ 16750, "小算" },
{ 13276, "小法" },
{ 10583, "小鸭" }
// ハッシュテーブルにキーと値のペア (key, value) を追加
{ 12836, "シャオハ" },
{ 15937, "シャオルオ" },
{ 16750, "シャオスワン" },
{ 13276, "シャオファ" },
{ 10583, "シャオヤー" }
};
/* クエリ操作 */
// ハッシュ表にキーを入力し、値を取得
/* 検索操作 */
// ハッシュテーブルにキー key を入力し、値 value を取得
string name = map[15937];
/* 削除操作 */
// ハッシュからキーペア (key, value) を削除
// ハッシュテーブルからキーと値のペア (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] = "小鸭"
// ハッシュテーブルにキーと値のペア (key, value) を追加
hmap[12836] = "シャオハ"
hmap[15937] = "シャオルオ"
hmap[16750] = "シャオスワン"
hmap[13276] = "シャオファ"
hmap[10583] = "シャオヤー"
/* クエリ操作 */
// ハッシュ表にキーを入力し、値を取得
/* 検索操作 */
// ハッシュテーブルにキー key を入力し、値 value を取得
name := hmap[15937]
/* 削除操作 */
// ハッシュからキーペア (key, value) を削除
// ハッシュテーブルからキーと値のペア (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] = "小鸭"
// ハッシュテーブルにキーと値のペア (key, value) を追加
map[12836] = "シャオハ"
map[15937] = "シャオルオ"
map[16750] = "シャオスワン"
map[13276] = "シャオファ"
map[10583] = "シャオヤー"
/* クエリ操作 */
// ハッシュ表にキーを入力し、値を取得
/* 検索操作 */
// ハッシュテーブルにキー key を入力し、値 value を取得
let name = map[15937]!
/* 削除操作 */
// ハッシュからキーペア (key, value) を削除
// ハッシュテーブルからキーと値のペア (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, '小鸭');
// ハッシュテーブルにキーと値のペア (key, value) を追加
map.set(12836, 'シャオハ');
map.set(15937, 'シャオルオ');
map.set(16750, 'シャオスワン');
map.set(13276, 'シャオファ');
map.set(10583, 'シャオヤー');
/* クエリ操作 */
// ハッシュ表にキーを入力し、値を取得
/* 検索操作 */
// ハッシュテーブルにキー key を入力し、値 value を取得
let name = map.get(15937);
/* 削除操作 */
// ハッシュからキーペア (key, value) を削除
// ハッシュテーブルからキーと値のペア (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');
// ハッシュテーブルにキーと値のペア (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);
/* クエリ操作 */
// ハッシュ表にキーを入力し、値を取得
/* 検索操作 */
// ハッシュテーブルにキー key を入力し、値 value を取得
let name = map.get(15937);
console.info('\n学籍番号15937を入力、名前を問い合わせ ' + name);
console.info('\n学籍番号 15937 を入力、名前を検索: ' + name);
/* 削除操作 */
// ハッシュからキーペア (key, value) を削除
// ハッシュテーブルからキーと値のペア (key, value) を削除
map.delete(10583);
console.info('\n10583を削除後、ハッシュ表は\nKey -> Value');
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] = "小鸭";
// ハッシュテーブルにキーと値のペア (key, value) を追加
map[12836] = "シャオハ";
map[15937] = "シャオルオ";
map[16750] = "シャオスワン";
map[13276] = "シャオファ";
map[10583] = "シャオヤー";
/* クエリ操作 */
// ハッシュ表にキーを入力し、値を取得
/* 検索操作 */
// ハッシュテーブルにキー key を入力し、値 value を取得
String name = map[15937];
/* 削除操作 */
// ハッシュからキーペア (key, value) を削除
// ハッシュテーブルからキーと値のペア (key, value) を削除
map.remove(10583);
```
@@ -241,51 +241,95 @@
```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());
// ハッシュテーブルにキーと値のペア (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());
/* クエリ操作 */
// ハッシュ表にキーを入力し、値を取得
/* 検索操作 */
// ハッシュテーブルにキー key を入力し、値 value を取得
let _name: Option<&String> = map.get(&15937);
/* 削除操作 */
// ハッシュからキーペア (key, value) を削除
// ハッシュテーブルからキーと値のペア (key, value) を削除
let _removed_value: Option<String> = map.remove(&10583);
```
=== "C"
```c title="hash_map.c"
// Cには組み込みのハッシュ表が提供されていません
// C には組み込みのハッシュテーブルはありません
```
=== "Kotlin"
```kotlin title="hash_map.kt"
/* ハッシュテーブルを初期化 */
val map = HashMap<Int,String>()
/* 追加操作 */
// ハッシュテーブルにキーと値のペア (key, value) を追加
map[12836] = "シャオハ"
map[15937] = "シャオルオ"
map[16750] = "シャオスワン"
map[13276] = "シャオファ"
map[10583] = "シャオヤー"
/* 検索操作 */
// ハッシュテーブルにキー key を入力し、値 value を取得
val name = map[15937]
/* 削除操作 */
// ハッシュテーブルからキーと値のペア (key, value) を削除
map.remove(10583)
```
ハッシュ表を走査する一般的な方法は3つあります:キー値ペアの走査、キーの走査、値の走査。以下はコード例です:
=== "Ruby"
```ruby title="hash_map.rb"
# ハッシュテーブルを初期化
hmap = {}
# 追加操作
# ハッシュテーブルにキーと値のペア (key, value) を追加
hmap[12836] = "シャオハ"
hmap[15937] = "シャオルオ"
hmap[16750] = "シャオスワン"
hmap[13276] = "シャオファ"
hmap[10583] = "シャオヤー"
# 検索操作
# ハッシュテーブルにキー key を入力し、値 value を取得
name = hmap[15937]
# 削除操作
# ハッシュテーブルからキーと値のペア (key, value) を削除
hmap.delete(10583)
```
??? pythontutor "可視化実行"
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%9F%A5%E8%AF%A2%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%90%91%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E8%BE%93%E5%85%A5%E9%94%AE%20key%20%EF%BC%8C%E5%BE%97%E5%88%B0%E5%80%BC%20value%0A%20%20%20%20name%20%3D%20hmap%5B15937%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E5%88%A0%E9%99%A4%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap.pop%2810583%29&cumulative=false&curInstr=2&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
ハッシュテーブルには、キーと値のペア、キー、値を走査する 3 つの一般的な方法があります。コード例は以下のとおりです:
=== "Python"
```python title="hash_map.py"
# ハッシュを走査
# キーペア key->value を走査
# ハッシュテーブルを走査
# キーと値のペア key->value を走査
for key, value in hmap.items():
print(key, "->", value)
# キーのみを走査
# キー key のみを走査
for key in hmap.keys():
print(key)
# 値のみを走査
# 値 value のみを走査
for value in hmap.values():
print(value)
```
@@ -293,12 +337,12 @@
=== "C++"
```cpp title="hash_map.cpp"
/* ハッシュを走査 */
// キーペア key->value を走査
/* ハッシュテーブルを走査 */
// キーと値のペア key->value を走査
for (auto kv: map) {
cout << kv.first << " -> " << kv.second << endl;
}
// イテレータを使用してキー値ペア key->value を走査
// イテレータを使って key->value を走査
for (auto iter = map.begin(); iter != map.end(); iter++) {
cout << iter->first << "->" << iter->second << endl;
}
@@ -307,16 +351,16 @@
=== "Java"
```java title="hash_map.java"
/* ハッシュを走査 */
// キーペア key->value を走査
for (Map.Entry<Integer, String> kv: map.entrySet()) {
/* ハッシュテーブルを走査 */
// キーと値のペア key->value を走査
for (Map.Entry <Integer, String> kv: map.entrySet()) {
System.out.println(kv.getKey() + " -> " + kv.getValue());
}
// キーのみを走査
// キー key のみを走査
for (int key: map.keySet()) {
System.out.println(key);
}
// 値のみを走査
// 値 value のみを走査
for (String val: map.values()) {
System.out.println(val);
}
@@ -325,16 +369,16 @@
=== "C#"
```csharp title="hash_map.cs"
/* ハッシュを走査 */
// キーペア Key->Value を走査
/* ハッシュテーブルを走査 */
// キーと値のペア Key->Value を走査
foreach (var kv in map) {
Console.WriteLine(kv.Key + " -> " + kv.Value);
}
// キーのみを走査
// キー key のみを走査
foreach (int key in map.Keys) {
Console.WriteLine(key);
}
// 値のみを走査
// 値 value のみを走査
foreach (string val in map.Values) {
Console.WriteLine(val);
}
@@ -343,16 +387,16 @@
=== "Go"
```go title="hash_map_test.go"
/* ハッシュを走査 */
// キーペア key->value を走査
/* ハッシュテーブルを走査 */
// キーと値のペア key->value を走査
for key, value := range hmap {
fmt.Println(key, "->", value)
}
// キーのみを走査
// キー key のみを走査
for key := range hmap {
fmt.Println(key)
}
// 値のみを走査
// 値 value のみを走査
for _, value := range hmap {
fmt.Println(value)
}
@@ -361,16 +405,16 @@
=== "Swift"
```swift title="hash_map.swift"
/* ハッシュを走査 */
// キーペア Key->Value を走査
/* ハッシュテーブルを走査 */
// キーと値のペア Key->Value を走査
for (key, value) in map {
print("\(key) -> \(value)")
}
// キーのみを走査
// キー Key のみを走査
for key in map.keys {
print(key)
}
// 値のみを走査
// 値 Value のみを走査
for value in map.values {
print(value)
}
@@ -379,16 +423,16 @@
=== "JS"
```javascript title="hash_map.js"
/* ハッシュを走査 */
console.info('\nキーペア Key->Value を走査');
/* ハッシュテーブルを走査 */
console.info('\nキーと値のペア Key->Value を走査');
for (const [k, v] of map.entries()) {
console.info(k + ' -> ' + v);
}
console.info('\nキーのみを走査 Key');
console.info('\nキー Key のみを走査');
for (const k of map.keys()) {
console.info(k);
}
console.info('\n値のみを走査 Value');
console.info('\n値 Value のみを走査');
for (const v of map.values()) {
console.info(v);
}
@@ -397,16 +441,16 @@
=== "TS"
```typescript title="hash_map.ts"
/* ハッシュを走査 */
console.info('\nキーペア Key->Value を走査');
/* ハッシュテーブルを走査 */
console.info('\nキーと値のペア Key->Value を走査');
for (const [k, v] of map.entries()) {
console.info(k + ' -> ' + v);
}
console.info('\nキーのみを走査 Key');
console.info('\nキー Key のみを走査');
for (const k of map.keys()) {
console.info(k);
}
console.info('\n値のみを走査 Value');
console.info('\n値 Value のみを走査');
for (const v of map.values()) {
console.info(v);
}
@@ -415,38 +459,38 @@
=== "Dart"
```dart title="hash_map.dart"
/* ハッシュを走査 */
// キーペア Key->Value を走査
/* ハッシュテーブルを走査 */
// キーと値のペア Key->Value を走査
map.forEach((key, value) {
print('$key -> $value');
print('$key -> $value');
});
// キーのみを走査 Key
// キー Key のみを走査
map.keys.forEach((key) {
print(key);
print(key);
});
// 値のみを走査 Value
// 値 Value のみを走査
map.values.forEach((value) {
print(value);
print(value);
});
```
=== "Rust"
```rust title="hash_map.rs"
/* ハッシュを走査 */
// キーペア Key->Value を走査
/* ハッシュテーブルを走査 */
// キーと値のペア Key->Value を走査
for (key, value) in &map {
println!("{key} -> {value}");
}
// キーのみを走査 Key
// キー Key のみを走査
for key in map.keys() {
println!("{key}");
}
// 値のみを走査 Value
// 値 Value のみを走査
for value in map.values() {
println!("{value}");
}
@@ -455,63 +499,93 @@
=== "C"
```c title="hash_map.c"
// Cには組み込みのハッシュ表が提供されていません
// C には組み込みのハッシュテーブルはありません
```
=== "Kotlin"
```kotlin title="hash_map.kt"
/* ハッシュテーブルを走査 */
// キーと値のペア key->value を走査
for ((key, value) in map) {
println("$key -> $value")
}
// キー key のみを走査
for (key in map.keys) {
println(key)
}
// 値 value のみを走査
for (_val in map.values) {
println(_val)
}
```
## ハッシュ表の簡単な実装
=== "Ruby"
まず、最も簡単なケースを考えてみましょう:**配列のみを使ってハッシュ表を実装すること**。ハッシュ表において、配列の各空きスロットは<u>バケット</u>と呼ばれ、各バケットはキー値ペアを格納できます。したがって、クエリ操作は`key`に対応するバケットを見つけ、そこから`value`を取得することになります。
```ruby title="hash_map.rb"
# ハッシュテーブルを走査
# キーと値のペア key->value を走査
hmap.entries.each { |key, value| puts "#{key} -> #{value}" }
では、`key`に基づいて対応するバケットをどのように特定するのでしょうか?これは<u>ハッシュ関数</u>によって実現されます。ハッシュ関数の役割は、より大きな入力空間をより小さな出力空間にマッピングすることです。ハッシュ表では、入力空間はすべてのキーで構成され、出力空間はすべてのバケット(配列インデックス)で構成されます。つまり、`key`が与えられた場合、**ハッシュ関数を使用して対応するキー値ペアの配列内の格納位置を決定できます**。
# キー key のみを走査
hmap.keys.each { |key| puts key }
与えられた`key`に対して、ハッシュ関数の計算は2つのステップで構成されます:
# 値 value のみを走査
hmap.values.each { |val| puts val }
```
1. 特定のハッシュアルゴリズム`hash()`を使用してハッシュ値を計算します。
2. ハッシュ値をバケット数(配列長)`capacity`で剰余を取り、キーに対応する配列`index`を取得します。
??? pythontutor "可視化実行"
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E9%94%AE%E5%80%BC%E5%AF%B9%20key-%3Evalue%0A%20%20%20%20for%20key,%20value%20in%20hmap.items%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key,%20%22-%3E%22,%20value%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E9%94%AE%20key%0A%20%20%20%20for%20key%20in%20hmap.keys%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E5%80%BC%20value%0A%20%20%20%20for%20value%20in%20hmap.values%28%29%3A%0A%20%20%20%20%20%20%20%20print%28value%29&cumulative=false&curInstr=8&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
## ハッシュテーブルの簡単な実装
まずは最も単純なケースとして、**1 つの配列だけでハッシュテーブルを実装する**ことを考えます。ハッシュテーブルでは、配列中の各空き位置を<u>バケット(bucket</u>と呼び、各バケットには 1 つのキーと値のペアを格納できます。したがって、検索操作とは `key` に対応するバケットを見つけ、そのバケットから `value` を取得することです。
では、`key` に基づいて対応するバケットをどのように特定するのでしょうか。これは<u>ハッシュ関数(hash function</u>によって実現されます。ハッシュ関数の役割は、大きな入力空間をより小さな出力空間に写像することです。ハッシュテーブルでは、入力空間はすべての `key` 、出力空間はすべてのバケット(配列インデックス)です。言い換えると、`key` を入力すると、**ハッシュ関数によってその `key` に対応するキーと値のペアの配列内での格納位置を求められます**。
`key` を入力したとき、ハッシュ関数の計算過程は次の 2 段階に分かれます。
1. あるハッシュアルゴリズム `hash()` を用いてハッシュ値を計算します。
2. ハッシュ値をバケット数(配列長)`capacity` で剰余し、その `key` に対応するバケット(配列インデックス)`index` を求めます。
```shell
index = hash(key) % capacity
```
その後、`index`を使用してハッシュ内の対応するバケットにアクセスし、`value`を取得できます。
その後、`index` を使てハッシュテーブル内の対応するバケットにアクセスし、`value` を取得できます。
配列長`capacity = 100`、ハッシュアルゴリズム`hash(key) = key`として定義されているとします。したがって、ハッシュ関数は`key % 100`として表現できます。以下の図は、`key`を学籍番号、`value`を名前として、ハッシュ関数の動作原理を示しています。
配列長`capacity = 100` 、ハッシュアルゴリズム`hash(key) = key` とすると、ハッシュ関数は `key % 100` となります。の図は、`key` を学籍番号、`value` を名前の例として、ハッシュ関数の動作原理を示します。
![ハッシュ関数の動作原理](hash_map.assets/hash_function.png)
以下のコードは簡単なハッシュ表を実装しています。ここでは、`key``value``Pair`クラスにカプセル化してキー値ペアを表現しています。
以下のコードは、単純なハッシュテーブルを実装したものです。ここでは、キーと値のペアを表すために `key``value` をクラス `Pair` にまとめています。
```src
[file]{array_hash_map}-[class]{array_hash_map}-[func]{}
```
## ハッシュ衝突とリサイズ
## ハッシュ衝突と拡張
本質的に、ハッシュ関数の役割は、すべてのキーの入力空間全体を、すべての配列インデックス出力空間にマッピングすることです。しかし、入力空間は出力空間よりはるかに大きいことがよくあります。したがって、**理論的には、「複数の入力が同じ出力に対応する」ケースが常に存在します**。
本質的に、ハッシュ関数の役割は、すべての `key` からなる入力空間を、配列のすべてのインデックスからなる出力空間に写像することです。しかし、入力空間は多くの場合、出力空間よりはるかに大きいため、**理論上は必ず「複数の入力が同じ出力に対応する」状況が存在します**。
の例では、与えられたハッシュ関数で、入力`key`の下桁が同じ場合、ハッシュ関数は同じ出力を生成します。えば、学籍番号1283620336の2人の学生をクエリすると、以下のことがわかります:
上の例ハッシュ関数で、入力 `key` の下 2 桁が同じであれば、出力結果も同じになります。たとえば、学籍番号 1283620336 の 2 人の学生を検索すると、次の結果を得ます:
```shell
12836 % 100 = 36
20336 % 100 = 36
```
図に示すように、両方の学籍番号が同じ名前を指しており、これは明らかに間違っています。この複数の入力が同じ出力に対応する状況を<u>ハッシュ衝突</u>と呼びます。
次の図に示すように、2 つの学籍番号が同じ名前を指してしまっており、これは明らかに誤りです。このような、複数の入力が同じ出力に対応する状況を<u>ハッシュ衝突hash collision</u>と呼びます。
![ハッシュ衝突の例](hash_map.assets/hash_collision.png)
ハッシュ表の容量$n$が増加するにつれて、複数のキーが同じバケットに割り当てられる確率が減少し、衝突少なくなることは理解しやすいです。したがって、**ハッシュ表をリサイズすることでハッシュ衝突を減らすことができます**。
容易に分かるように、ハッシュテーブルの容量 $n$ が大きいほど、複数の `key` が同じバケットに割り当てられる確率は低くなり、衝突少なくなります。したがって、**ハッシュテーブルを拡張することでハッシュ衝突を減らます**。
図に示すように、リサイズ前はキーペア`(136, A)``(236, D)`が衝突していました。しかし、リサイズ後は衝突が解されています。
次の図に示すように、拡張前はキーと値のペア `(136, A)``(236, D)` が衝突していますが、拡張後は衝突が解されます。
![ハッシュ表のリサイズ](hash_map.assets/hash_table_reshash.png)
![ハッシュテーブルの拡張](hash_map.assets/hash_table_reshash.png)
配列の拡張と同様に、ハッシュ表のリサイズにはすべてのキーペアを元のハッシュから新しいものに移行する必要があり、時間がかかります。さらに、ハッシュ表の`capacity`が変更されるため、ハッシュ関数を使用してすべてのキーペアの格納位置を再計算する必要があり、リサイズプロセスの計算オーバーヘッドがさらに増加します。したがって、プログラミング言語は頻繁なリサイズを防ぐために、ハッシュ表に十分大きな容量を割り当てることがよくあります。
配列の拡張と同様に、ハッシュテーブルの拡張ではすべてのキーと値のペアを元のハッシュテーブルから新しいハッシュテーブルへ移し替える必要があり、非常に時間がかかります。また、ハッシュテーブルの容量 `capacity` が変るため、ハッシュ関数を使てすべてのキーと値のペアの格納位置を再計算しなければならず、これによって拡張過程の計算コストがさらに増加します。そのため、プログラミング言語では通常、頻繁な拡張を防ぐために十分大きなハッシュテーブル容量をあらかじめ確保します。
<u>負荷率</u>はハッシュ表の重要な概念です。ハッシュ内の要素数バケット数の比率として定義されます。ハッシュ衝突の深刻を測定するために使用され、**しばしばハッシュ表のリサイズのトリガーとしても機能します**。例えばJavaでは、負荷率が$0.75$を超えると、システムはハッシュ表を元のサイズの2倍にリサイズします。
<u>負荷率load factor</u>はハッシュテーブルにおける重要な概念であり、ハッシュテーブル内の要素数バケット数で割ったものとして定義されハッシュ衝突の深刻を測るために用いられます。**また、ハッシュテーブル拡張の発動条件としてもよく使われます**。例えば Java では、負荷率が $0.75$ を超えると、システムはハッシュテーブルを元の $2$ 倍に拡張します。
+5 -5
View File
@@ -1,9 +1,9 @@
# ハッシュ
# ハッシュテーブル
![ハッシュ](../assets/covers/chapter_hashing.jpg)
![ハッシュテーブル](../assets/covers/chapter_hashing.jpg)
!!! abstract
コンピューティングの世界において、ハッシュ表は賢い司書のようなものです。
インデックス番号の計算方法を理解し、目的の本を迅速に取得することを可能にします。
コンピュータの世界では、ハッシュテーブルは聡明な図書館員のような存在です。
彼は請求記号の計算方法を知っており、そのため目的の本を素早く見つけられます。
+32 -28
View File
@@ -1,47 +1,51 @@
# まとめ
### 重要ポイント
### 重要ポイントの振り返り
- 入力`key`が与えられると、ハッシュ表は$O(1)$時間で対応する`value`を取得でき、非常に効率的です
- 一般的なハッシュの操作には、クエリ、キー値ペアの追加、キーペアの削除、ハッシュ表の走査があります
- ハッシュ関数は`key`を配列インデックスにマッピングし、対応するバケットにアクセスして`value`を取得できるようにします。
- 2つの異なるキーがハッシュ化後に同じ配列インデックスになる場合があり、誤ったクエリ結果につながります。この現象ハッシュ衝突として知られています
- ハッシュの容量が大きいほど、ハッシュ衝突の確率は低くなります。したがって、ハッシュ表のリサイズはハッシュ衝突を緩和できます。配列のリサイズと同様に、ハッシュ表のリサイズはコストが高いです
- 要素数をバケット数で割った負荷率は、ハッシュ衝突の深刻を反映し、しばしばハッシュ表リサイズのトリガー条件として使用されます
- 連鎖法は各要素を連結リストに変換し、衝突するすべての要素を同じリストに格納することでハッシュ衝突に対処します。ただし、過度に長いリストはクエリ効率低下させる可能性があり、リストを赤黒木に変換することで改善できます
- オープンアドレス法は複数回のプローブを通してハッシュ衝突を処理します。線形プローブは固定ステップサイズを使用しますが、要素を削除できず、クラスタリングを起こしやすい傾向があります。多重ハッシュはプローブに複数のハッシュ関数を使用し、線形プローブと比較してクラスタリングを減らしますが、計算オーバーヘッドが増加します
- 異なるプログラミング言語はさまざまなハッシュ表実装採用しています。例えば、Java`HashMap`は連鎖を使用し、Python`dict`はオープンアドレス法を採用しています
- ハッシュ表では、決定性、高効率、均等分散を持つハッシュアルゴリズムが望まれます。暗号では、ハッシュアルゴリズムは衝突性と雪崩効果も持つべきで
- ハッシュアルゴリズムは通常、ハッシュ値の均等分散を保証しハッシュ衝突を減らすために、大きな素数を剰余として使用します
- 一般的なハッシュアルゴリズムにはMD5、SHA-1、SHA-2、SHA-3があります。MD5はファイル整合性チェックによく使用され、SHA-2は安全なアプリケーションとプロトコルで一般的に使用されます
- プログラミング言語は通常、ハッシュ表のバケットインデックスを計算するために、データ型に対して組み込みのハッシュアルゴリズムを提供します。一般的に、不変オブジェクトのみがハッシュ可能です
- `key` を入力すると、ハッシュテーブルは $O(1)$ 時間で `value` を検索でき、非常に効率である
- 一般的なハッシュテーブルの操作には、検索、キーと値のペアの追加、キーと値のペアの削除、ハッシュテーブルの走査などがある
- ハッシュ関数は `key` を配列インデックスに写像し、それによって対応するバケットにアクセスして `value` を取得す
- 異なる 2 つの `key` が、ハッシュ関数を通した後に同じ配列インデックスになることがあり、検索結果の誤りを引き起こす。この現象ハッシュ衝突と呼ぶ
- ハッシュテーブルの容量が大きいほど、ハッシュ衝突の確率は低くなる。そのため、ハッシュテーブルを拡張することでハッシュ衝突を緩和でき。配列の拡張と同様に、ハッシュテーブルの拡張操作のコストは大きい
- 負荷率は、ハッシュテーブル内の要素数をバケット数で割ったものと定義され、ハッシュ衝突の深刻を反映する。ハッシュテーブル拡張を発動する条件としてよく用いられる
- 連鎖方式では、単一要素を連結リストに変換し、衝突したすべての要素を同じ連結リストに格納する。しかし、連結リストが長すぎると検索効率低下するため、さらに連結リストを赤黒木に変換して効率を高めることができる
- オープンアドレス法は複数回の探索によってハッシュ衝突を処理する。線形探索は固定ステップ幅を用いるが、要素を削除できず、クラスタリングが発生しやすいという欠点がある。二重ハッシュは複数のハッシュ関数を用いて探索するため、線形探索に比べてクラスタリングが起きにくいが、複数のハッシュ関数によって計算量が増える
- プログラミング言語ごとに、異なるハッシュテーブル実装採用されている。たとえば、Java`HashMap` は連鎖方式を使用し、Python`Dict` はオープンアドレス法を採用してい
- ハッシュテーブルでは、ハッシュアルゴリズムに決定性、高効率、均一分布という特徴が求められる。暗号では、ハッシュアルゴリズムはさらに耐衝突性とアバランシェ効果も備えるべきである
- ハッシュアルゴリズムは通常、大きな素数を法として用い、ハッシュ値の均一分布を最大限に保証しハッシュ衝突を減らす。
- 一般的なハッシュアルゴリズムには MD5、SHA-1、SHA-2、SHA-3 などがある。MD5 はファイル完全性の検証によく用いられ、SHA-2 はセキュリティ用途やプロトコルでよく用いられる
- プログラミング言語は通常、データ型に対して組み込みのハッシュアルゴリズムを提供し、ハッシュテーブル内のバケットインデックスの計算に用いる。通常、ハッシュ可能なのは不変オブジェクトだけである
### Q & A
**Q**: ハッシュの時間計算量が$O(n)$に悪化するのはいつですか?
**Q**ハッシュテーブルの時間計算量が $O(n)$ になるのはどのような場合ですか?
ハッシュ表の時間計算量は、ハッシュ衝突が深刻な場合に$O(n)$に悪化する可能性があります。ハッシュ関数が適切に設計され、容量が適切に設定され、衝突が均等に分散されている場合、時間計算量は$O(1)$です。プログラミング言語組み込みハッシュ表を使用する場合、通常は時間計算量を$O(1)$と考えます。
ハッシュ衝突が深刻な場合、ハッシュテーブルの時間計算量は $O(n)$ に劣化する。ハッシュ関数の設計が適切で、容量設定が合理的で、衝突が比較的均等な場合、時間計算量は $O(1)$ である。プログラミング言語組み込みハッシュテーブルを使うとき、通常は時間計算量を $O(1)$ とみなす。
**Q**: なぜハッシュ関数$f(x) = x$を使用しないのですか?これなら衝突を排除できます
**Q**なぜハッシュ関数 $f(x) = x$ を使ないのですか? そうすれば衝突は起きません
ハッシュ関数$f(x) = x$では、各要素一意のバケットインデックスに対応し、これは配列と同等です。しかし、入力空間は通常出力空間(配列長)よりはるかに大きいため、ハッシュ関数の最後のステップは配列長の剰余を取ることがよくあります。言い換えると、ハッシュの目は、$O(1)$のクエリ効率を提供しながら、より大きな状態空間をより小さなものにマッピングすることで
$f(x) = x$ というハッシュ関数では、各要素一意のバケットインデックスに対応し、これは配列と等価である。しかし、入力空間は通常出力空間(配列長)よりはるかに大きいため、ハッシュ関数の最後のステップはたいてい配列長の剰余になる。言い換えると、ハッシュテーブルの目は、大きな状態空間をより小さな空間に写像し、$O(1)$ の検索効率を提供することである
**Q**: ハッシュ表がこれらの構造を使って実装されているにもかかわらず、なぜ配列、連結リスト、二分木より効率になるのですか?
**Q**ハッシュテーブルの基礎実装は配列、連結リスト、二分木なのに、なぜそれらより効率になり得るのですか?
まず、ハッシュは時間効率が高いですが、空間効率は低いです。ハッシュ表のメモリの大部分未使用のままです
まず、ハッシュテーブルは時間効率が高くなる一方で、空間効率は低くなる。ハッシュテーブルには、かなりの部分未使用のメモリが存在する
次に、ハッシュ表は特定のユースケースでのみ時間効率が高いです。配列や連結リストを使用して同じ時間計算量で機能を実装できる場合、通常はハッシュ表を使用するよりも高速です。これは、ハッシュ関数の計算がオーバーヘッドを発生させ、時間計算量の定数因子が大きくなるためです
次に、時間効率が高くなるのは特定の利用場面に限られる。ある機能が同じ時間計算量で配列や連結リストによって実装できるなら、通常はハッシュテーブルより速い。これは、ハッシュ関数の計算にコストがかかり、時間計算量の定数項がより大きいからである
最後に、ハッシュの時間計算量は化する可能性があります。例えば連鎖では、連結リストや赤黒木で検索操作を実行し、これは依然として$O(n)$時間に化するリスクがあります
最後に、ハッシュテーブルの時間計算量は化する可能性がある。たとえば連鎖方式では、連結リストや赤黒木で検索操作を行うため、なお $O(n)$ 時間に化するリスクがあ
**Q**: 多重ハッシュにも要素を直接削除できないという欠陥がありますか?削除としてマークされた空間は再利用できますか?
**Q**:二重ハッシュにも要素を直接削除できない欠点がありますか? 削除済みとマークした領域は再利用できますか?
重ハッシュはオープンアドレス法の一形態であり、すべてのオープンアドレス法は要素を直接削除できないという欠点があります。要素を削除済みとしてマークする必要があります。マークされた空間は再利用できます。ハッシュ表に新しい要素を挿入する際、ハッシュ関数削除済みとしてマークされた位置を指している場合、その位置は新しい要素によって使用できます。これにより、ハッシュ表のプローブシーケンスを維持しながら、空間の効率的な使用が保証されます
重ハッシュはオープンアドレス法の一であり、オープンアドレス法はいずれも要素を直接削除できないという欠点があるため、削除のマーク付けが必要になる。削除済みとマークされた領域は再利用できる。新しい要素をハッシュテーブルに挿入し、ハッシュ関数によって削除済みとマークされた位置を見つけた場合、その位置は新しい要素に使用できる。こうすることで、ハッシュテーブルの探索系列を変えずに保ちつつ、空間利用率も確保できる
**Q**: なぜ線形プローブの検索プロセス中にハッシュ衝突が発生するのですか?
**Q**なぜ線形探索では、要素を探すときにハッシュ衝突が発生するのですか?
検索プロセス中、ハッシュ関数対応するバケットとキー値ペアを指します。`key`が一致しない場合、ハッシュ衝突を示します。したがって、線形プローブは正しいキーペア見つるか検索が失敗するまで、事前に決められたステップサイズで下方向に検索します
探索時には、ハッシュ関数対応するバケットとキーと値のペアを見つけ、`key` が一致しないことが分かると、それはハッシュ衝突を意味する。そのため、線形探索法では事前に設定したステップ幅に従って順に探索し、正しいキーと値のペア見つるか、見つからずに終了するまで続ける
**Q**: なぜハッシュ表のリサイズがハッシュ衝突を緩和できるのですか?
**Q**なぜハッシュテーブルの拡張でハッシュ衝突を緩和できるのですか?
ハッシュ関数の最後のステップは、出力を配列インデックス範囲内に保つために、配列長$n$の剰余を取ることがよくあります。リサイズ時、配列長$n$が変化し、キーに対応するインデックスも変化する可能性があります。以前に同じバケットにマッピングされていたキーが、リサイズ後に複数のバケットに分散される可能性があり、それによってハッシュ衝突が緩和されます
ハッシュ関数の最後のステップは、たいてい配列長 $n$ での剰余を取り、出力を配列インデックス範囲内に収めることである。拡張後は配列長 $n$ が変化し、`key` に対応するインデックスも変化する可能性がある。もともと同じバケットに入っていた複数の `key` は、拡張後に複数のバケットに割り当てられる可能性があり、それによってハッシュ衝突が緩和され
**Q**:高効率な読み書きのためなら、配列を直接使えばよいのではないですか?
データの `key` が連続した小範囲の整数であれば、配列を直接使えばよく、単純で高効率である。しかし `key` が別の型(たとえば文字列)の場合は、ハッシュ関数を用いて `key` を配列インデックスに写像し、さらにバケット配列を通じて要素を格納する必要がある。このような構造がハッシュテーブルである。