mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-24 03:56:28 +00:00
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:
@@ -1,108 +1,108 @@
|
||||
# ハッシュ衝突
|
||||
|
||||
前節で述べたように、**ほとんどの場合、ハッシュ関数の入力空間は出力空間よりもはるかに大きい**ため、理論的にはハッシュ衝突は避けられません。例えば、入力空間がすべての整数で、出力空間が配列容量のサイズの場合、複数の整数が必然的に同じバケットインデックスにマッピングされます。
|
||||
前節で述べたように、**通常、ハッシュ関数の入力空間は出力空間よりもはるかに大きい**ため、理論上ハッシュ衝突は避けられません。例えば、入力空間がすべての整数で、出力空間が配列の容量サイズである場合、必然的に複数の整数が同じバケットインデックスに写像されます。
|
||||
|
||||
ハッシュ衝突は誤ったクエリ結果につながり、ハッシュ表の使いやすさに深刻な影響を与える可能性があります。この問題に対処するために、ハッシュ衝突が発生するたびに、衝突が消えるまでハッシュ表のリサイズを実行します。このアプローチは非常にシンプルで直接的であり、うまく機能します。しかし、テーブルの拡張には大量のデータ移行とハッシュコードの再計算が含まれ、これらは高コストであるため、非常に非効率的に見えます。効率を向上させるために、以下の戦略を採用できます:
|
||||
ハッシュ衝突は検索結果の誤りを招き、ハッシュテーブルの利用可能性に深刻な影響を与えます。この問題を解決するために、ハッシュ衝突が発生するたびにハッシュテーブルを拡張し、衝突が消えるまで続けることが考えられます。この方法は単純で効果的ですが、効率が低すぎます。なぜなら、ハッシュテーブルの拡張には大量のデータ移動とハッシュ値の計算が必要だからです。効率を高めるために、次の戦略を採用できます。
|
||||
|
||||
1. **ハッシュ衝突が発生した場合でも、ターゲット要素の検索が適切に機能する**ようにハッシュ表のデータ構造を改善する。
|
||||
2. 深刻な衝突が観察され、必要になる前に、拡張は最後の手段とする。
|
||||
1. ハッシュテーブルのデータ構造を改良し、**ハッシュ衝突が発生してもハッシュテーブルが正常に動作できるようにする**。
|
||||
2. 必要な場合、すなわちハッシュ衝突が比較的深刻なときにのみ、拡張操作を実行する。
|
||||
|
||||
ハッシュ表の構造を改善する主な方法は2つあります:「連鎖法」と「オープンアドレス法」です。
|
||||
ハッシュテーブルの構造改善方法には、主に「チェイン法」と「オープンアドレッシング」があります。
|
||||
|
||||
## 連鎖法
|
||||
## チェイン法
|
||||
|
||||
元のハッシュ表では、各バケットは1つのキー値ペアのみを格納できます。<u>連鎖法</u>は単一の要素を連結リストに変換し、キー値ペアをリストノードとして扱い、衝突するすべてのキー値ペアを同じ連結リストに格納します。下図は連鎖法を使用したハッシュ表の例を示しています。
|
||||
元のハッシュテーブルでは、各バケットには 1 つのキーと値のペアしか格納できません。<u>チェイン法(separate chaining)</u>では、単一要素を連結リストに置き換え、キーと値のペアを連結リストのノードとして扱い、衝突したすべてのキーと値のペアを同じ連結リストに格納します。下図はチェイン法によるハッシュテーブルの例を示しています。
|
||||
|
||||

|
||||

|
||||
|
||||
連鎖法で実装されたハッシュ表の操作は以下のように変更されます:
|
||||
チェイン法で実装されたハッシュテーブルでは、操作方法が次のように変わります。
|
||||
|
||||
- **要素のクエリ**: `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` はすべて同じバケットに写像されます。線形探索によって、それらはそのバケットとその後続のバケットに順に格納されます。
|
||||
|
||||

|
||||

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

|
||||

|
||||
|
||||
この問題を解決するために、<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 個のキーと値のペアを格納でき、容量を超えるとオーバーフローバケットを連結します。オーバーフローバケットが多すぎる場合は、性能を確保するために特殊な等量拡張操作を実行します。
|
||||
|
||||
Reference in New Issue
Block a user