mirror of
https://github.com/krahets/hello-algo.git
synced 2026-11-11 00:11:00 +08:00
* Retranslate the complete Japanese online edition from Chinese * Fit Japanese homepage labels and localize heading permalinks * Point Japanese contribution instructions to the Japanese preview * Replace locale conditionals with multilingual UI tables
84 lines
5.7 KiB
Markdown
84 lines
5.7 KiB
Markdown
# 二分探索
|
||
|
||
<u>二分探索(binary search)</u>は、分割統治法に基づく効率的な探索アルゴリズムです。データが整列されていることを利用し、各反復で探索範囲を半分に縮め、目的の要素が見つかるか探索区間が空になるまで続けます。
|
||
|
||
!!! question
|
||
|
||
長さ $n$ の配列 `nums` が与えられます。要素は昇順に並び、重複しません。要素 `target` を探し、そのインデックスを返してください。配列にその要素が含まれない場合は $-1$ を返します。例を下図に示します。
|
||
|
||

|
||
|
||
下図のように、まずポインタ $i = 0$ と $j = n - 1$ を初期化し、配列の先頭要素と末尾要素をそれぞれ指すようにします。これらは探索区間 $[0, n - 1]$ を表します。角括弧は閉区間を表し、境界値そのものを含むことに注意してください。
|
||
|
||
続いて、次の二つの手順を繰り返します。
|
||
|
||
1. 中点のインデックス $m = \lfloor {(i + j) / 2} \rfloor$ を計算します。ここで $\lfloor \: \rfloor$ は切り捨てを表します。
|
||
2. `nums[m]` と `target` の大小関係を判定します。次の三つの場合があります。
|
||
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>"
|
||

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

|
||
|
||
=== "<3>"
|
||

|
||
|
||
=== "<4>"
|
||

|
||
|
||
=== "<5>"
|
||

|
||
|
||
=== "<6>"
|
||

|
||
|
||
=== "<7>"
|
||

|
||
|
||
$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(1)$**:ポインタ $i$ と $j$ が使う領域は定数サイズです。
|
||
|
||
## 区間の表し方
|
||
|
||
前述の両端を含む閉区間のほか、「左閉右開」の区間もよく使われます。$[0, n)$ と表し、左端は含みますが右端は含みません。この表し方では、区間 $[i, j)$ は $i = j$ のとき空になります。
|
||
|
||
この表し方でも、同じ機能を持つ二分探索を実装できます。
|
||
|
||
```src
|
||
[file]{binary_search}-[class]{}-[func]{binary_search_lcro}
|
||
```
|
||
|
||
下図のように、区間の表し方が異なると、二分探索の初期化、ループ条件、区間を縮める操作も変わります。
|
||
|
||
両端を含む閉区間では、左右の境界がどちらも含まれるため、ポインタ $i$ と $j$ で区間を縮める操作も対称になります。そのほうが間違いにくいため、**一般には両端を含む閉区間で書くことを勧めます**。
|
||
|
||

|
||
|
||
## 長所と制約
|
||
|
||
二分探索は時間と空間の両面で優れた性能を持ちます。
|
||
|
||
- 二分探索は時間効率が高く、データが多い場合、対数時間の計算量が大きな利点となります。例えば、データの大きさが $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$ が小さいと、線形探索のほうが速くなります。
|