再帰法

このページでは、自分自身を呼び出す再帰関数の考え方と、階乗・最大公約数の再帰的な定義、再帰を使って2分探索木を探す方法がわかります。

  • 一言で言うと:
    • 関数の中で自分自身を呼び出す「再帰」の話。階乗や最大公約数のような計算と、木の探索で使う例が出てくる。午前の頻出計算問題があるテーマ。

再帰関数の定義(階乗、最大公約数)→ 木の探索で使う(探索関数)

覚え方の軸

  • 再帰関数は「止まる条件」と「1つ小さい自分を呼ぶ式」の2つでできている:
    • 階乗なら、n=0で1を返す(止まる)、それ以外はn×f(n−1)(自分を呼ぶ)
  • 再帰は「下りて行って、帰りに計算する」:
    • f(3) → f(2) → f(1) → f(0)と呼び出し、f(0)=1が返ってきてから、1 → 1 → 2 → 6と掛け算していく
  • 計算問題は「式に数字を入れて1行ずつ書き下す」だけ:
    • F(252, 105)のような問題は、止まる条件(y=0)になるまで書き換える
  • 木は再帰的な構造なので、再帰と相性がよい:
    • 左(右)部分木を探すには、左(右)の子へのポインタを引数にして自分を呼ぶ

再帰関数:自分で自分を呼ぶ関数

  • 再帰関数:
    • 関数の定義の中で、自分自身を使って定義する関数
  • 階乗関数(n!):
    • 再帰関数の代表
    • n!=n×(n−1)×(n−2)×…×2×1
    • 定義1(プログラムの形):
      • f(n):if n=0 then return 1 else return n×f(n−1)
      • 受け取ったnが0なら1を返し、それ以外ならn×(n−1)!を返す
    • 定義2(数式の形):
      • n>0のとき f(n)=n×f(n−1)、n=0のとき f(n)=1
  • 漸化式:
    • 定義2のような式。f(1)、f(2)、…、f(n)、…という関数の列で、f(1)〜f(n)のいくつかを使ってf(n+1)を求める法則を表す式
  • 3!を求める流れ
    • ① f(3):
      • nが0でないので、f(2)を呼び出す
    • ② f(2):
      • nが0でないので、f(1)を呼び出す
    • ③ f(1):
      • nが0でないので、f(0)を呼び出す
    • ④ f(0):
      • nが0だから1を返す
    • ⑤ f(1):
      • 1×1=1を返す
    • ⑥ f(2):
      • 2×1=2を返す
    • ⑦ f(3):
      • 3×2=6を返す
    • ポイント:
      • 呼び出しの途中の状態はスタックに積まれ、後から呼んだものから先に終わる(LIFO)
  • 例題:
    • f(4)=4×f(3)=4×6=24
  • ★頻出パターン(最大公約数を求める再帰関数)
    • 定義:
      • F(x, y):y=0のときx、y>0のとき F(y, x mod y)
      • mod は割り算の余り
    • F(252, 105)の値は?
      • =F(105, 252 mod 105)=F(105, 42)(252÷105=2余り42)
      • =F(42, 105 mod 42)=F(42, 21)(105÷42=2余り21)
      • =F(21, 42 mod 21)=F(21, 0)(42÷21=2余り0)
      • =21(y=0なのでxを返す)
    • これはユークリッドの互除法で、答えは252と105の最大公約数(252=2²×3²×7、105=3×5×7なので3×7=21。検算OK)
  • 例題:
    • F(48, 18)=F(18, 12)=F(12, 6)=F(6, 0)=6(48と18の最大公約数は6)

再帰関数の実例:2分探索木を再帰で探す

  • 再帰的な構造:
    • 自分自身の一部が、自分自身と同じ形をしている構造。木構造がその代表。だから木を使った探索には再帰処理が便利
  • ノードのデータ構造:
    • value:
      • ノードがもつデータ
    • left:
      • 左の子のノードを指すポインタ
    • right:
      • 右の子のノードを指すポインタ
    • 子がない場合は nil
  • ポインタの書き方:
    • 変数pがあるノードへのポインタのとき、そのノードのメンバは p->value、p->left、p->right と書く
    • p->leftの値をpに代入すると、pは左の子へのポインタになる
  • 探索関数search(データxが2分探索木の中にあるかを判定する)
function search( x, p )
    // x:探したいデータ、p:今見ているノードへのポインタ
    if ( p が nil )
        return FALSE                   // 行き止まり=見つからない
    endif
    if ( x = p->value )
        return TRUE                    // 見つかった
    elseif ( x < p->value )
        return search( x, p->left )    // 小さいので左部分木で自分を呼ぶ
    else
        return search( x, p->right )   // 大きいので右部分木で自分を呼ぶ
    endif
endfunction
  • 流れ:
    • pがnil(もう進む先がない)→ FALSE
    • xとp->valueが等しい → TRUE
    • xの方が小さい → 左部分木を探す、大きい → 右部分木を探す
  • ★試験では、再帰呼出しのときの引数が問われる
    • 左(右)部分木を探すには、左(右)部分木の根のノードへのポインタ(p->left、p->right)を引数にして、自分自身を呼び出せばよい

混同しやすいペア

  • 止まる条件(n=0なら1、y=0ならx)/自分を呼ぶ式(n×f(n−1)、F(y, x mod y))
  • 呼び出す順(f(3)→f(2)→f(1)→f(0))/計算が終わる順(f(0)→f(1)→f(2)→f(3)、スタックなので逆順)
  • F(x, y)の引数の入れ替わり:
    • 次の呼び出しでは、yが前に、x mod yが後ろに来る
  • p->left(左の子へのポインタ、小さいとき)/p->right(右の子へのポインタ、大きいとき)
  • FALSE(pがnil=見つからない)/TRUE(p->valueと等しい=見つかった)

← 02 アルゴリズムとプログラミングの目次に戻る

タイトルとURLをコピーしました