Files
hello-algo/ja/docs/chapter_searching/exercises.md
T
2026-07-19 07:19:42 +08:00

95 lines
5.9 KiB
Markdown

# 演習
## 確認問題
### 二分探索で探索区間を狭める方法
ソート済み配列 `[2, 5, 8, 12, 16, 23, 38]` から 16 を探索します。
両閉区間 `[i, j]` を使い、中点を
$m=i+(j-i)/2$(小数点以下切り捨て)とします。
目的の値が見つかるまで、各回の `(i, j, m)`、中点の要素、次に区間をどのように狭めるかを書いてください。
??? success "解答"
各回の探索過程は次のとおりです。
| 回 | `(i, j, m)` | 中点の要素 | 次の操作 |
| --- | --- | --- | --- |
| 1 | `(0, 6, 3)` | 12 | `12 < 16` なので `i = 4` とする |
| 2 | `(4, 6, 5)` | 23 | `23 > 16` なので `j = 4` とする |
| 3 | `(4, 4, 4)` | 16 | 目的の値を発見し、インデックス 4 を返す |
配列はソート済みなので、中点の値が目的の値より小さければ、中点とその左側を除外できます。
中点の値が目的の値より大きければ、中点とその右側を除外できます。
### 重複する要素の左右の境界
配列 `[1, 2, 2, 2, 4, 6]` から数値 2 を探索します。
ある生徒は、二分探索でインデックス 2 に目的の値を見つけるとすぐに返し、次のように言いました。
「インデックス 2 が数値 2 の左端境界です。」
<!-- numbered-subquestions -->
1. この生徒の説明は正しいですか?数値 2 の左端境界と右端境界は、それぞれどこですか?理由も説明してください。
2. 左端境界を探索するとき、中点の要素が目的の値と等しければ、次にどちら側を探索すべきですか?
3. 右端境界を探索するときは、どちら側を探索すべきですか?方向だけを説明し、探索過程をすべて書く必要はありません。
??? success "解答"
1. この生徒の説明は正しくありません。2 を 1 つ見つけてすぐに返す方法では、いずれかの 2 を見つけたことしか保証できず、最も左または最も右の 2 だとは限りません。
この問題の左端境界はインデックス 1、右端境界はインデックス 3 です。
2. 左端境界を探索するときは、中点の要素が 2 と等しくても、引き続き左側を探索します。
たとえば、両閉区間を使う場合は `j = m - 1` とします。
3. 右端境界を探索するときは、中点の要素が 2 と等しければ、引き続き右側を探索します。
たとえば、`i = m + 1` とします。
### データに応じた探索方法の選び方
「線形探索、二分探索、ハッシュテーブル」から、次の 3 つの場面に適した方法を選び、理由を説明してください。
<!-- numbered-subquestions -->
1. ソート済みで今後変更されない $10^7$ 個の整数を繰り返し探索する。ほかのデータ構造は追加で作らない。
2. 挿入と削除が頻繁に行われるデータ集合で、あるキーが存在するかを繰り返し判定する。順序を保つ必要も、範囲探索を行う必要もない。
3. ソートされていない配列から、ある値を 1 回だけ探索する。
??? success "解答"
1. 二分探索です。データがソート済みで変更されないため、追加の空間を使わず $O(\log n)$ で探索できます。
2. ハッシュテーブルです。ハッシュ関数によってキーが各バケットへほぼ均等に分散される場合、挿入、削除、キーによる探索の平均時間計算量はいずれも $O(1)$ です。
3. 先頭から末尾まで直接走査します。1 回しか探索しない場合、ソートやハッシュテーブルの作成でも、最初に配列全体を処理する必要があるため、
この 1 回の処理に必要な作業の総量は減りません。
どの方法を選ぶかは、データがソート済みか、追加の構造を作れるか、探索回数、必要な操作の種類によって決まります。
## プログラミング演習
### ソート済み配列の二分探索
重複なく昇順に並んだ整数配列 `nums` と目的の値 `target` が与えられます。二分探索を使って `target` を見つけてください。存在する場合はその配列インデックスを返し、存在しない場合は -1 を返します。
??? tip "解法のヒント"
1. 最初の区間は left = 0、right = n - 1 で、区間が空でない条件は left <= right です
2. mid = left + (right - left) // 2 で中点を計算します
3. もし `nums[mid]``target` より小さければ左端を `mid + 1` へ移し、`nums[mid]``target` より大きければ右端を `mid - 1` へ移します。等しければすぐに返します
[LeetCode](https://leetcode.com/problems/binary-search/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
### ソート済み配列への挿入位置
重複なく昇順に並んだ整数配列 `nums` と目的の値 `target` が与えられます。
`target` がすでに配列にあれば、そのインデックスを返します。なければ、`target` を挿入した後も重複のない昇順を保てる位置を返します。答えは 0 の場合も、配列の長さと等しい場合もあります。二分探索を使って求めてください。
??? tip "解法のヒント"
1. 答えは 0 の場合も、配列の長さ n の場合もあります
2. 両閉区間を使う場合、`nums[mid]``target` 以上なら `right = mid - 1` とし、さらに左の位置を調べます。そうでなければ `left = mid + 1` とします
3. ループが終了した時点で、left が挿入位置です
[LeetCode](https://leetcode.com/problems/search-insert-position/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }