大きな画面での利用をおすすめします — アニメーションと操作がより快適になります。
入力

再帰とコールスタック

再帰関数が自分を呼び出すとき、コールスタックが未完了の仕事を保存します。呼び出しごとにフレームを作り、最新のフレームを実行し、戻り値を下で待つ呼び出し元へ渡します。待機中のフレームもメモリを使うため、計算が難しくなる前に深さの上限へ達することがあります。

フレームを付箋に置き換えて考えます。最初の付箋の fact(4) は、先に fact(3) を必要とするため、新しい付箋を上に置きます。処理するのは一番上だけで、下の付箋は未完了の仕事を保って待ちます。画面では付箋一枚がフレーム一つに対応し、待機ラベルはそのフレームに仕事が残っていることを示します。

再帰にコールスタックが必要な理由

再帰関数には基底ケースと再帰ケースがあります。基底ケースは再帰せずに戻ります。再帰ケースは小さい入力で呼び出し、戻り値を使って残りの仕事を終えます。

階乗の基底ケースは n <= 1 で、1 を返します。それ以外では fact(n) が fact(n - 1) を呼び、戻り値を child に保存して n * child を返します。

fact(4) が fact(3) を呼んでも、元のフレームは n = 4 と未確定の child = ? を保ちます。fact(3) が 6 を返すと、その値が child に入り、fact(4) は 4 * 6 を計算できます。

制御は後入れ先出しです。最後に積んだ呼び出しから戻り、待機中の各フレームはスタック領域を使います。

ステップごとの解説

既定では fact(4) を計算します。

  1. answer = fact(n) から始まり、n = 4、スタックは空です。
  2. fact(4) を呼び、局所状態 n = 4 のフレームを積みます。
  3. 4 <= 1 は偽なので、基底ケースからは戻れません。
  4. fact(4) は child = fact(3) で止まり、child の値を待ちます。
  5. fact(3) を上に積んで実行し、fact(4) は下で待ちます。
  6. fact(3) は 3 <= 1 が偽なので、child = fact(2) で待ちます。
  7. fact(2) を積みます。2 <= 1 は偽で、child = fact(1) で待ちます。
  8. fact(1) を積みます。1 <= 1 は真なので、基底ケースが 1 を返します。
  9. 戻り値を fact(2) に渡し、child = 1 にします。
  10. fact(2) が 2 * 1 = 2 を返します。
  11. fact(3) は child = 2 を受け取り、3 * 2 = 6 を返します。
  12. fact(4) は child = 6 を受け取り、4 * 6 = 24 を返します。
  13. 最上位の呼び出しが answer = 24 を設定し、スタックは再び空になります。

方向の変化を見てください。呼び出しはフレームを上へ積み、戻り値は下の呼び出し元へ渡ります。fact(2) が先に終わらなければ、fact(4) は終われません。

次にコードを missingBase に切り替えます。基底ケースを取り除くと、先頭のフレームは次のフレームを要求し続け、戻りに到達しません。

時間計算量と空間計算量

方式時間計算量スタック領域挙動
基底ケースがある fact(n)O(n)O(n)n から 1 まで一つずつフレームを作り、順に戻る
fact_without_base(n)自力では終了しない再帰上限まで増える各フレームが次を呼び、戻りに到達しない

本レッスンの単位コストモデルでは、各フレームの仕事は条件確認、必要な再帰呼び出し、戻る際の乗算という定数回の処理です。n フレーム(n >= 1)なので、時間とスタック領域は O(n) です。大きい n には、ループへの書き換え、明示的なスタックの利用、影響を確認したうえでの再帰上限の変更を検討します。

基底ケースのない版は階乗を実装していません。停止規則がなく、ランタイムの再帰上限で止まるだけです。Python の既定の上限は約 1000 フレームで、六つではありません。画面では失敗を追えるよう、小さい上限を使っています。

境界ケース

  • n = 1 は直ちに基底ケースに入り、一つ積んだフレームが 1 を返して取り除かれます。
  • 通常入力の上限 n = 6 は、六つのフレームを画面に収めるためです。階乗の定義の上限ではありません。
  • 基底ケースがなければ、負の値でも止まりません。fact_without_base(-2) は fact_without_base(-3) を呼び、次は -4 と続きます。
  • 戻り値の行き先は一つです。呼び出し元の待機中の child、または最上位の呼び出しに対応する大域の answer に入ります。

よくある間違い

  • プログラム全体をコピーすると思う。 同じ関数に新しいフレームを作り、局所変数を別々に持ちます。
  • 呼び出し元が待つことを忘れる。 fact(3) が戻るまで、fact(4) は乗算を終えられません。
  • 基底ケースを任意と考える。 停止条件がなければ、ランタイムが止めるまでスタックが増えます。
  • 呼び出し順と戻り順を混同する。 呼び出しの引数は 4, 3, 2, 1、戻り値は 1, 2, 6, 24 の順です。
  • child を一つの共有変数として考える。 fact(2).child と fact(4).child は別フレームの領域です。

このレッスンでの簡略化

フレームには関数名、引数、局所変数 child、戻り値だけを表示します。実際の Python フレームは、バイトコード位置、大域変数やクロージャーへの参照、例外状態、インタープリターの管理情報も持ちます。ここでは、呼び出しごとにフレームがあり、現在はスタックの先頭だけが実行中だという点を追います。