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
+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 個のキーと値のペアを格納でき、容量を超えるとオーバーフローバケット連結ます。オーバーフローバケットが多すぎる場合、性能を確保するために特な等量拡張操作実行ます。