edubun
日本語

再帰

64枚の円盤のハノイの塔を動かすには 18,446,744,073,709,551,615 回の移動が必要ですが、再帰関数ならその解き方を4行で書けます。

自分自身を呼び出す関数

中に自分の小さな写しをふくむ問題があります。5の階乗は 5! と書き、5 × 4 × 3 × 2 × 1 = 120 です。もう一度見てみましょう。これは 5 × (4 × 3 × 2 × 1)、つまり 5 × 4! です。一般に n! = n × (n − 1)! なので、階乗を計算するには、小さいほうの階乗を計算してかければよいのです。問題を小さくした版に対して自分自身を呼び出して解く関数を、再帰的な関数と言います。

function factorial(n) {
  if (n <= 1) return 1;          // 基底部: ここで止まる
  return n * factorial(n - 1);   // 再帰部: 小さくした問題
}
console.log(factorial(5)); // 120

再帰関数には必ず2つの部分が必要です。基底部(ベースケース)は、もう一度呼び出さなくても直接答えられるほど小さな入力です。ここでは「1の階乗は1」です。再帰部は問題を小さくして、もう一度関数を呼び出します。呼び出しのたびに基底部に近づかなければなりません。そうでないと、たとえばまちがえて factorial(n + 1) と書くと、呼び出しは終わりません。

呼び出しスタック

コンピューターは、終わっていない呼び出しをどうやって覚えておくのでしょうか。factorial(5) が実行されると 5 * factorial(4) にたどりつき、その答えを待たなければなりません。コンピューターは待っている呼び出しをスタックに積みます。スタックとは、最後に置いたものが最初に取り出される山のことです。factorial(4) は factorial(3) を待ち、これが factorial(1) まで続きます。これはすぐに1を返します。するとスタックがほどけていきます。factorial(2) が2を、factorial(3) が6を、factorial(4) が24を返し、factorial(5) が120を返します。

スタックの大きさには限りがあります。基底部にたどりつかない再帰は、場所がなくなるまで呼び出しを積み続け、JavaScriptはエラーで止まります。ChromeとNode.jsでは「RangeError: Maximum call stack size exceeded」というメッセージが出ます。同じ限界があるので、何十万段にもなるとても深い再帰は、ループで書くほうがよいのです。

ハノイの塔

ハノイの塔は、フランスの数学者エドゥアール・リュカが1883年に発表したパズルです。3本の棒があり、1本目の棒に大きさの違う円盤が、いちばん大きいものを下にして積まれています。目標は、山全体を3本目の棒へ移すことです。一度に動かせる円盤は1枚だけで、動かせるのは棒のいちばん上の円盤だけ、大きい円盤を小さい円盤の上に置いてはいけません。

円盤を1枚ずつどこへ動かすかを順に考えていくと、すぐに混乱します。再帰的に考えれば短くすみます。n枚の円盤をAからCへ動かすには、まず上のn − 1枚をAからBへどけます。次に最大の円盤をAからCへ動かします。そしてn − 1枚をBからCのその上へ動かします。n − 1枚を動かすのは1つ小さい同じパズルで、1枚を動かすのが基底部です。3枚なら7回の移動になります。ラボでは、移動のたびに待っている呼び出しのスタックが表示されます。

function hanoi(n, from, to, via) {
  if (n === 0) return;
  hanoi(n - 1, from, via, to);
  console.log("disk " + n + ": " + from + " -> " + to);
  hanoi(n - 1, via, to, from);
}
hanoi(3, "A", "C", "B");  // 7回の移動
読み込み中

ドラッグで回転、スクロールまたはピンチで拡大・縮小。

円盤3枚、移動7回。右の呼び出しスタックには、この移動が終わるのを待っている hanoi の呼び出しが並びます。円盤4枚と5枚も試しましょう。

n枚の円盤には何回の移動が必要でしょうか。n枚を動かすには、n − 1枚の移動2回に1回を足した手間がかかります。M(n) = 2M(n − 1) + 1、M(1) = 1 です。これで1、3、7、15、31となるので M(n) = 2ⁿ − 1 で、これより少ない手数の解き方はありません。10枚なら1,023回です。64枚なら 2⁶⁴ − 1 = 18,446,744,073,709,551,615 回で、1秒に1回動かしても約5,850億(585 billion)年かかります。これが前のユニットの O(2ⁿ) の増え方です。

再帰、ループ、くり返される仕事

再帰で書けるものは、ループと自分で用意したスタックでも書けます。階乗のような簡単な例は、ループのほうがわかりやすいこともよくあります。再帰がかがやくのは、問題そのものが木の形をしているときです。マージソートでリストを分けるとき、フォルダーの中のフォルダーを調べるとき、そして最後のユニットのテーマである迷路の探索などです。1つの落とし穴は、くり返される仕事です。フィボナッチ数 0, 1, 1, 2, 3, 5, 8, … は fib(n) = fib(n − 1) + fib(n − 2) にしたがいます。これをそのまま再帰で書くと、fib(10) = 55 は177回、fib(20) は21,891回、fib(30) は2,692,537回の呼び出しをします。同じ小さい値が何度も計算されるからです。一度見つけた答えを覚えておくか、ループで下から積み上げれば、このむだがなくなります。

再帰問題を小さくした版に対して自分自身を呼び出して解く関数
基底部(ベースケース)直接答えられ、呼び出しを止める入力
再帰部問題を小さくして、もう一度呼び出す部分
呼び出しスタック答えを待っている呼び出しの山。後入れ先出し
スタックオーバーフロー待っている呼び出しが多すぎること。JavaScriptでは RangeError
階乗 n!n × (n − 1) × … × 1。5! = 120
ハノイの塔リュカ、1883年。n枚の円盤で 2ⁿ − 1 回の移動
  1. 6!(6の階乗)はいくつですか?
  2. 円盤が4枚のハノイの塔では、移動は何回必要ですか?
  3. 円盤が10枚なら、移動は何回ですか?
  4. 基底部は何をしますか?
    • 小さな入力に直接答え、呼び出しの連鎖を止める
    • 関数を速く実行させる
    • もっと大きな入力で関数を呼び出す
    • 結果を表示する
  5. 再帰関数が基底部にたどりつかないと、どうなりますか?
    • 0を返す
    • 呼び出しスタックがいっぱいになり、プログラムがエラーで止まる
    • エラーを出さずに静かに永遠にくり返す
    • JavaScriptが基底部を選んでくれる
  6. スタックは、どの順番でものを取り出しますか?
    • 後入れ先出し
    • 先入れ先出し
    • 小さいものから
    • ランダム
  7. factorial(5) が factorial(1) を計算しているとき、スタックにはそれもふくめて factorial の呼び出しがいくつありますか?
  8. n枚の円盤をAからCへ動かすとき、再帰的な作戦の最初の手順は何ですか?
    • 上のn − 1枚をAからBへ動かす
    • 最大の円盤をCへ動かす
    • 最小の円盤をCへ動かす
    • すべての円盤をBへ動かす
  9. 1883年にハノイの塔のパズルを発表したのはだれですか?
    • レオンハルト・オイラー
    • エドゥアール・リュカ
    • トニー・ホーア
    • ユークリッド
  10. 素朴な再帰の fib は、fib(10) のために177回呼び出しをします。fib(10) はいくつですか?
  11. 素朴な再帰の fib が遅いのはなぜですか?
    • 同じ小さい値を何度も計算するから
    • 再帰はいつも遅いから
    • 足し算に手間がかかるから
    • 基底部がないから
ポイント
  • 再帰関数は、問題を小さくした版に対して自分自身を呼び出す。
  • 呼び出しを止める基底部が必要。
  • 待っている呼び出しは呼び出しスタックに積まれる。後入れ先出し。
  • ハノイの塔:2ⁿ − 1 回の移動。64枚なら、1秒に1回動かしても約5,850億(585 billion)年かかる。
  • 素朴な再帰のフィボナッチのような、くり返される仕事に気をつける。
このコースの出典 (11)
  • MIT OpenCourseWare 6.006 Introduction to Algorithms, Spring 2020 (Demaine, Ku, Solomon)
  • lecture notes 1 Introduction, 3 Sorting, 9 Breadth-First Search, 10 Depth-First Search (ocw.mit.edu/courses/6-006-introduction-to-algorithms-spring-2020/pages/lecture-notes) and Fall 2011 lecture 13 BFS notes (ocw.mit.edu/courses/6-006-introduction-to-algorithms-fall-2011)
  • Cormen, Leiserson, Rivest, Stein, Introduction to Algorithms (MIT Press)
  • with the CLRS-based Khan Academy algorithms unit by Thomas Cormen and Devin Balkcom of Dartmouth (khanacademy.org/computing/computer-science/algorithms: binary search, selection sort analysis, merge sort, quicksort overview and analysis)
  • MacTutor History of Mathematics, Al-Khwarizmi (mathshistory.st-andrews.ac.uk/Biographies/Al-Khwarizmi: word algorithm from the Latin title Algoritmi de numero Indorum)
  • D. E. Joyce, Euclid's Elements Book VII Propositions 1 and 2 (mathcs.clarku.edu/~djoyce/elements/bookVII)
  • Wolfram MathWorld, Merge Sort (mathworld.wolfram.com/MergeSort.html: proposed by John von Neumann in 1945, after Knuth 1998 p. 159)
  • Stanford CS "Tony Hoare: Quicksort" (cs.stanford.edu/people/eroberts/courses/soco/projects/2008-09/tony-hoare/quicksort.html: devised while a student at Moscow State University for Russian-English machine translation with a tape dictionary, programmed in Algol 60 and published 1961; the year 1959 from the Wikipedia Quicksort article and its cited sources)
  • Bodleian Libraries, Ada Lovelace (blogs.bodleian.ox.ac.uk/adalovelace: 1843 paper, Note G Bernoulli numbers)
  • Fourmilab text of Menabrea's Sketch with Lovelace's notes (fourmilab.ch/babbage/sketch.html)
  • Seven Bridges of Konigsberg (Euler 1736) and Tower of Hanoi (Edouard Lucas, 1883) checked against the Wikipedia articles and their cited sources.

確認日: 2026-10-17