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,27 +1,27 @@
|
||||
# 二分探索
|
||||
|
||||
<u>二分探索</u>は分割統治戦略を用いる効率的な探索アルゴリズムです。配列内の要素の整列順序を利用し、各反復で探索区間を半分に減らしながら、目標要素が見つかるか探索区間が空になるまで続行します。
|
||||
<u>二分探索(binary search)</u>は分割統治法に基づく効率的な探索アルゴリズムです。データが整列済みである性質を利用し、各ラウンドで探索範囲を半分に縮小し、目標要素を見つけるか探索区間が空になるまで続けます。
|
||||
|
||||
!!! question
|
||||
|
||||
長さ$n$の配列`nums`が与えられ、要素は重複なしで昇順に配列されています。この配列内の要素`target`のインデックスを見つけて返してください。配列に要素が含まれていない場合は$-1$を返してください。例を下図に示します。
|
||||
長さ $n$ の配列 `nums` が与えられます。要素は小さい順に並んでおり、重複しません。要素 `target` がこの配列内にある場合はそのインデックスを返し、含まれない場合は $-1$ を返してください。例を次の図に示します。
|
||||
|
||||

|
||||

|
||||
|
||||
下図に示すように、まず$i = 0$と$j = n - 1$でポインタを初期化し、それぞれ配列の最初と最後の要素を指します。これらはまた全体の探索区間$[0, n - 1]$を表します。角括弧は閉区間を示し、境界値自身も含むことに注意してください。
|
||||
次の図に示すように、まずポインタ $i = 0$ と $j = n - 1$ を初期化し、それぞれ配列の先頭要素と末尾要素を指すようにして、探索区間 $[0, n - 1]$ を表します。角括弧は閉区間を表し、境界値自体を含むことに注意してください。
|
||||
|
||||
そして、以下の2つのステップをループで実行する可能性があります。
|
||||
次に、以下の 2 つの手順を繰り返します。
|
||||
|
||||
1. 中点インデックス$m = \lfloor {(i + j) / 2} \rfloor$を計算します。ここで$\lfloor \: \rfloor$は床関数を表します。
|
||||
2. `nums[m]`と`target`の比較に基づいて、以下の3つのケースのうち1つを選択して実行します。
|
||||
1. `nums[m] < target`の場合、`target`は区間$[m + 1, j]$にあることを示すため、$i = m + 1$とします。
|
||||
2. `nums[m] > target`の場合、`target`は区間$[i, m - 1]$にあることを示すため、$j = m - 1$とします。
|
||||
3. `nums[m] = target`の場合、`target`が見つかったことを示すため、インデックス$m$を返します。
|
||||
1. 中央のインデックス $m = \lfloor {(i + j) / 2} \rfloor$ を計算します。ここで $\lfloor \: \rfloor$ は切り捨てを表します。
|
||||
2. `nums[m]` と `target` の大小関係を判定し、次の 3 つの場合に分かれます。
|
||||
1. `nums[m] < target` のとき、`target` は区間 $[m + 1, j]$ にあるため、$i = m + 1$ を実行します。
|
||||
2. `nums[m] > target` のとき、`target` は区間 $[i, m - 1]$ にあるため、$j = m - 1$ を実行します。
|
||||
3. `nums[m] = target` のとき、`target` が見つかったので、インデックス $m$ を返します。
|
||||
|
||||
配列に目標要素が含まれていない場合、探索区間は最終的に空になり、$-1$を返して終了します。
|
||||
配列に目標要素が含まれない場合、探索区間は最終的に空まで縮小されます。このとき $-1$ を返します。
|
||||
|
||||
=== "<1>"
|
||||

|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
@@ -41,43 +41,43 @@
|
||||
=== "<7>"
|
||||

|
||||
|
||||
$i$と$j$が両方とも`int`型であるため、**$i + j$は`int`型の範囲を超える可能性がある**ことは注目に値します。大きな数のオーバーフローを避けるため、通常は式$m = \lfloor {i + (j - i) / 2} \rfloor$を使用して中点を計算します。
|
||||
注意すべき点として、$i$ と $j$ はどちらも `int` 型であるため、**$i + j$ が `int` 型の範囲を超える可能性があります**。大きな数によるオーバーフローを避けるため、通常は式 $m = \lfloor {i + (j - i) / 2} \rfloor$ を用いて中点を計算します。
|
||||
|
||||
コードは以下の通りです:
|
||||
コードは次のとおりです。
|
||||
|
||||
```src
|
||||
[file]{binary_search}-[class]{}-[func]{binary_search}
|
||||
```
|
||||
|
||||
**時間計算量は$O(\log n)$です**:二分ループにおいて、区間は各ラウンドで半分に減少するため、反復回数は$\log_2 n$となります。
|
||||
**時間計算量は $O(\log n)$** :二分探索のループでは各ラウンドで区間が半分になるため、ループ回数は $\log_2 n$ です。
|
||||
|
||||
**空間計算量は$O(1)$です**:ポインタ$i$と$j$は定数サイズの空間を占有します。
|
||||
**空間計算量は $O(1)$** :ポインタ $i$ と $j$ に必要なのは定数サイズの空間だけです。
|
||||
|
||||
## 区間表現方法
|
||||
## 区間の表し方
|
||||
|
||||
上記の閉区間の他に、もう一つの一般的な区間表現は「左閉右開」区間で、$[0, n)$として定義され、左境界は自身を含み、右境界は含みません。この表現では、$i = j$のとき区間$[i, j)$は空になります。
|
||||
上記の両閉区間のほかに、一般的な区間表現として「左閉右開」区間があり、$[0, n)$ と定義されます。つまり左端は含み、右端は含みません。この表現では、区間 $[i, j)$ は $i = j$ のとき空です。
|
||||
|
||||
この表現に基づいて同じ機能を持つ二分探索アルゴリズムを実装できます:
|
||||
この表現に基づいて、同じ機能を持つ二分探索アルゴリズムを実装できます。
|
||||
|
||||
```src
|
||||
[file]{binary_search}-[class]{}-[func]{binary_search_lcro}
|
||||
```
|
||||
|
||||
下図に示すように、2つの区間表現タイプにおいて、二分探索アルゴリズムの初期化、ループ条件、区間縮小操作が異なります。
|
||||
次の図に示すように、2 種類の区間表現では、二分探索アルゴリズムの初期化、ループ条件、区間の縮小操作がそれぞれ異なります。
|
||||
|
||||
「閉区間」表現では両方の境界が包含的であるため、ポインタ$i$と$j$による区間縮小操作も対称的です。これによりエラーが発生しにくくなるため、**一般的に「閉区間」アプローチの使用が推奨されます**。
|
||||
「両閉区間」の表現では左右の境界がどちらも閉区間として定義されるため、ポインタ $i$ とポインタ $j$ による区間縮小の操作も対称になります。このほうがミスをしにくいため、**一般には「両閉区間」の書き方を推奨します**。
|
||||
|
||||

|
||||

|
||||
|
||||
## 利点と制限
|
||||
## 利点と限界
|
||||
|
||||
二分探索は時間と空間の両方の面で良好な性能を示します。
|
||||
二分探索は時間と空間の両面で優れた性能を持ちます。
|
||||
|
||||
- 二分探索は時間効率が良いです。大きなデータセットでは、対数時間計算量が大きな利点を提供します。例えば、サイズ$n = 2^{20}$のデータセットが与えられた場合、線形探索は$2^{20} = 1048576$回の反復が必要ですが、二分探索は$\log_2 2^{20} = 20$回のループのみで済みます。
|
||||
- 二分探索には追加の空間が必要ありません。追加の空間に依存する探索アルゴリズム(ハッシュ探索など)と比較して、二分探索はより空間効率的です。
|
||||
- 二分探索は時間効率が高いです。データ量が大きい場合、対数時間計算量は大きな優位性を持ちます。たとえば、データサイズ $n = 2^{20}$ のとき、線形探索では $2^{20} = 1048576$ 回のループが必要ですが、二分探索では $\log_2 2^{20} = 20$ 回で済みます。
|
||||
- 二分探索は追加の空間を必要としません。追加領域を要する探索アルゴリズム(たとえばハッシュ探索)と比べて、二分探索はより省メモリです。
|
||||
|
||||
しかし、二分探索は以下の懸念により、すべてのシナリオに適しているとは限りません。
|
||||
しかし、二分探索があらゆる状況に適しているわけではなく、主な理由は次のとおりです。
|
||||
|
||||
- 二分探索はソート済みデータにのみ適用できます。未ソートのデータは二分探索を適用する前にソートする必要があり、ソートアルゴリズムは通常$O(n \log n)$の時間計算量を持つため、これは価値がないかもしれません。このコストは線形探索よりも高く、二分探索自体は言うまでもありません。頻繁な挿入があるシナリオでは、配列を順序に保つコストは非常に高く、特定の位置に新しい要素を挿入する時間計算量は$O(n)$です。
|
||||
- 二分探索は配列のみを使用できます。二分探索には非連続(ジャンプ)要素アクセスが必要で、これは連結リストでは非効率的です。そのため、連結リストや連結リストに基づくデータ構造はこのアルゴリズムに適していない可能性があります。
|
||||
- 線形探索は小さなデータセットでより良い性能を示します。線形探索では各反復で1つの判定操作のみが必要ですが、二分探索では1つの加算、1つの除算、1つから3つの判定操作、1つの加算(減算)を含み、合計4つから6つの操作が必要です。そのため、データサイズ$n$が小さい場合、線形探索は二分探索よりも高速です。
|
||||
- 二分探索は整列済みデータにしか適用できません。入力データが無秩序な場合、二分探索を使うためだけにソートするのは割に合いません。ソートアルゴリズムの時間計算量は通常 $O(n \log n)$ であり、線形探索や二分探索よりも高いからです。要素を頻繁に挿入する場面では、配列の整列性を保つために特定位置へ挿入する必要があり、その時間計算量は $O(n)$ と高コストです。
|
||||
- 二分探索は配列にしか適していません。二分探索では要素へ飛び飛びにアクセスする必要がありますが、連結リストでそのようなアクセスを行う効率は低いため、連結リストやそれを基に実装されたデータ構造には向きません。
|
||||
- データ量が小さい場合は線形探索のほうが高性能です。線形探索では各ラウンドで 1 回の比較だけで済みますが、二分探索では 1 回の加算、1 回の除算、1 ~ 3 回の比較、1 回の加算(減算)が必要で、合計 4 ~ 6 個の基本操作になります。したがって、データ量 $n$ が小さいときは、線形探索のほうがかえって速くなります。
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
# 二分探索の境界
|
||||
|
||||
## 左境界を見つける
|
||||
## 左端境界を探す
|
||||
|
||||
!!! question
|
||||
|
||||
重複要素を含む可能性がある長さ$n$のソート済み配列`nums`が与えられ、最も左の要素`target`のインデックスを返してください。要素が配列に存在しない場合は、$-1$を返してください。
|
||||
長さ $n$ のソート済み配列 `nums` が与えられ、その中には重複要素が含まれる可能性があります。配列内で最も左にある要素 `target` のインデックスを返してください。配列にこの要素が含まれない場合は、$-1$ を返します。
|
||||
|
||||
挿入位置の二分探索方法を思い出すと、探索完了後、インデックス$i$は`target`の最も左の出現を指します。したがって、**挿入位置の探索は本質的に最も左の`target`のインデックスを見つけることと同じです**。
|
||||
二分探索で挿入位置を求める方法を思い出すと、探索完了後に $i$ は最も左にある `target` を指します。**したがって、挿入位置を探すことの本質は、最も左にある `target` のインデックスを探すことです**。
|
||||
|
||||
挿入位置を見つける関数を使用して`target`の左境界を見つけることができます。配列に`target`が含まれていない可能性があることに注意してください。これは以下の2つの結果につながる可能性があります:
|
||||
挿入位置を探す関数を使って左端境界を求めることを考えます。なお、配列に `target` が含まれない場合があり、そのときは次の 2 つの結果が起こりえます。
|
||||
|
||||
- 挿入位置のインデックス$i$が範囲外です。
|
||||
- 要素`nums[i]`が`target`と等しくありません。
|
||||
- 挿入位置のインデックス $i$ が範囲外になる。
|
||||
- 要素 `nums[i]` が `target` と等しくない。
|
||||
|
||||
これらの場合、単に$-1$を返します。コードは以下の通りです:
|
||||
上の 2 つの状況に当てはまる場合は、直接 $-1$ を返せば十分です。コードは以下のとおりです:
|
||||
|
||||
```src
|
||||
[file]{binary_search_edge}-[class]{}-[func]{binary_search_left_edge}
|
||||
```
|
||||
|
||||
## 右境界を見つける
|
||||
## 右端境界を探す
|
||||
|
||||
`target`の最も右の出現をどのように見つけるでしょうか?最も直接的な方法は、`nums[m] == target`の場合に探索境界を調整する方法を変更して、従来の二分探索ロジックを修正することです。コードはここでは省略されています。興味がある場合は、自分でコードを実装してみてください。
|
||||
では、最も右にある `target` はどのように探せるでしょうか。最も直接的な方法はコードを修正し、`nums[m] == target` の場合のポインタの縮小操作を置き換えることです。ここではコードを省略するので、興味があれば自分で実装してみてください。
|
||||
|
||||
以下では、さらに2つの巧妙な方法を紹介します。
|
||||
ここでは、より巧妙な 2 つの方法を紹介します。
|
||||
|
||||
### 左境界探索を再利用する
|
||||
### 左端境界探索を再利用する
|
||||
|
||||
`target`の最も右の出現を見つけるには、最も左の`target`を見つけるために使用された関数を再利用できます。具体的には、最も右のターゲットの探索を最も左のターゲット + 1の探索に変換します。
|
||||
実際には、最も左の要素を探す関数を利用して最も右の要素を探せます。具体的には、**最も右にある `target` を探すことを、最も左にある `target + 1` を探すことに変換します**。
|
||||
|
||||
下図に示すように、探索完了後、ポインタ$i$は最も左の`target + 1`(存在する場合)を指し、ポインタ$j$は`target`の最も右の出現を指します。したがって、$j$を返すことで右境界が得られます。
|
||||
下図のように、探索完了後、ポインタ $i$ は最も左にある `target + 1`(存在する場合)を指し、$j$ は最も右にある `target` を指します。**したがって $j$ を返せばよいです**。
|
||||
|
||||

|
||||

|
||||
|
||||
返される挿入位置は$i$であることに注意してください。したがって、$j$を得るためには1を引く必要があります:
|
||||
返される挿入位置は $i$ なので、そこから $1$ を引いて $j$ を得る必要があることに注意してください:
|
||||
|
||||
```src
|
||||
[file]{binary_search_edge}-[class]{}-[func]{binary_search_right_edge}
|
||||
@@ -41,16 +41,16 @@
|
||||
|
||||
### 要素探索に変換する
|
||||
|
||||
配列に`target`が含まれていない場合、$i$と$j$は最終的に`target`より大きい最初の要素と小さい最初の要素をそれぞれ指します。
|
||||
配列に `target` が含まれない場合、最終的に $i$ と $j$ はそれぞれ `target` より大きい最初の要素と、`target` より小さい最初の要素を指すことになります。
|
||||
|
||||
したがって、下図に示すように、配列に存在しない要素を構築して、左と右の境界を探索できます。
|
||||
したがって、下図のように、配列中に存在しない要素を構成して、それを使って左右の境界を探せます。
|
||||
|
||||
- 最も左の`target`を見つけるには:`target - 0.5`を探索することに変換でき、ポインタ$i$を返します。
|
||||
- 最も右の`target`を見つけるには:`target + 0.5`を探索することに変換でき、ポインタ$j$を返します。
|
||||
- 最も左にある `target` の探索:`target - 0.5` を探すことに変換でき、ポインタ $i$ を返します。
|
||||
- 最も右にある `target` の探索:`target + 0.5` を探すことに変換でき、ポインタ $j$ を返します。
|
||||
|
||||

|
||||

|
||||
|
||||
コードはここでは省略されていますが、このアプローチについて注意すべき2つの重要な点があります。
|
||||
ここではコードを省略しますが、次の 2 点に注意が必要です。
|
||||
|
||||
- 与えられた配列`nums`には小数が含まれていないため、等しい場合の処理は心配ありません。
|
||||
- ただし、このアプローチで小数を導入するには、`target`変数を浮動小数点型に変更する必要があります(Pythonでは変更は不要です)。
|
||||
- 与えられた配列には小数が含まれないため、等しい場合をどう処理するかを気にする必要はありません。
|
||||
- この方法では小数を導入するため、関数内の変数 `target` を浮動小数点数型に変更する必要があります(Python は変更不要です)。
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
# 二分探索による挿入
|
||||
# 二分探索の挿入位置
|
||||
|
||||
二分探索は目標要素を探索するだけでなく、目標要素の挿入位置を探索するなど、多くの変種問題を解決するためにも使用されます。
|
||||
二分探索は目標要素の検索だけでなく、目標要素の挿入位置を探すなど、多くの派生問題の解決にも利用できます。
|
||||
|
||||
## 重複要素がない場合
|
||||
|
||||
!!! question
|
||||
|
||||
一意の要素を持つ長さ$n$のソート済み配列`nums`と要素`target`が与えられ、ソート順を維持しながら`target`を`nums`に挿入します。`target`が配列にすでに存在する場合は、既存の要素の左側に挿入します。挿入後の配列における`target`のインデックスを返してください。下図に示す例を参照してください。
|
||||
長さ $n$ の整列済み配列 `nums` と要素 `target` が与えられます。配列には重複要素は存在しません。ここで `target` を配列 `nums` に挿入し、その順序を保ちます。配列中にすでに要素 `target` が存在する場合は、その左側に挿入します。挿入後の配列における `target` のインデックスを返してください。例を以下の図に示します。
|
||||
|
||||

|
||||

|
||||
|
||||
前のセクションの二分探索コードを再利用したい場合、以下の2つの質問に答える必要があります。
|
||||
前節の二分探索コードを再利用したい場合は、次の二つの問題に答える必要があります。
|
||||
|
||||
**質問1**:配列にすでに`target`が含まれている場合、挿入位置は既存要素のインデックスになりますか?
|
||||
**問題 1**:配列に `target` が含まれる場合、挿入位置のインデックスはその要素のインデックスですか?
|
||||
|
||||
`target`を等しい要素の左側に挿入するという要件は、新しく挿入される`target`が元の`target`の位置を置き換えることを意味します。つまり、**配列に`target`が含まれている場合、挿入位置は確かにその`target`のインデックスです**。
|
||||
問題では `target` を等しい要素の左側に挿入するよう求めているため、新しく挿入された `target` は元の `target` の位置に入ります。つまり、**配列に `target` が含まれる場合、挿入位置のインデックスはその `target` のインデックスです**。
|
||||
|
||||
**質問2**:配列に`target`が含まれていない場合、どのインデックスに挿入されますか?
|
||||
**問題 2**:配列に `target` が存在しない場合、挿入位置はどの要素のインデックスですか?
|
||||
|
||||
二分探索プロセスをさらに考えてみましょう:`nums[m] < target`のとき、ポインタ$i$が移動します。これは、ポインタ$i$が`target`以上の要素に近づいていることを意味します。同様に、ポインタ$j$は常に`target`以下の要素に近づいています。
|
||||
二分探索の過程をさらに考えると、`nums[m] < target` のときは $i$ が移動します。これは、ポインタ $i$ が `target` 以上の要素へ近づいていることを意味します。同様に、ポインタ $j$ は常に `target` 以下の要素へ近づいています。
|
||||
|
||||
したがって、二分の終了時には確実に:$i$は`target`より大きい最初の要素を指し、$j$は`target`より小さい最初の要素を指します。**配列に`target`が含まれていない場合、挿入位置は$i$であることは明らかです**。コードは以下の通りです:
|
||||
したがって二分探索の終了時には、$i$ は最初の `target` より大きい要素を指し、$j$ は最初の `target` より小さい要素を指します。**よって、配列に `target` が含まれない場合、挿入インデックスは $i$ です**。コードは次のとおりです:
|
||||
|
||||
```src
|
||||
[file]{binary_search_insertion}-[class]{}-[func]{binary_search_insertion_simple}
|
||||
@@ -30,28 +30,28 @@
|
||||
|
||||
!!! question
|
||||
|
||||
前の質問に基づいて、配列に重複要素が含まれている可能性があると仮定し、他はすべて同じとします。
|
||||
前問を踏まえ、配列には重複要素が含まれる可能性があるものとし、それ以外の条件は変わりません。
|
||||
|
||||
配列に`target`の複数の出現がある場合、通常の二分探索は`target`の1つの出現のインデックスのみを返すことができ、**その位置の左右に`target`の出現がいくつあるかを特定することはできません**。
|
||||
配列中に複数の `target` が存在する場合、通常の二分探索ではそのうち一つの `target` のインデックスしか返せず、**その要素の左側と右側にあといくつ `target` があるかは分かりません**。
|
||||
|
||||
問題では目標要素を最も左の位置に挿入することが要求されているため、**配列内の最も左の`target`のインデックスを見つける必要があります**。最初に下図に示すステップを通してこれを実装することを考えてみましょう。
|
||||
問題では目標要素を最も左に挿入する必要があるため、**配列中で最も左にある `target` のインデックスを探す必要があります**。まずは以下の図に示す手順で実現することを考えます。
|
||||
|
||||
1. 二分探索を実行して`target`の任意のインデックス、例えば$k$を見つけます。
|
||||
2. インデックス$k$から開始して、最も左の`target`の出現が見つかるまで左に線形探索を行い、このインデックスを返します。
|
||||
1. 二分探索を実行し、任意の `target` のインデックスを得て、これを $k$ とします。
|
||||
2. インデックス $k$ から始めて左へ線形探索し、最も左の `target` を見つけたら返します。
|
||||
|
||||

|
||||

|
||||
|
||||
この方法は実現可能ですが、線形探索を含むため、時間計算量は$O(n)$です。この方法は、配列に多くの重複する`target`が含まれている場合に非効率です。
|
||||
この方法は使用できますが、線形探索を含むため、時間計算量は $O(n)$ です。配列中に重複した `target` が多い場合、この方法の効率は低くなります。
|
||||
|
||||
今度は二分探索コードを拡張することを考えてみましょう。下図に示すように、全体的なプロセスは同じままです。各ラウンドで、まず中間インデックス$m$を計算し、次に`target`と`nums[m]`の値を比較して、以下のケースになります。
|
||||
次に、二分探索のコードを拡張することを考えます。以下の図に示すように、全体の流れは変えず、各反復でまず中点インデックス $m$ を計算し、その後 `target` と `nums[m]` の大小関係を判定して、次のいくつかの状況に分けます。
|
||||
|
||||
- `nums[m] < target`または`nums[m] > target`のとき、これは`target`がまだ見つかっていないことを意味するため、通常の二分探索を使用して探索範囲を狭め、**ポインタ$i$と$j$を`target`に近づけます**。
|
||||
- `nums[m] == target`のとき、これは`target`より小さい要素が範囲$[i, m - 1]$にあることを示すため、$j = m - 1$を使用して範囲を狭め、**ポインタ$j$を`target`より小さい要素に近づけます**。
|
||||
- `nums[m] < target` または `nums[m] > target` のときは、まだ `target` を見つけていないことを意味するため、通常の二分探索と同じ区間縮小を行い、**ポインタ $i$ と $j$ を `target` に近づけます**。
|
||||
- `nums[m] == target` のときは、`target` より小さい要素が区間 $[i, m - 1]$ にあることを意味するため、$j = m - 1$ として区間を縮小し、**ポインタ $j$ を `target` より小さい要素に近づけます**。
|
||||
|
||||
ループ後、$i$は最も左の`target`を指し、$j$は`target`より小さい最初の要素を指すため、**インデックス$i$が挿入位置です**。
|
||||
ループ終了後、$i$ は最も左の `target` を指し、$j$ は最初の `target` より小さい要素を指すため、**インデックス $i$ が挿入位置です**。
|
||||
|
||||
=== "<1>"
|
||||

|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
@@ -74,9 +74,9 @@
|
||||
=== "<8>"
|
||||

|
||||
|
||||
以下のコードを観察してください。分岐`nums[m] > target`と`nums[m] == target`の操作は同じであるため、これら2つの分岐をマージできます。
|
||||
以下のコードを観察すると、分岐 `nums[m] > target` と `nums[m] == target` の処理は同じであるため、両者はまとめることができます。
|
||||
|
||||
それでも、ロジックがより明確になり、可読性が向上するため、条件を展開したままにしておくことができます。
|
||||
それでも、判定条件を分けたままにしておくことは可能であり、そのほうがロジックがより明確で、可読性も高くなります。
|
||||
|
||||
```src
|
||||
[file]{binary_search_insertion}-[class]{}-[func]{binary_search_insertion}
|
||||
@@ -84,8 +84,8 @@
|
||||
|
||||
!!! tip
|
||||
|
||||
このセクションのコードは「閉区間」を使用しています。「左閉右開」に興味がある場合は、自分でコードを実装してみてください。
|
||||
本節のコードはすべて「両閉区間」の書き方です。興味のある読者は「左閉右開」の書き方を自分で実装してみてください。
|
||||
|
||||
要約すると、二分探索は本質的にポインタ$i$と$j$の探索目標を設定することです。これらの目標は特定の要素(`target`など)または要素の範囲(`target`より小さいものなど)である可能性があります。
|
||||
要するに、二分探索とはポインタ $i$ と $j$ にそれぞれ探索目標を設定することにほかなりません。目標は具体的な要素(たとえば `target`)である場合もあれば、要素の範囲(たとえば `target` より小さい要素)である場合もあります。
|
||||
|
||||
二分探索の連続ループにおいて、ポインタ$i$と$j$は段階的に事前定義された目標に近づきます。最終的に、それらは答えを見つけるか、境界を越えた後に停止します。
|
||||
繰り返される二分のループの中で、ポインタ $i$ と $j$ はどちらも事前に定めた目標へ徐々に近づいていきます。最終的に、それらは答えを見つけるか、境界を越えたところで停止します。
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# 探索
|
||||
|
||||

|
||||

|
||||
|
||||
!!! abstract
|
||||
|
||||
探索は未知への冒険です。神秘的な空間の隅々まで巡る必要があるかもしれませんし、あるいはすぐに目標を見つけることができるかもしれません。
|
||||
|
||||
この発見の旅において、それぞれの探査は予期しない答えで終わるかもしれません。
|
||||
探索は未知の冒険であり、私たちは神秘的な空間の隅々まで歩き回る必要があるかもしれず、あるいは素早く目標を特定できるかもしれません。
|
||||
|
||||
この探索の旅において、すべての探求が思いもよらなかった答えをもたらすかもしれません。
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
# ハッシュ最適化戦略
|
||||
# ハッシュによる最適化戦略
|
||||
|
||||
アルゴリズム問題において、**線形探索をハッシュベースの探索に置き換えることで、アルゴリズムの時間計算量を削減することがよくあります**。アルゴリズム問題を使用して理解を深めましょう。
|
||||
アルゴリズムの問題では,**線形探索をハッシュ探索に置き換えることでアルゴリズムの時間計算量を下げることがよくあります**。ここでは,あるアルゴリズム問題を通じて理解を深めましょう。
|
||||
|
||||
!!! question
|
||||
|
||||
整数配列`nums`と目標要素`target`が与えられ、配列内で「和」が`target`に等しい2つの要素を探索し、それらの配列インデックスを返してください。任意の解が受け入れられます。
|
||||
整数配列 `nums` と目標要素 `target` が与えられたとき,配列内から和が `target` となる 2 つの要素を探索し,それらの配列インデックスを返してください。任意の 1 つの解を返せば十分です。
|
||||
|
||||
## 線形探索:時間を空間と交換
|
||||
## 線形探索:時間と引き換えに空間を節約
|
||||
|
||||
すべての可能な組み合わせを直接横断することを考えてみます。下図に示すように、ネストしたループを開始し、各反復で2つの整数の和が`target`に等しいかどうかを判断します。そうであれば、それらのインデックスを返します。
|
||||
考えられるすべての組み合わせを直接走査することを考えます。次の図に示すように,2 重ループを開始し,各ラウンドで 2 つの整数の和が `target` であるかを判定します。そうであれば,それらのインデックスを返します。
|
||||
|
||||

|
||||

|
||||
|
||||
コードは以下の通りです:
|
||||
コードは次のとおりです:
|
||||
|
||||
```src
|
||||
[file]{two_sum}-[class]{}-[func]{two_sum_brute_force}
|
||||
```
|
||||
|
||||
この方法の時間計算量は$O(n^2)$、空間計算量は$O(1)$で、大容量データでは非常に時間がかかる可能性があります。
|
||||
この方法の時間計算量は $O(n^2)$ ,空間計算量は $O(1)$ であり,大規模データでは非常に時間がかかります。
|
||||
|
||||
## ハッシュ探索:空間を時間と交換
|
||||
## ハッシュ探索:空間と引き換えに時間を節約
|
||||
|
||||
ハッシュテーブルの使用を考えてみましょう。キーと値のペアはそれぞれ配列要素とそのインデックスです。配列をループし、各反復中に下図に示すステップを実行します。
|
||||
ハッシュテーブルを利用し,キーと値をそれぞれ配列要素と要素のインデックスにします。配列をループで走査し,各ラウンドで次の図に示す手順を実行します。
|
||||
|
||||
1. 数値`target - nums[i]`がハッシュテーブルにあるかどうかを確認します。ある場合は、これら2つの要素のインデックスを直接返します。
|
||||
2. キーと値のペア`nums[i]`とインデックス`i`をハッシュテーブルに追加します。
|
||||
1. 数値 `target - nums[i]` がハッシュテーブル内にあるかを判定します。あれば,この 2 つの要素のインデックスを直接返します。
|
||||
2. キーと値の組 `nums[i]` とインデックス `i` をハッシュテーブルに追加します。
|
||||
|
||||
=== "<1>"
|
||||

|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
@@ -36,12 +36,12 @@
|
||||
=== "<3>"
|
||||

|
||||
|
||||
実装コードは以下に示され、単一のループのみが必要です:
|
||||
実装コードは次のとおりで,単一ループだけで済みます:
|
||||
|
||||
```src
|
||||
[file]{two_sum}-[class]{}-[func]{two_sum_hash_table}
|
||||
```
|
||||
|
||||
この方法は、ハッシュ探索を使用することで時間計算量を$O(n^2)$から$O(n)$に削減し、実行時効率を大幅に向上させます。
|
||||
この方法ではハッシュ探索によって時間計算量を $O(n^2)$ から $O(n)$ に下げ,実行効率を大幅に向上させます。
|
||||
|
||||
追加のハッシュテーブルを維持する必要があるため、空間計算量は$O(n)$です。**それにもかかわらず、この方法は全体的により均衡のとれた時空間効率を持ち、この問題の最適解となります**。
|
||||
追加のハッシュテーブルを維持する必要があるため,空間計算量は $O(n)$ です。**それでも,この方法は全体として時間と空間の効率のバランスがより良く,本問の最適解です**。
|
||||
|
||||
@@ -1,84 +1,84 @@
|
||||
# 探索アルゴリズムの再検討
|
||||
# 探索アルゴリズム再考
|
||||
|
||||
<u>探索アルゴリズム(検索アルゴリズム)</u>は、配列、連結リスト、木、グラフなどのデータ構造内で特定の基準を満たす1つ以上の要素を取得するために使用されます。
|
||||
<u>探索アルゴリズム(searching algorithm)</u>は、データ構造(配列、連結リスト、木、グラフなど)の中から、特定の条件を満たす 1 つまたは複数の要素を探索するために用いられます。
|
||||
|
||||
探索アルゴリズムは、そのアプローチに基づいて以下の2つのカテゴリに分けることができます。
|
||||
探索アルゴリズムは、実装の考え方に応じて次の 2 種類に分けられます。
|
||||
|
||||
- **データ構造を横断することで目標要素を特定する**:配列、連結リスト、木、グラフの横断など。
|
||||
- **データの組織構造や既存のデータを使用して効率的な要素探索を実現する**:二分探索、ハッシュ探索、二分探索木探索など。
|
||||
- **データ構造を走査して目標要素を特定する方法**。配列、連結リスト、木、グラフの走査などがこれに当たります。
|
||||
- **データの構成やデータに含まれる事前情報を利用して、要素を効率よく探す方法**。二分探索、ハッシュ探索、二分探索木による探索などがこれに当たります。
|
||||
|
||||
これらのトピックは前の章で紹介されたため、私たちには馴染みのないものではありません。このセクションでは、より体系的な観点から探索アルゴリズムを再検討します。
|
||||
これらのトピックはすでに前の章で扱っているため、探索アルゴリズムは私たちにとって見慣れたものです。本節では、より体系的な視点から探索アルゴリズムをあらためて見直します。
|
||||
|
||||
## 総当たり探索
|
||||
|
||||
総当たり探索は、データ構造のすべての要素を横断することで目標要素を特定します。
|
||||
総当たり探索は、データ構造の各要素を順に調べて目標要素を特定します。
|
||||
|
||||
- 「線形探索」は配列や連結リストなどの線形データ構造に適しています。データ構造の一端から開始し、目標要素が見つかるか、目標要素を見つけることなく他端に到達するまで、各要素に一つずつアクセスします。
|
||||
- 「幅優先探索」と「深さ優先探索」は、グラフと木の2つの横断戦略です。幅優先探索は初期ノードから開始し、層ごと(左から右へ)に探索し、近くから遠くのノードにアクセスします。深さ優先探索は初期ノードから開始し、パスの終端(上から下へ)まで追跡し、その後バックトラックして他のパスを試し、データ構造全体が横断されるまで続行します。
|
||||
- “線形探索”は配列や連結リストなどの線形データ構造に適しています。データ構造の一端から始めて、要素を 1 つずつ調べ、目標要素が見つかるか、もう一方の端に達しても見つからないまで続けます。
|
||||
- “幅優先探索”と“深さ優先探索”は、グラフと木における 2 つの走査戦略です。幅優先探索は初期ノードから始めて層ごとに探索し、近いところから遠いところへ各ノードを訪れます。深さ優先探索は初期ノードから始めて 1 本の経路を最後までたどり、その後でバックトラックしてほかの経路を試し、データ構造全体を走査し終えるまで続けます。
|
||||
|
||||
総当たり探索の利点は、その単純さと汎用性であり、**データの前処理や追加のデータ構造の助けが不要**です。
|
||||
総当たり探索の利点は、単純で汎用性が高く、**データの前処理や追加のデータ構造を必要としない**ことです。
|
||||
|
||||
ただし、**このタイプのアルゴリズムの時間計算量は$O(n)$**で、$n$は要素数であるため、大規模なデータセットでは性能が悪くなります。
|
||||
しかし、**この種のアルゴリズムの時間計算量は $O(n)$ です**。ここで $n$ は要素数であり、そのためデータ量が大きい場合は性能が低くなります。
|
||||
|
||||
## 適応的探索
|
||||
## 適応的な探索
|
||||
|
||||
適応的探索は、データの固有の性質(順序など)を使用して探索プロセスを最適化し、それにより目標要素をより効率的に特定します。
|
||||
適応的な探索は、データが持つ固有の性質(整列性など)を利用して探索過程を最適化し、目標要素をより効率よく特定します。
|
||||
|
||||
- 「二分探索」はデータの整列性を使用して効率的な探索を実現し、配列にのみ適用可能です。
|
||||
- 「ハッシュ探索」はハッシュテーブルを使用して探索データと目標データの間にキーと値のマッピングを確立し、それによりクエリ操作を実装します。
|
||||
- 特定の木構造(二分探索木など)での「木探索」は、ノード値の比較に基づいてノードを迅速に除外し、それにより目標要素を特定します。
|
||||
- “二分探索”は、データの順序性を利用して効率的な探索を行う方法で、配列にしか適用できません。
|
||||
- “ハッシュ探索”は、ハッシュ表を用いて探索対象のデータと目標データをキーと値の対応にし、問い合わせ操作を実現します。
|
||||
- “木探索”は、特定の木構造(たとえば二分探索木)の中で、ノード値の比較に基づいて不要なノードをすばやく除外し、目標要素を特定します。
|
||||
|
||||
これらのアルゴリズムの利点は高効率であり、**時間計算量が$O(\log n)$または$O(1)$にまで達します**。
|
||||
この種のアルゴリズムの利点は効率が高く、**時間計算量が $O(\log n)$ あるいは $O(1)$ に達する**ことです。
|
||||
|
||||
ただし、**これらのアルゴリズムを使用するには、多くの場合データの前処理が必要です**。例えば、二分探索では事前に配列をソートする必要があり、ハッシュ探索と木探索の両方で追加のデータ構造の助けが必要です。これらの構造を維持することも、時間と空間の面でより多くのオーバーヘッドが必要です。
|
||||
しかし、**これらのアルゴリズムを使うには、たいていデータの前処理が必要です**。たとえば、二分探索では事前に配列をソートする必要があり、ハッシュ探索と木探索では追加のデータ構造が必要です。これらのデータ構造を維持するにも、追加の時間と空間のコストがかかります。
|
||||
|
||||
!!! tip
|
||||
|
||||
適応的探索アルゴリズムは、多くの場合探索アルゴリズムと呼ばれ、**主に特定のデータ構造内で目標要素を迅速に取得するために使用されます**。
|
||||
適応的な探索アルゴリズムは、しばしば検索アルゴリズムとも呼ばれ、**主に特定のデータ構造の中で目標要素を高速に取得するために用いられます**。
|
||||
|
||||
## 探索方法の選択
|
||||
## 探索手法の選択
|
||||
|
||||
サイズ$n$のデータセットが与えられた場合、線形探索、二分探索、木探索、ハッシュ探索、またはその他の方法を使用して目標要素を取得できます。これらの方法の動作原理を下図に示します。
|
||||
大きさ $n$ のデータ集合が与えられたとき、線形探索、二分探索、木探索、ハッシュ探索など、さまざまな方法で目標要素を探索できます。各手法の動作原理を下図に示します。
|
||||
|
||||

|
||||

|
||||
|
||||
前述の方法の特性と操作効率を以下の表に示します。
|
||||
上記のいくつかの手法について、操作効率と特性を次の表に示します。
|
||||
|
||||
<p align="center"> 表 <id> 探索アルゴリズム効率の比較 </p>
|
||||
<p align="center"> 表 <id> 探索アルゴリズムの効率比較 </p>
|
||||
|
||||
| | 線形探索 | 二分探索 | 木探索 | ハッシュ探索 |
|
||||
| ------------------ | ------------- | --------------------- | --------------------------- | -------------------------- |
|
||||
| 要素探索 | $O(n)$ | $O(\log n)$ | $O(\log n)$ | $O(1)$ |
|
||||
| 要素挿入 | $O(1)$ | $O(n)$ | $O(\log n)$ | $O(1)$ |
|
||||
| 要素削除 | $O(n)$ | $O(n)$ | $O(\log n)$ | $O(1)$ |
|
||||
| 追加空間 | $O(1)$ | $O(1)$ | $O(n)$ | $O(n)$ |
|
||||
| データ前処理 | / | ソート $O(n \log n)$ | 木構築 $O(n \log n)$ | ハッシュテーブル構築 $O(n)$ |
|
||||
| データ順序性 | 無順序 | 順序 | 順序 | 無順序 |
|
||||
| | 線形探索 | 二分探索 | 木探索 | ハッシュ探索 |
|
||||
| ------------ | -------- | ------------------ | ------------------ | --------------- |
|
||||
| 要素探索 | $O(n)$ | $O(\log n)$ | $O(\log n)$ | $O(1)$ |
|
||||
| 要素挿入 | $O(1)$ | $O(n)$ | $O(\log n)$ | $O(1)$ |
|
||||
| 要素削除 | $O(n)$ | $O(n)$ | $O(\log n)$ | $O(1)$ |
|
||||
| 追加領域 | $O(1)$ | $O(1)$ | $O(n)$ | $O(n)$ |
|
||||
| データ前処理 | / | ソート $O(n \log n)$ | 木構築 $O(n \log n)$ | ハッシュ表構築 $O(n)$ |
|
||||
| データの順序性 | なし | あり | あり | なし |
|
||||
|
||||
探索アルゴリズムの選択は、データ量、探索性能要件、データクエリと更新の頻度などにも依存します。
|
||||
探索アルゴリズムの選択は、規模、探索性能の要求、データの問い合わせ頻度や更新頻度などにも左右されます。
|
||||
|
||||
**線形探索**
|
||||
|
||||
- 汎用性が良く、データ前処理操作が不要です。データを一度だけクエリする必要がある場合、他の3つの方法のデータ前処理時間は線形探索の時間よりも長くなります。
|
||||
- 小容量のデータに適しており、時間計算量が効率に与える影響は小さいです。
|
||||
- データ更新が非常に頻繁なシナリオに適しています。この方法はデータの追加メンテナンスを必要としないためです。
|
||||
- 汎用性が高く、データの前処理をまったく必要としません。データを 1 回だけ問い合わせればよい場合、ほか 3 つの手法では前処理にかかる時間のほうが、線形探索そのものより長くなることがあります。
|
||||
- 規模の小さいデータに適しています。この場合、時間計算量が効率に与える影響は比較的小さいです。
|
||||
- データ更新頻度が高い場面に適しています。この手法では、データに対する追加の保守が不要だからです。
|
||||
|
||||
**二分探索**
|
||||
|
||||
- より大きなデータ量に適しており、安定した性能と最悪ケースの時間計算量$O(\log n)$を持ちます。
|
||||
- ただし、データ量が大きすぎることはできません。配列の保存には連続したメモリ空間が必要だからです。
|
||||
- 頻繁な追加と削除があるシナリオには適していません。順序付き配列の維持に多くのオーバーヘッドが発生するためです。
|
||||
- 大規模データに適しており、効率が安定しています。最悪時間計算量は $O(\log n)$ です。
|
||||
- データ量が大きすぎる場合には向きません。配列の格納には連続したメモリ領域が必要だからです。
|
||||
- 頻繁な挿入・削除がある場面には向きません。整列配列を維持するコストが高いためです。
|
||||
|
||||
**ハッシュ探索**
|
||||
|
||||
- 高速クエリ性能が不可欠なシナリオに適しており、平均時間計算量は$O(1)$です。
|
||||
- 順序付きデータや範囲探索が必要なシナリオには適していません。ハッシュテーブルはデータの順序性を維持できないためです。
|
||||
- 問い合わせ性能への要求が高い場面に適しており、平均時間計算量は $O(1)$ です。
|
||||
- 順序付きデータや範囲探索が必要な場面には向きません。ハッシュ表ではデータの順序性を維持できないからです。
|
||||
- ハッシュ関数とハッシュ衝突処理戦略への依存度が高く、性能劣化のリスクが大きいです。
|
||||
- 過度に大容量のデータには適していません。ハッシュテーブルは衝突を最小化し、良好なクエリ性能を提供するために追加の空間が必要だからです。
|
||||
- データ量が大きすぎる場合には向きません。ハッシュ表は衝突をできるだけ減らして良好な問い合わせ性能を出すために、追加の空間を必要とするからです。
|
||||
|
||||
**木探索**
|
||||
|
||||
- 大容量データに適しています。木ノードはメモリ内に分散して保存されるためです。
|
||||
- 順序付きデータの維持や範囲探索に適しています。
|
||||
- ノードの継続的な追加と削除により、二分探索木は偏る可能性があり、時間計算量が$O(n)$に劣化する可能性があります。
|
||||
- AVL木や赤黒木を使用する場合、操作は$O(\log n)$効率で安定して実行できますが、木のバランスを維持する操作により追加のオーバーヘッドが追加されます。
|
||||
- 巨大データに適しています。木ノードはメモリ上に分散して格納されるためです。
|
||||
- 順序付きデータの維持や範囲探索が必要な場面に適しています。
|
||||
- ノードの挿入・削除を続ける過程で、二分探索木は偏ることがあり、時間計算量は $O(n)$ まで劣化する可能性があります。
|
||||
- AVL 木や赤黒木を使えば、各種操作を $O(\log n)$ の効率で安定して実行できますが、木の平衡を保つ処理による追加コストが発生します。
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# まとめ
|
||||
|
||||
- 二分探索はデータの順序に依存し、探索区間を反復的に半分にすることで探索を実行します。入力データがソート済みである必要があり、配列または配列ベースのデータ構造にのみ適用可能です。
|
||||
- 無順序データセット内のエントリを見つけるには、総当たり探索が必要な場合があります。データ構造に基づいて異なる探索アルゴリズムを適用できます:線形探索は配列と連結リストに適しており、幅優先探索(BFS)と深さ優先探索(DFS)はグラフと木に適しています。これらのアルゴリズムは非常に汎用性が高く、データの前処理が不要ですが、$O(n)$という高い時間計算量を持ちます。
|
||||
- ハッシュ探索、木探索、二分探索は効率的な探索方法で、特定のデータ構造内で目標要素を迅速に特定できます。これらのアルゴリズムは非常に効率的で、時間計算量が$O(\log n)$または$O(1)$にまで達しますが、通常は追加のデータ構造を収容するために追加の空間が必要です。
|
||||
- 実際には、データ量、探索性能要件、データクエリと更新頻度などの要因を分析して、適切な探索方法を選択する必要があります。
|
||||
- 線形探索は小さなデータや頻繁に更新される(変動性の高い)データに理想的です。二分探索は大きくてソート済みのデータに適しています。ハッシュ探索は高いクエリ効率が必要で範囲クエリが不要なデータに適しています。木探索は順序を維持し、範囲クエリをサポートする必要がある大きな動的データに最も適しています。
|
||||
- 線形探索をハッシュ探索に置き換えることは、実行時性能を最適化する一般的な戦略で、時間計算量を$O(n)$から$O(1)$に削減します。
|
||||
### 要点の振り返り
|
||||
|
||||
- 二分探索はデータの順序性に依存し、ループによって探索区間を半分ずつ縮小しながら探索を行う。入力データがソート済みであることを前提とし、配列または配列ベースで実装されたデータ構造にのみ適用できる。
|
||||
- 総当たり探索はデータ構造を走査してデータを特定する。線形探索は配列と連結リストに適しており、幅優先探索と深さ優先探索はグラフと木に適している。この種のアルゴリズムは汎用性が高く、データの前処理を必要としないが、時間計算量 $O(n)$ は高い。
|
||||
- ハッシュ探索、木探索、二分探索は高効率な探索手法であり、特定のデータ構造内で目的の要素を高速に特定できる。この種のアルゴリズムは効率が高く、時間計算量は $O(\log n)$ あるいは $O(1)$ に達するが、通常は追加のデータ構造を必要とする。
|
||||
- 実際には、データ規模、探索性能の要件、データの問い合わせ頻度や更新頻度などの要因を具体的に分析し、そのうえで適切な探索手法を選択する必要がある。
|
||||
- 線形探索は小規模または頻繁に更新されるデータに適している。二分探索は大規模でソート済みのデータに適している。ハッシュ探索は問い合わせ効率への要求が高く、範囲検索を必要としないデータに適している。木探索は順序の維持と範囲検索のサポートが必要な大規模動的データに適している。
|
||||
- ハッシュ探索で線形探索を置き換えることは、実行時間を最適化するための一般的な戦略であり、時間計算量を $O(n)$ から $O(1)$ へと下げられる。
|
||||
|
||||
Reference in New Issue
Block a user