情報学入門(データ演習) / 第 14 章

再帰定義

自分自身を呼ぶ ── ハノイの塔と迷路

この章のねらい

1. 再帰定義とは

関数や手続きを定義する際に、プログラムの本体で自分自身の呼出しを含む場合、 再帰定義(recursive definition)という。
また、プログラムの本体で自分自身を呼出すことを再帰呼出し(recursive call)という。
複雑な繰り返しをする場合、簡単に定義することができる。

── 元資料 3 枚目

すでに再帰的な定義には出会っています。第 10 章のフィボナッチ数列です。

fib(1) = 1  fib(2) = 1
fib(n) = fib(n−1) + fib(n−2) (n ≧ 3)

第 10 章では、これを繰り返しに書き直して計算しました。 この章では定義のまま書きます

再帰には、必ず 2 つのものが要ります。

どちらかを忘れると永久に呼び続けます。 第 8 章 §5.3 で「毎回かならず小さくなる量を探す」と言ったのは、 まさにこの確認のことです。
※ この教材の処理系は 400 段で打ち切ります。本物の Excel では 「スタック領域が不足しています」というエラーになります。

Sub Macro1()
    Cells(1, 1) = "これから呼びます"
    Call owaranai(5)
    Cells(2, 1) = "ここには来ません"
End Sub

Sub owaranai(ByVal n As Integer)
    ' 終了条件がない!
    Call owaranai(n - 1)
End Sub

2. 呼び出す前に表示するか、あとに表示するか

元資料 4・5 枚目の対比です。1 行の位置を変えるだけで、出力の順序が逆になります。

Dim gyou As Integer      ' どちらの Sub からも見える(第 10 章 §4)
Dim gyou2 As Integer

Sub Macro2()
    Cells(1, 1) = "呼ぶ前に表示"
    Cells(1, 3) = "呼んだ後に表示"

    gyou = 2
    Call mae(5)

    gyou2 = 2
    Call ato(5)
End Sub

' 自分を呼ぶ「前」に書く
Sub mae(ByVal n As Integer)
    If n = 0 Then Exit Sub
    Cells(gyou, 1) = n          ' ← ここ
    gyou = gyou + 1
    Call mae(n - 1)
End Sub

' 自分を呼ぶ「後」に書く
Sub ato(ByVal n As Integer)
    If n = 0 Then Exit Sub
    Call ato(n - 1)
    Cells(gyou2, 3) = n         ' ← ここ
    gyou2 = gyou2 + 1
End Sub

左は 5, 4, 3, 2, 1、右は 1, 2, 3, 4, 5 になります。 なぜでしょうか。呼び出しは「積み上がる」のです。

ato(1) ← いまここ。n=1 を表示できる
ato(2) ← ato(1) の帰りを待っている
ato(3) ← 待っている
ato(4) ← 待っている
ato(5) ← いちばん最初に呼ばれた

ato先に自分を呼ぶので、 n=5 → 4 → 3 → 2 → 1 → 0 まで一気に潜っていきます。 そして 0 で Exit Sub して戻ってくるとき、 いちばん深いところ(n=1)から順に表示されます。 だから 1, 2, 3, 4, 5 の順になるのです。

この「積み上がって、あとから戻る」構造をスタックといいます。 再帰は、スタックを自動で使う繰り返しだと言えます ── だから深くなりすぎると「スタック領域が不足」になるのです。

3. 1〜N の和(元資料 6・7 枚目)

第 5 章では GoTo で、第 7 章では For で書きました。 再帰ならこう定義できます ── 「1〜N の和は、N +(1〜N−1 の和)」。

Sub Macro3()
    Dim n As Integer

    For n = 1 To 8
        Cells(n, 1) = n
        Cells(n, 2) = wa(n)
    Next n

    Cells(10, 1) = "wa(100)"
    Cells(10, 2) = wa(100)
End Sub

Function wa(ByVal n As Integer) As Long
    If n = 1 Then          ' 終了条件
        wa = 1
    Else
        wa = n + wa(n - 1) ' 自分を呼ぶ(引数は必ず小さくなる)
    End If
End Function

定義がそのままプログラムになっています。 For 版と比べてみてください ── 繰り返し版は「どう計算するか」の手順を書きますが、 再帰版は「何であるか」を書いています。 問題の定義が再帰的なら、再帰で書くほうが素直なのです。

4. 再帰が絶望的に遅くなる場合

元資料は「複雑な繰り返しを簡単に定義できる」と書いていますが、 簡単に書けることと、速いことは別です。 フィボナッチを定義のまま書くと、とんでもなく遅くなります。 これは触れておくべき重要な点なので、節を立てます。

Dim yobi As Long              ' 呼び出された回数を数える

Sub Macro4()
    Dim n As Integer

    Cells(1, 1) = "n": Cells(1, 2) = "fib(n)": Cells(1, 3) = "呼び出し回数"

    For n = 1 To 24
        yobi = 0
        Cells(n + 1, 1) = n
        Cells(n + 1, 2) = fib(n)
        Cells(n + 1, 3) = yobi
    Next n
End Sub

Function fib(ByVal n As Integer) As Long
    yobi = yobi + 1
    If n <= 2 Then
        fib = 1
    Else
        fib = fib(n - 1) + fib(n - 2)
    End If
End Function
nfib(n)再帰版の呼び出し回数繰り返し版(第 10 章)
5591
10551091
206,76513,5291
2446,36892,7351
40102,334,155約 2 億回1

なぜこんなに増えるのか。 fib(24) を求めるのに fib(23) と fib(22) を呼びますが、 fib(23) の中でも fib(22) を呼びます ── 同じものを何度も計算しなおしているのです。 fib(20) は fib(24) の計算中に 5 回も呼ばれます。 呼び出し回数は 2 × fib(n) − 1 で、 fib 自体が指数的に増えるので、呼び出し回数も指数的に増えます。

だから、再帰で書くべきかどうかは考えて決めます。 ハノイの塔(§5)や迷路(§6)は再帰でしか素直に書けませんが、 フィボナッチや階乗は繰り返しのほうが速いのです。 判断の目安は「同じ計算を何度もしていないか」です (答えを覚えておく「メモ化」という工夫で再帰版も速くできますが、それは先の話です)。

5. ハノイの塔(元資料 15 枚目〜)

① にある円盤(N 個)を、③ に移すことを考える。
・一度に、一つの円盤しか動かすことができない
小さい円盤の上に、大きな円盤を乗せることはできない

手続きの意味(元資料 18 枚目)
hanoi(n, a, b, c) ── N 個の円盤を変数 a の値にある棒から、変数 b の値の棒へ移す。 変数 c の値の棒は補助となる棒である。

解き方の発想。 n 枚をいきなり動かす方法は思いつきませんが、 n−1 枚を動かす方法は分かっている」と仮定すれば、3 段で書けます。

  1. 上の n−1 枚を、補助の棒 c へ移す(いちばん下の 1 枚が露出する)
  2. いちばん大きい円盤 na から b 1 回動かす
  3. c に置いた n−1 枚を、b移す

①と③は「n−1 枚を動かす」という同じ問題の小さい版です。 だから自分を呼べばよい ── これが再帰の考え方の核心です。 「全部の手順を考える」のではなく「1 段だけ考えて、残りは自分に任せる」。

Dim cnt As Integer

Sub Macro5()
    Dim n As Integer

    n = Cells(1, 5).Value
    cnt = 0

    Cells(1, 1) = "手": Cells(1, 2) = "円盤": Cells(1, 3) = "動かす"
    Call hanoi(n, 1, 3, 2)        ' ① から ③ へ、② を補助に

    Cells(1, 6) = "手数"
    Cells(2, 6) = cnt
    Cells(3, 6) = "2^n - 1 ="
    Cells(4, 6) = 2 ^ n - 1
End Sub

Sub hanoi(ByVal n As Integer, ByVal a As Integer, ByVal b As Integer, ByVal c As Integer)
    If n = 0 Then Exit Sub            ' 終了条件

    Call hanoi(n - 1, a, c, b)        ' ① 上の n-1 枚を補助の c へ
    cnt = cnt + 1
    If cnt <= 15 Then                 ' 表は 15 手までにしておく
        Cells(cnt + 1, 1) = cnt
        Cells(cnt + 1, 2) = n
        Cells(cnt + 1, 3) = a & " ⇒ " & b
    End If
    Call hanoi(n - 1, c, b, a)        ' ③ c の n-1 枚を b へ
End Sub

3 枚のとき 7 手、4 枚で 15 手、n 枚で 2n − 1 手です。 手数は1 枚増えるごとに倍以上になります(第 8 章の倍々貯金と同じ増え方)。 伝説では 64 枚だそうですが、264−1 ≒ 1844 京手。 1 秒に 1 枚動かしても5800 億年かかります。

5.1 呼び出しの木(元資料 20 枚目)

3 枚のときに hanoi がどう呼ばれるかを書き出したものです。 元資料 20 枚目の図と、手順の番号(①〜⑤)まで一致します

main └ hanoi(3, 1, 3, 2) 3 枚を ①→③(補助 ②) ├ hanoi(2, 1, 2, 3) 2 枚を ①→②(補助 ③) │ ├ hanoi(1, 1, 3, 2) 1 枚を ①→③ │ │ ├ hanoi(0, 1, 2, 3) 何もしない │ │ ├ ① 円盤1 を 1 ⇒ 3 │ │ └ hanoi(0, 2, 3, 1) 何もしない │ ├ ② 円盤2 を 1 ⇒ 2 │ └ hanoi(1, 3, 2, 1) 1 枚を ③→② │ ├ hanoi(0, 3, 1, 2) │ ├ ③ 円盤1 を 3 ⇒ 2 │ └ hanoi(0, 1, 2, 3) ├ ④ 円盤3 を 1 ⇒ 3 └ hanoi(2, 2, 3, 1) 2 枚を ②→③(補助 ①) ├ hanoi(1, 2, 1, 3) │ ├ ⑤ 円盤1 を 2 ⇒ 1⑥ 円盤2 を 2 ⇒ 3 └ hanoi(1, 1, 3, 2) └ ⑦ 円盤1 を 1 ⇒ 3

木の葉に近いところから順に手が確定していくことに注目してください。 いちばん大きい円盤(④)は、ちょうど真ん中で 1 回だけ動きます。 そして前半と後半は同じ形の小さい問題になっています ── これが再帰の構造がそのまま見える形です。

6. 迷路(元資料 8〜14 枚目)

迷路をスタート地点からゴール地点に最短で移動する経路を列挙するプログラムを考える。
■ は壁であり、進むことができない。最短で移動するために、右と下にのみ移動できる。

「右と下だけ」という条件が効いています。 右か下にしか進めないので、同じ場所に戻ってくることがありません。 だから「行き止まりで引き返す」処理を書かなくても、素直な再帰で全部の経路を数えられます。 (左や上にも動けるなら「来た道を覚えておく」必要が出て、ぐっと難しくなります。)

動かす⑥ 迷路の経路を数える マスをクリックして壁を置ける

左上(S)から右下(G)へ、右と下だけで進みます。 マスをクリックすると壁(黒)になります。

Dim kazu As Integer
Dim W As Integer
Dim H As Integer

Sub Macro6()
    W = 6: H = 6            ' 迷路の大きさ
    kazu = 0

    Call susumu(1, 1)       ' 左上から出発

    Cells(8, 1) = "経路の数"
    Cells(8, 2) = kazu
    Cells(9, 1) = "壁がなければ"
    Cells(9, 2) = 252       ' 10C5 = 252 通り
End Sub

Sub susumu(ByVal r As Integer, ByVal c As Integer)
    ' 外へ出たら行き止まり
    If r > H Or c > W Then Exit Sub
    ' 壁なら行き止まり
    If Cells(r, c).Value = "■" Then Exit Sub
    ' ゴールに着いたら 1 通り数える
    If r = H And c = W Then
        kazu = kazu + 1
        Exit Sub
    End If

    Call susumu(r, c + 1)   ' 右へ進む場合
    Call susumu(r + 1, c)   ' 下へ進む場合
End Sub

この手続きは 3 つの終了条件を持っています。 「外へ出た」「壁だった」「ゴールに着いた」── 再帰では、終了条件が複数あることがふつうです。 そして本体は 右へ下へ の 2 行だけ。 「その場で 2 通りに分かれる」ことだけ書けば、 残りは自分自身が処理してくれるのです。

壁がない 6×6 なら 252 通りです (右 5 回と下 5 回の並べ方=10 個から 5 個選ぶ組み合わせ = 252)。 上の配置(壁 4 つ)では 30 通りに減ります。 壁の位置を変えて、数がどう変わるか試してみてください ── 真ん中の壁ほどよく効きます

7. 再帰を書くときの型

再帰は「慣れ」で書くものではなく、決まった型があります。

  1. 終了条件を先に書く。「これ以上分けられない場合」を最初に片づける
  2. 1 段だけ考える。「残りは自分自身が正しくやってくれる」と仮定してよい
  3. 引数が必ず終了条件に近づくことを確かめるn−1、右か下へ、範囲が狭まる…)
  4. 同じ計算を繰り返していないか点検する。していたら繰り返しで書くか、答えを覚える
再帰が向くか、繰り返しが向くか
問題向いているのは理由
1〜N の和、階乗繰り返し1 本道なので再帰の利点がない
フィボナッチ繰り返し再帰だと同じ計算を大量に繰り返す
ハノイの塔再帰問題が 2 つの小さい同じ問題に分かれる
迷路の経路の列挙再帰分岐が枝分かれし、深さが決まらない
フォルダの中を全部たどる再帰入れ子の深さが分からない(第 1 章のファイルシステム)
図形の中に同じ図形(第 15 章)再帰定義そのものが再帰的

8. まとめ

  1. 再帰定義=本体で自分自身を呼ぶ定義。再帰呼出し=実際に呼ぶこと
  2. 必ず終了条件小さくなることの 2 つを備える
  3. 呼び出しは積み上がる(スタック)。だから「呼ぶ前/呼んだ後」で順序が逆になる
  4. 簡単に書けることと速いことは別。フィボナッチの再帰版は fib(24) で 9 万回以上呼ぶ
  5. ハノイの塔は 2n − 1 手。「1 段だけ考えて、残りは自分に任せる」
  6. 迷路は終了条件が 3 つ、本体は「右へ」「下へ」の 2 行だけ
  7. 向くのは問題が同じ形の小さい問題に分かれるとき

章末問題

付録:元資料からの修正一覧

もとにしたのは「情報学入門(VBA)185-2025」全 23 枚 (および同内容の 180-2019・23 枚)です。

元資料この教材種別
Sub hanoi(n As Intger, a As Integer, b As Integer, c As Integer) Integer の綴り誤り。VBA では 「ユーザー定義型は定義されていません」というコンパイルエラーになり実行できない誤り
「複雑な繰り返しをする場合、簡単に定義することができる」とあるが、 再帰が遅くなる場合に触れていない §4 として追加。fib(24) で 92,735 回呼ばれることを実際に数えて示し、 再帰が向く問題と向かない問題を表にした不足
終了条件の必要性が明示されていない 「終了条件」と「必ず小さくなること」の 2 つを要件として立て、 忘れたときに何が起きるかを実行して見せた不足
「呼び出す前に表示/呼び出した後に表示」の 2 例があるが、 なぜ順序が逆になるかの説明がない スタックの図を添えて、 深く潜ってから戻るときに表示されることを説明した不足
迷路で「右と下にのみ移動できる」という条件の意味が書かれていない この条件のおかげで同じ場所に戻らないので、 引き返す処理が不要になることを明示。壁なしなら 252 通り(10 個から 5 個選ぶ組み合わせ)も追記不足
ハノイの塔の手数に触れていない 2n − 1 手。64 枚なら 1 秒 1 手で 5800 億年不足
資料 180(23 枚)と 185(23 枚)がほぼ同内容で 2 本ある 185(2025 年版)を採り、1 つの章にまとめた重複

元資料で正しかったこと。 再帰定義・再帰呼出しの定義文、 hanoi(n, a, b, c) の意味づけ(a から b へ移す、c は補助)、 20 枚目の呼び出しの木と手順の番号(①1を1⇒3、②2を1⇒2、③1を3⇒2、④3を1⇒3、⑤1を2⇒1)は この教材のプログラムを実行した結果と完全に一致しました。 ハノイの塔の 3 つの規則、迷路の課題設定(右と下のみ)も適切です。 とくに「呼び出す前/呼び出した後」を並べて見せる構成は、 再帰を理解するうえで的確な導入です。