再帰の枠組み:基底ケース、信頼、構築
3段階の方法を適用し、すべての呼び出しを追跡することなく、階乗、べき乗、各桁の和の正しい再帰解を記述します。
「再帰の枠組み:基底ケース、信頼、構築」はCoddyKit上の無料Coding Interview Prepレッスンです。 これはレッスン1/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはCoding Interview Prep学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Coding Interview Prepコースには全4レッスンが含まれています。
再帰が難しく感じられる理由
多くの初心者は、再帰呼び出しをすべて頭の中で追跡しようとするため、5段階程度の深さでもすぐに圧倒されてしまいます。専門的なアプローチでは、3段階のフレームワーク、つまり「ベースケース、信頼、構築」を使います。これにより、呼び出し木全体を頭の中でシミュレーションしなくても、正しい再帰関数を書けるようになります。
このフレームワークは、leap of faith と呼ばれることもあります。小さい入力に対して関数が正しく動くと信頼し、その前提を使って大きな入力に対する解を構築します。
ステップ1:ベースケースを定義する
ベースケースとは、これ以上再帰を続けなくても答えがわかる、最も単純な入力です。すべての再帰関数には、少なくとも1つのベースケースが必要です。ベースケースがないと、関数は永遠に再帰し続けます(スタックオーバーフロー)。適切なベースケースには、空のリスト、要素が1つのリスト、n == 0、n == 1、または問題が自明な恒等式に帰着する場合などがあります。
再帰のロジックを書く前に、まずベースケースを書きます。『この問題のうち、すぐに答えられる最小の形は何か』と考えて、ベースケースを見つけてください。
# Base cases for common problems
def factorial(n):
if n == 0: # base case: 0! = 1
return 1
# ... recursive step below
def sum_list(lst):
if not lst: # base case: sum of empty list is 0
return 0
# ...
def height(node):
if node is None: # base case: height of null node is 0
return 0
# ...
print('Base cases identified')ステップ2:再帰呼び出しを信頼する
信頼のステップは、思い切って仮定する段階です。現在の入力より厳密に小さい任意の入力に対して、関数がすでに正しく動作すると仮定します。小さい入力すべてについて、今すぐ証明する必要はありません。帰納法による証明がそれを保証します。小さな部分問題に対して関数を呼び出し、正しい結果が返されると信頼してください。
初心者はこのステップを省略し、代わりに頭の中で処理をシミュレートしようとしがちです。その衝動を抑えてください。この枠組みを身につければ、任意に深い再帰にも対応できます。
# Trust example: sum_list([3, 1, 4, 1, 5])
# Trust: sum_list([1, 4, 1, 5]) = 11 (we TRUST this, don't trace it)
# Build: 3 + 11 = 14
# So:
def sum_list(lst):
if not lst:
return 0
# Trust that sum_list(lst[1:]) returns sum of the rest
return lst[0] + sum_list(lst[1:])
print(sum_list([3, 1, 4, 1, 5])) # 14ステップ3:解を構築する
構築のステップでは、信頼した部分問題の結果と現在の要素による寄与を組み合わせ、入力全体に対する答えを作ります。通常は、現在の要素と再帰呼び出しの結果に対して演算を適用する1行の処理です。よくある構築方法には、合計に加える、リストの先頭に追加する、カウントを増やす、2つの部分結果を結合するなどがあります。
def factorial(n):
if n == 0:
return 1
# Trust: factorial(n-1) gives (n-1)!
# Build: n * (n-1)! = n!
return n * factorial(n - 1)
def power(base, exp):
if exp == 0:
return 1
# Trust: power(base, exp-1) gives base^(exp-1)
# Build: base * base^(exp-1) = base^exp
return base * power(base, exp - 1)
print(factorial(6)) # 720
print(power(2, 10)) # 1024桁の合計にフレームワークを適用する
問題:0以上の整数の各桁の合計を計算します。ベースケース:n == 0 → 合計は0(または n < 10 → n 自身)です。信頼:sumDigits(n // 10) は、最後の桁を除くすべての桁の合計を返します。構築:最後の桁 n % 10 を、信頼した結果に加えます。このフレームワークによって、3つの宣言的なステップで解を導けます。
def sumDigits(n):
if n < 10:
return n # base case: single digit
# Trust: sumDigits(n // 10) gives sum of all digits except last
# Build: add the last digit
return n % 10 + sumDigits(n // 10)
print(sumDigits(0)) # 0
print(sumDigits(7)) # 7
print(sumDigits(123)) # 6
print(sumDigits(9999)) # 36フィボナッチ:2つの部分問題
フィボナッチでは、2つの再帰呼び出し、fib(n-1) と fib(n-2) が必要です。フレームワークを適用しましょう。ベースケースは fib(0) = 0 と fib(1) = 1 です。信頼:2つの小さな入力に対する呼び出しが、正しいフィボナッチ数を返すと仮定します。構築:それらの合計を返します。この素朴な実装の計算量は O(2^n) です。これについては、メモ化のレッスンで改善します。
def fib(n):
if n <= 1:
return n # base cases: fib(0)=0, fib(1)=1
# Trust both smaller sub-problems
return fib(n - 1) + fib(n - 2)
for i in range(8):
print(f'fib({i}) = {fib(i)}') # 0,1,1,2,3,5,8,13文字列を再帰的に反転する
問題:文字列を再帰的に反転します。ベースケース:空文字列または1文字の文字列は、すでに反転されています。信頼:reverse(s[1:]) は、先頭文字以降のすべてを反転した結果を返します。構築:先頭文字を、反転した後半部分の末尾に追加します。このフレームワークによって、3行の解になります。
def reverse_str(s):
if len(s) <= 1:
return s # base case
# Trust: reverse_str(s[1:]) = reverse of 'ello' for 'hello'
# Build: append first character at end
return reverse_str(s[1:]) + s[0]
print(reverse_str('')) # ''
print(reverse_str('a')) # 'a'
print(reverse_str('hello')) # 'olleh'
print(reverse_str('racecar')) # 'racecar'出現回数を再帰的に数える
問題:リスト内で、対象の値が出現する回数を再帰的に数えます。ベースケース:空のリストでは、カウントは0です。信頼:count(lst[1:], target) は、残りの部分における出現回数を返します。構築:先頭の要素が対象と一致する場合は1を加え、それ以外の場合は0を加えます。再帰の各ステップでは、リストのサイズを1減らすことで、ベースケースに近づいていきます。
def count_occurrences(lst, target):
if not lst:
return 0
# Trust: count in rest of list is handled recursively
# Build: add 1 if first element matches, else 0
return (1 if lst[0] == target else 0) + count_occurrences(lst[1:], target)
print(count_occurrences([1, 2, 3, 2, 4, 2], 2)) # 3
print(count_occurrences([], 5)) # 0
print(count_occurrences([7, 7, 7], 7)) # 3リストがソート済みか確認する
問題:リストが昇順にソートされているかを再帰的に確認します。ベースケース:要素数が0または1のリストは、常にソート済みです。信頼:is_sorted(lst[1:]) によって、残りの部分がソート済みかどうかがわかります。構築:先頭の要素が2番目の要素以下であり、かつ残りの部分がソート済みなら、リスト全体もソート済みです。これは、構築ステップで2つの条件の論理 AND を使う、わかりやすい例です。
def is_sorted(lst):
if len(lst) <= 1:
return True
# Trust: is_sorted(lst[1:]) tells us if tail is sorted
# Build: head <= second element AND tail is sorted
return lst[0] <= lst[1] and is_sorted(lst[1:])
print(is_sorted([])) # True
print(is_sorted([1])) # True
print(is_sorted([1, 2, 3, 4])) # True
print(is_sorted([1, 3, 2, 4])) # False二分探索を再帰的に行う(再訪)
フレームワークを使って二分探索を再帰的に表現します。ベースケース:lo > hi → 見つからないため -1 を返します。信頼:正しい半分に対する再帰呼び出しが、対象を見つけるか -1 を返します。構築:中央位置を計算し、比較して、適切な半分に対して呼び出します。反復形式のほうが本番環境では O(1) の空間計算量になるため一般に好まれますが、再帰形式では分割統治の構造が明確に示されます。
def binary_search(arr, target, lo, hi):
if lo > hi: # base case: search space exhausted
return -1
mid = lo + (hi - lo) // 2
if arr[mid] == target:
return mid
# Trust both halves return correct results
if arr[mid] < target:
return binary_search(arr, target, mid + 1, hi)
else:
return binary_search(arr, target, lo, mid - 1)
arr = [1, 3, 5, 7, 9, 11]
print(binary_search(arr, 7, 0, len(arr) - 1)) # 3
print(binary_search(arr, 4, 0, len(arr) - 1)) # -1再帰と反復を使い分ける
再帰は、問題が同じ種類の小さな部分問題へ自然に分解できる場合(木構造、分割統治、バックトラッキング)に力を発揮します。次のような場合は反復が適しています。再帰の深さが大きい場合(デフォルトで約1000に設定されている Python では、スタックオーバーフローのリスクがあります)、再帰版と反復版が同じ程度にわかりやすい場合、または単純なループで表せる問題(階乗、メモ化なしのフィボナッチ)です。
目安として、再帰木を自然に描けるなら再帰を使います。木が一直線になる場合(末尾再帰)は、反復に変換します。
import sys
# Python's default recursion limit
print('Recursion limit:', sys.getrecursionlimit()) # 1000
# A list of 2000 elements would overflow the recursive sum_list
# Use iteration for safety:
def sum_list_iter(lst):
total = 0
for x in lst:
total += x
return total
big = list(range(2000))
print(sum_list_iter(big)) # 1999000 — no stack overflow理解度チェック
このレッスンで扱った Data Structures & Algorithms — Coding Interview Prep の概念について、理解度を確認しましょう。
レッスンのまとめ
このレッスンでは、3ステップのフレームワークが、ベースケース(最も単純で既知の答え)、信頼(部分問題が解決済みだと仮定)、構築(現在の要素と信頼した結果を組み合わせる)で構成されること、最初にベースケースを書き、呼び出し木全体を頭の中で追跡しようとしないこと、そして再帰の深さによってスタックオーバーフローが起きるおそれがある場合や、再帰形式と反復形式が同じ程度に明快な場合は反復を使うことを学びました。次は、呼び出しスタックを詳しく可視化します。
よくある質問
「再帰の枠組み:基底ケース、信頼、構築」レッスンは無料ですか?
はい。「再帰の枠組み:基底ケース、信頼、構築」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Coding Interview Prepコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Coding Interview Prepコースには全4レッスンが含まれています。
「再帰の枠組み:基底ケース、信頼、構築」で何を学びますか?
3段階の方法を適用し、すべての呼び出しを追跡することなく、階乗、べき乗、各桁の和の正しい再帰解を記述します。 ブラウザで直接実行するハンズオンコードでCoding Interview Prepを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
Coding Interview Prepを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのCoding Interview Prepは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン1/4です。
「再帰の枠組み:基底ケース、信頼、構築」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このCoding Interview Prepレッスンでコードを書いて実行できますか?
はい。すべてのCoding Interview Prepレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- 再帰の枠組み:基底ケース、信頼、構築
- コールスタックを可視化する
- 再帰と反復のトレードオフ
- メモ化:再帰結果をキャッシュする