関数を再帰的に定義すると, 複雑な繰り返しの処理を 簡潔に記述できることがよくある.
関数の再帰的定義では, その関数を作るために, その関数自身を利用する:
// 関数の再帰的定義の基本パターン 型 関数A(仮引数 ...) { ... if (終了条件) return (...); // 繰り返しの終了 ... 関数A(実引数 ...); // 再帰呼び出し // ここで定義している関数A自身の先頭に戻る ... return (...); }
再帰呼び出しによって, その関数自身の先頭へ戻るようなループを形成している. メイン関数などからこの関数を呼び出すと, この関数内の処理が繰り返し実行されることになる.
なお,再帰では無限ループは,現実的には不可能であり, 終了条件 によって, 繰り返しを終了(return)する必要がある. または,継続条件によって, 再帰呼び出しするのでもよい.
自然数の 1 から n までの総和 Sn を例として, 反復的な計算を再帰的に定義し直してみよう.
プログラミングの前に,まずは数式として定義しておこう:
Sn = Σi=1n i = 1 + 2 + 3 + ... + n
Sn = ((...(((0 + 1) + 2) + 3) + ...) + n)
n = 0 に対して Sn = 0 (または,より簡潔に書くと... S0 = 0) n > 0 に対して Sn = Sn − 1 + n
次にC言語の関数として定義してみよう. ソース sum.c:
#include <stdio.h> // 自然数 1 から n までの総和を計算する関数の反復版 int sum1(int n) { int i; int s = 0; // S_0 = 0 for (i = 1; i <= n; i++) { // i = 1, 2, 3, ..., n s += i; // S_i = ((...) + i) } return (s); // S_n } // 自然数 1 から n までの総和を計算する関数の再帰版 int sum2(int n) { if (n == 0) return (0); // S_0 = 0 return (sum2(n - 1) + n); // S_n = S_n-1 + n } int main(void) { int n; printf("自然数 n > "); scanf("%d", &n); printf("反復版... 総和 = %d\n", sum1(n)); printf("再帰版... 総和 = %d\n", sum2(n)); return (0); }
総和関数 sum( ) として, 反復版 sum1( ) と再帰版 sum2( ) の2種が定義されているが, 実行結果は当然,どちらも同じとなる.
しかし,ソースコードはかなり異なり. 再帰版では,同じ計算のコードを実にコンパクトに表現できている. 反復版では,カウンタや結果記憶のために必要な変数が コードサイズ増加の原因となっている.
「反復」と「再帰」は,結果としては同じように見えるが, 方法としては異なる.混同しないこと!! 問題に応じて相応しい方法を採用しよう.
なお,反復では無限回の繰り返しも可能であったが, 再帰では有限回までしか実行できない. したがって,再帰では特に終了条件(return の条件)が重要となるが, 書き忘れやすいので注意しよう. または,継続条件(再帰呼び出しの条件)でもよい.
なぜ無限には再帰できないのか? 再帰呼び出しは,実際には,関数自身を直接呼び出してはいない. プログラムの内部的には,関数の分身(コピー)を作り, その分身の方を呼び出している. このとき,関数内のデータ(変数,引数,戻り値)をコピーするため メモリ領域を消費する. 利用可能なメモリ領域の限界以上に再帰しようとすると, スタックオーバフロー(stack overflow)となり, 実行中のプログラムは強制終了されてしまう.
教科書 pp.82-84 には, 自然数 n の階乗 n! = 1✕2✕3✕ ... ✕ n を例として, 再帰版と反復版のソースコードが掲載されている. これを参考に,sum.c を元にして, 階乗計算プログラム fact.c を作成してみよう.
階乗の数学的な定義式:
n! = 1 × 2 × 3 × ... × n
n! = (...(((1 × 1) × 2) × 3) × ... )× n
n = 0 に対して n! = 1
n > 0 に対して n! = (n − 1)! n
カウントアップ/ダウンの処理を再帰的に定義してみよう.
ソース count.c:
#include <stdio.h> /* カウントアップ表示する関数の再帰版 引数 n:最終値(初期値はゼロ) 戻り値:なし */ void countup(int n) { if (n > 0) countup(n - 1); // 0〜(n-1)を表示 printf("%d\n", n); // nを表示 } /* カウントダウン表示する関数の再帰版 引数 n:初期値(最終値はゼロ) 戻り値:なし */ void countdown(int n) { printf("%d\n", n); // nを表示 if (n > 0) countdown(n - 1); // (n-1)〜0を表示 } int main(void) { int n; printf("自然数 n > "); scanf("%d", &n); printf("カウントアップ:\n"); countup(n); printf("カウントダウン:\n"); countdown(n); return (0); }
この例では,繰り返しの終了が終了条件ではなく継続条件として記述されている. 終了条件として書いてもよいが,コードが少し長くなってしまう:
void countup(int n) { if (n < 0) return; // 終了条件 countup(n - 1); printf("%d\n", n); }
(今回の課題1です.)
前回作成した 累乗計算のプログラム power.c の関数分割版を元にして, 累乗関数 int power(int x, int y) の再帰版を定義せよ.
ヒント:(定義式)
xy = x x x ... x y = 0 に対して xy = 1 y > 0 に対して xy = xy − 1 x
(今回の課題2です.)
以前作成した反復による掛け算のプログラム mul.c を元にして, 乗算関数 int mul(int x, int y) の再帰版を定義せよ.
ヒント:(定義式)
x y = x+x+x+...+x y = 0 に対して x y = 0 y > 0 に対して x y = x(y − 1)+x
余裕ある人は,割り算 div.c 等も再帰関数化してみては? さらに上級者は,フィボナッチ数や二項係数について調査し,再帰関数を定義しては?
質問 Q1〜Q4 に回答し,電子メールで提出せよ.
メールの送信形式を必ず「テキスト形式 or プレーンテキスト」に変更せよ.