一覧へ

再帰関数 — 自分自身を呼び出す関数

再帰関数がどのように機能するか、コールスタックとは何か、なぜ終了条件が重要なのかをステップごとに学びます。

中級
|
10
|
検証済み (2026-07)
再帰関数base caseコールスタック分割統治階乗
進捗0/23 (0%)

再帰関数 — 自分自身を呼び出す関数

このトピックを終えると

再帰関数の仕組みを説明でき、終了条件(ベースケース)の重要性を理解し、簡単な再帰問題を自分で解くことができます。


再帰とは何か

再帰(recursion)とは、関数が自分自身を呼び出すことです。

python
def countdown(n):
if n <= 0:
print("Launch!")
return
print(n)
countdown(n - 1) # 自分自身を呼び出す
countdown(5)
# 5
# 4
# 3
# 2
# 1
# Launch!

countdown(5)countdown(4)を呼び、countdown(4)countdown(3)を呼び… countdown(0)になると、"Launch!"と出力して停止します。

これが再帰のすべてです。2つの要素があります。

  1. 終了条件(ベースケース): if n <= 0 — これ以上呼び出さない条件
  2. 再帰ステップ(再帰ケース): countdown(n - 1) — 自分自身を呼び出すが、問題をより小さくする

なぜ終了条件が不可欠なのか

終了条件がない場合、無限に自分自身を呼び出し続けます。

python
def infinite():
print("Help!")
infinite() # 永遠に呼び出す
infinite()
# Help!
# Help!
# Help!
# ...
# RecursionError: maximum recursion depth exceeded

Pythonでは、基本的に1,000回までしか再帰を許可しません。それ以上になると、RecursionErrorで強制終了します。これは、無限再帰によってメモリが使い果たされるのを防ぐための安全装置です。


階乗 — 再帰の教科書的な例

5! = 5 × 4 × 3 × 2 × 1 = 120

階乗を再帰的に考えると、5! = 5 × 4!となります。

python
def factorial(n):
# ベースケース
if n <= 1:
return 1
# 再帰ケース
return n * factorial(n - 1)
print(factorial(5)) # 120

呼び出しのプロセスを追うと:

text
factorial(5)
  → 5 * factorial(4)
       → 4 * factorial(3)
            → 3 * factorial(2)
                 → 2 * factorial(1)
                      → 1  (ベースケース!)
                 ← 2 * 1 = 2
            ← 3 * 2 = 6
       ← 4 * 6 = 24
  ← 5 * 24 = 120

関数が「深く入り込み」、結果を「再び戻る」構造です。


コールスタック — 再帰が動作する仕組み

関数が呼び出されるたびに、コンピュータは**スタック(stack)**と呼ばれるメモリ空間に現在の状態を保存します。

text
factorial(5) の呼び出し → スタック: [factorial(5)]
factorial(4) の呼び出し → スタック: [factorial(5), factorial(4)]
factorial(3) の呼び出し → スタック: [factorial(5), factorial(4), factorial(3)]
factorial(2) の呼び出し → スタック: [factorial(5), factorial(4), factorial(3), factorial(2)]
factorial(1) の呼び出し → スタック: [factorial(5), factorial(4), factorial(3), factorial(2), factorial(1)]
factorial(1) の戻り値 → スタック: [factorial(5), factorial(4), factorial(3), factorial(2)]
factorial(2) の戻り値 → スタック: [factorial(5), factorial(4), factorial(3)]
...

再帰が深くなるほど、スタックが積み重なります。Pythonのデフォルトの制限が1,000であるのは、スタックごとにメモリを使用するため、深すぎるとメモリが不足するのを防ぐためです。


フィボナッチ数 — 再帰の落とし穴

python
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
print(fib(10)) # 55
print(fib(30)) # 832040 — しかし遅い!
# print(fib(50)) # 終わらない…

コードは簡潔ですが、fib(50)は永遠に終わりません。同じ値を繰り返し計算するためです。

text
fib(5)
├── fib(4)
│   ├── fib(3)
│   │   ├── fib(2) ← 繰り返し
│   │   └── fib(1)
│   └── fib(2) ← 繰り返し
└── fib(3) ← 繰り返し
    ├── fib(2) ← 繰り返し
    └── fib(1)

fib(2)が何度も計算されます。fib(30)では数百万回、fib(50)では数十億回の重複計算が発生します。

メモ化で解決

python
from functools import lru_cache
@lru_cache(maxsize=None)
def fib_fast(n):
if n <= 1:
return n
return fib_fast(n - 1) + fib_fast(n - 2)
print(fib_fast(50)) # 12586269025 — 瞬時!
print(fib_fast(100)) # 354224848179261915075

@lru_cacheは、すでに計算した結果を保存しておき、同じ引数で呼び出された場合、保存された値を返します。重複計算がなくなるため、O(2^n)がO(n)に減少します。


再帰 vs 繰り返し

同じ問題を繰り返し(ループ)でも解くことができます。

python
# 再帰
def factorial_rec(n):
if n <= 1:
return 1
return n * factorial_rec(n - 1)
# 繰り返し
def factorial_iter(n):
result = 1
for i in range(2, n + 1):
result *= i
return result
比較再帰繰り返し
可読性問題の構造が再帰的であれば直感的単純な繰り返しはより明確
パフォーマンスコールスタックのオーバーヘッドがある一般的に高速
メモリスタックの深さに応じて使用O(1)
適切な問題ツリー探索、分割統治、順列/組み合わせ単純な繰り返し、累積計算

ルール: 問題の構造が自然に再帰的である場合(ツリー、グラフ、分割統治)、再帰を使用し、単純な繰り返しである場合はforループを使用します。


実践例 — ディレクトリの探索

python
import os
def list_all_files(directory, indent=0):
"""再帰的にディレクトリツリー内のすべてのファイルをリスト表示します。"""
items = sorted(os.listdir(directory))
for item in items:
path = os.path.join(directory, item)
print(" " * indent + item)
if os.path.isdir(path):
list_all_files(path, indent + 1) # サブディレクトリ → 再帰
list_all_files("project")
# project
# app.js
# routes
# users.js
# posts.js
# public
# css
# style.css
# index.html

フォルダの中にフォルダがあり、その中にさらにフォルダがある構造 — これが典型的な再帰問題です。「何段階まであるかは分からないが、同じパターンが繰り返される」場合は再帰が自然です。


主要なまとめ

概念まとめ
再帰関数が自分自身を呼び出す
ベースケースこれ以上再帰しない終了条件(必須
再帰ケース問題をより小さくして自分自身を呼び出す
コールスタック呼び出しごとにメモリに状態を保存(Pythonの制限は1,000)
メモ化重複計算の防止(@lru_cache

再帰を初めて見たとき、「関数が自分自身を呼び出すと無限ループになるのではないか?」という疑問が生じます。重要なのは、毎回問題がより小さくなり、最終的に終了条件に到達するということです。このパターンを理解すれば、ツリー探索、ソート(マージソート)、組み合わせ/順列、グラフ探索(DFS)など、多くのアルゴリズムを自然に理解できます。


実践パターン — ネストされた辞書の探索

JSONや設定ファイルで、深さが不明なネストされた構造に遭遇した場合、再帰が自然です。

python
def flatten_dict(d, prefix=""):
result = {}
for key, value in d.items():
full_key = f"{prefix}.{key}" if prefix else key
if isinstance(value, dict):
result.update(flatten_dict(value, full_key))
else:
result[full_key] = value
return result
config = {
"database": {
"host": "localhost",
"port": 5432,
"credentials": {
"user": "admin",
"password": "secret"
}
},
"debug": True
}
print(flatten_dict(config))
# {
# 'database.host': 'localhost',
# 'database.port': 5432,
# 'database.credentials.user': 'admin',
# 'database.credentials.password': 'secret',
# 'debug': True
# }

このパターンは、ログシステム、設定管理、Elasticsearchインデックス作成などでよく使用されます。

💬 質問・コメント

0件のコメント

ログインせずに投稿できます。ゲスト投稿は投稿者自身で編集・削除できません。

0/2000

読み込み中...