関数の応用
再帰は向かい合った2つの鏡のようなものです——像の中に像が入り、遠くの光の点が消えるまで続きません。自分自身を呼び出す関数は、漸次的に問題を縮小するこの力で問題を解決します。
再帰の原理
再帰とは関数が自分自身を直接または間接的に呼び出すプログラミング手法です。すべての再帰には2つの要素が必要です。
- 再帰条件:問題をより小さな同種の部分問題に分解できる
- 基底ケース(停止条件):最小の部分問題には直接の答えがあり、これ以上再帰しない
int factorial(int n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
factorial(4)を例に、実行過程を示します。
factorial(4)
= 4 * factorial(3)
= 4 * 3 * factorial(2)
= 4 * 3 * 2 * factorial(1)
= 4 * 3 * 2 * 1
= 24
古典的な再帰の例
階乗
nの階乗の定義:n! = n x (n-1)!、0! = 1。
long factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
long型の範囲には限界があり、factorial(20)はすでに64ビットの限界に近いことに注意してください。
フィボナッチ数列
フィボナッチの定義:F(1)=1, F(2)=1, F(n)=F(n-1)+F(n-2)。
long fib(int n) {
if (n <= 2) return 1;
return fib(n - 1) + fib(n - 2);
}
fib(40)は数億回の呼び出しを要します。実践ではループやメモ化を使ってください。
ハノイの塔
n枚の円盤を棒Aから棒Cに移動します。棒Bを補助に使います。ルール:大きい円盤を小さい円盤の上に置いてはいけません。
#include <stdio.h>
void hanoi(int n, char from, char mid, char to) {
if (n == 1) {
printf("%c -> %c\n", from, to);
return;
}
hanoi(n - 1, from, to, mid);
printf("%c -> %c\n", from, to);
hanoi(n - 1, mid, from, to);
}
int main(void) {
hanoi(3, 'A', 'B', 'C');
return 0;
}
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C
考え方:まず上のn-1枚を補助棒に移し、最大の円盤を目的棒に移し、それからn-1枚を補助棒から目的棒に移します。3枚で7手、n枚で2^n - 1手必要です。
例
再帰で整数の桁の合計を計算します。
#include <stdio.h>
int digit_sum(int n) {
if (n < 10) return n;
return n % 10 + digit_sum(n / 10);
}
int main(void) {
printf("%d\n", digit_sum(12345));
printf("%d\n", digit_sum(7));
return 0;
}
15
7
digit_sum(12345) = 5 + digit_sum(1234) = 5 + 4 + digit_sum(123) = ... = 5+4+3+2+1 = 15。
変数のスコープ
スコープは、どのコード領域が変数を認識し使えるかを決定します。
ブロックスコープ
{}内で定義された変数はそのブロック内でのみ可視であり、ブロック終了時に破棄されます。
int main(void) {
int x = 10;
{
int y = 20;
printf("%d %d\n", x, y);
}
printf("%d\n", x);
return 0;
}
yは内側のブロック内にしか存在せず、外側のブロックからはアクセスできません。
ファイルスコープ
すべての関数の外で定義された変数はファイルスコープを持ち、定義位置からファイル末尾まで可視です。これらは大域変数と呼ばれます。
int count = 0;
void increment(void) {
count++;
}
int main(void) {
increment();
increment();
printf("%d\n", count);
return 0;
}
名前の隠蔽
内側のスコープの変数は、外側のスコープの同名の変数を隠します。
int x = 100;
int main(void) {
int x = 10;
printf("%d\n", x);
return 0;
}
出力は10です。内側のxが大域のxを隠しています。内側のブロックから大域のxには直接アクセスできません。
記憶クラス
記憶クラスは変数の寿命と可視性を決定します。Cには4つの記憶クラスキーワードがあります。auto、static、extern、registerです。
auto
autoは局所変数のデフォルトの記憶クラスで、通常は省略されます。変数はブロックに入った時に作成され、ブロックを抜けると破棄されます。
void func(void) {
auto int x = 10;
}
これはint x = 10;と等価です。
static
staticが局所変数に修飾されると、変数はプログラム実行中ずっと存在しますが、スコープは関数内に限定されたままです。重要な性質:1回だけ初期化され、以降の呼び出しでは前回の値が保持されます。
#include <stdio.h>
void counter(void) {
static int count = 0;
count++;
printf("Call #%d\n", count);
}
int main(void) {
counter();
counter();
counter();
return 0;
}
Call #1
Call #2
Call #3
staticがなければ、countは毎回0に再初期化され、常に"Call #1"と出力されてしまいます。
staticが大域変数や関数に修飾されると、可視性を現在のファイルに制限します(内部結合)。他のソースファイルからexternでアクセスできなくなります。
extern
externは別のソースファイルで定義された大域変数を宣言し、コンパイラに「この変数は存在するが、現在のファイルでは定義されていない」と伝えます。
file1.c:
int shared_data = 42;
file2.c:
#include <stdio.h>
extern int shared_data;
int main(void) {
printf("%d\n", shared_data);
return 0;
}
externはメモリを割り当てず、宣言のみです。複数ファイルのプロジェクトで大域データを共有するにはexternが必要です。
register
registerはコンパイラに変数をCPUレジスタに格納するよう提案し、高速なアクセスを狙います。
void fast_loop(void) {
register int i;
for (i = 0; i < 1000000; i++) {
}
}
現代のコンパイラは最適化が非常に優れており、頻繁に使う変数は自動的にレジスタに配置するため、registerのヒントはほとんど効果がありません。注意:register変数はアドレスを取得(&)できません。レジスタにはメモリアドレスがないためです。
例
ループによる反復的フィボナッチ(再帰の重複計算を回避)。
#include <stdio.h>
long fib_iter(int n) {
if (n <= 2) return 1;
long prev = 1;
long curr = 1;
long next;
int i;
for (i = 3; i <= n; i++) {
next = prev + curr;
prev = curr;
curr = next;
}
return curr;
}
int main(void) {
int i;
for (i = 1; i <= 10; i++) {
printf("F(%d) = %ld\n", i, fib_iter(i));
}
return 0;
}
F(1) = 1
F(2) = 1
F(3) = 2
F(4) = 3
F(5) = 5
F(6) = 8
F(7) = 13
F(8) = 21
F(9) = 34
F(10) = 55
この反復版の時間計算量はO(n)であり、素朴な再帰のO(2^n)よりはるかに優れています。
❓ よくある質問
registerはほぼ追加の効果がなく、このキーワードは主に歴史的な意味を持ちます。📖 まとめ
- 再帰には基底ケースが必須です。さもないとスタックオーバーフローします
- 素朴な再帰フィボナッチは非効率です。実践的な問題にはループやメモ化が必要です
- 局所変数はブロックスコープ、大域変数はファイルスコープを持ちます
- static局所変数はプログラム全体で持続しますが、スコープは変わりません
- externはファイル間の変数共有を可能にします。registerは現代のコンパイラではほとんど意味がありません
📝 練習問題
- 正の整数の桁を逆順に出力する再帰関数
void print_reverse(int n)を書いてください(例:123は3 2 1と出力)。 - static変数を使って、関数が何回呼び出されたかを数える関数を書いてください。mainで5回呼び出し、回数を出力してください。
- 再帰で二分探索を実装してください。ソート済み配列で目標値を探し、見つかれば添字を、見つからなければ-1を返します。



