第5回:再帰と分割統治法¶
前回コールスタックを学んだ。今回は「関数が自分自身を呼ぶ」という発想と、それを使った代表的アルゴリズムを見る。
実験1:関数が自分自身を呼ぶコード¶
何が出力されるか、走らせる前に予想してから実行してみよう。
実行すると 120 が出力される。関数 fact の中で、また fact を呼んでいる。これを再帰という。
再帰関数¶
再帰関数: 関数の中で自分自身を呼ぶ関数。2つの部品が必要。
- 基底ケース(base case): もう自分を呼ばずに答えを返す条件
- 再帰ケース(recursive case): 自分自身を呼ぶ部分
どちらが欠けても壊れる
- 基底ケースがない → 永遠に止まらない(無限再帰)
- 再帰ケースがない → ただの普通の関数
階乗(factorial)の例¶
動作:
再帰はコールスタックで動いている¶
前回学んだコールスタックそのもの。「最後に呼ばれた関数が、最初に終わる」 = LIFO。
fact(3) を呼ぶと:
push fact(3)
push fact(2)
push fact(1)
push fact(0) → 1 を返す → pop
1 を返す → pop
2 を返す → pop
6 を返す → pop
演習5-1¶
fact(4) の値はいくつになるか? 計算過程を、コールスタックの動きとして書け(fact(4) = 4 * fact(3) = ... の形)。
二分探索(Binary Search)¶
整列されたデータから目的の値を探すアルゴリズム。
手順¶
- 真ん中の要素を見る
- 目的の値と比較
- 目的の値が小さい → 左半分だけ調べる
- 目的の値が大きい → 右半分だけ調べる
- 1.に戻る
毎回、調べる範囲が半分になる。
何回半分にできるか? → \(\log_2(n)\) 回。 これが「最悪ケースでもこの回数で済む」 → O(log n)。
二分探索の計算量¶
| n | 線形探索 O(n) | 二分探索 O(log n) |
|---|---|---|
| 1,000 | 最大 1,000 | 最大 10 |
| 100,000 | 最大 100,000 | 最大 17 |
| 10億 | 最大 10億 | 最大 30 |
データが大きくなるほど差が圧倒的に開く。ただし整列されていないと使えない。
補足:再帰を使った二分探索の実装¶
二分探索は「半分にして同じ操作を繰り返す」アルゴリズムなので、再帰で素直に書ける。
def binary_search(arr, target, left, right):
# 基底ケース:範囲が無効なら「見つからなかった」を返す
if left > right:
return -1
# 真ん中のインデックスを計算
mid = (left + right) // 2
if arr[mid] == target:
return mid # 見つかった
elif arr[mid] < target:
return binary_search(arr, target, mid + 1, right) # 右半分だけ再帰
else:
return binary_search(arr, target, left, mid - 1) # 左半分だけ再帰
arr = [1, 4, 7, 10, 13, 16, 19, 22, 25]
print(binary_search(arr, 19, 0, len(arr) - 1)) # → 6(インデックス)
print(binary_search(arr, 100, 0, len(arr) - 1)) # → -1(見つからない)
// ってなに?
// は切り捨て除算(floor division)。割り算の結果の小数点以下を切り捨てて整数にする。
7 / 2 # → 3.5 (普通の割り算、float になる)
7 // 2 # → 3 (切り捨て除算、int のまま)
8 // 2 # → 4
9 // 2 # → 4 (4.5 → 4 に切り捨て)
インデックスは整数でなければいけないので、mid = (left + right) // 2 で // を使う。/ だと mid が float になって arr[mid] がエラーになる。
再帰と分割統治の対応関係
上のコードはまさに分割統治の3ステップそのもの:
- Divide:
midで半分に分ける - Conquer: 片方を再帰呼び出しで解く
- Combine: 不要(片方しか見ないから、結果をそのまま return する)
演習5-2¶
配列 [1, 4, 7, 10, 13, 16, 19, 22, 25] から 19 の場所を二分探索で探す。比較する要素を順に並べよ(何回目に何を見るか)。
分割統治法(Divide and Conquer)¶
大きな問題を小さな部分問題に分割 → それぞれ解く → 統合する。
3ステップ:
- Divide: 問題を小さく分ける
- Conquer: 小さな問題を解く(多くの場合、再帰で)
- Combine: 結果を組み合わせる
二分探索は分割統治の特殊例¶
二分探索を3ステップで見ると:
- Divide: 真ん中で半分に分ける
- Conquer: 片方だけ調べる(再帰的に)
- Combine: 必要ない(片方しか見ないから)
二分探索は「片方を捨てる」シンプルな分割統治。 一般的な分割統治は「両方解いて統合する」で、これは第7回のソートで本格的に出てくる。
分割統治の威力¶
- O(n) のループで解いていた問題を O(log n) や O(n log n) に圧縮できる
- 「問題を半分にする」だけで、計算量のスケールが変わる
- データが大きくなるほど、効いてくる
演習5-3¶
以下のコードは「リストの合計を求める」目的で生成AIが出力したものです。問題点として最も適切なものを選べ。
- A. 正しく動作するが効率が悪い
- B. 空リストを渡すとエラーになる
- C. 結果が間違っている
- D. 問題ない
再帰コードを読むときの第一チェックポイント
基底ケースの有無を最初に確認する。基底ケースがないと、いつまでも自分を呼び続けてしまう。