C言語 再帰関数の練習問題

再帰関数(自分自身を呼び出す関数)を書く問題を、初級から上級まで12問そろえました。main 関数はあらかじめ書いてあり、あなたが作るのは指定された関数の中身だけです。ループを使わず再帰で書くのが目標です。「採点する」を押すと、本物の gcc でいくつかの入力を試し、出力が正しいかを判定します。

この練習問題の使い方
  1. 問題文と実行例を読み、下の欄にコードを書く
  2. 「▶ 実行」で自分で入力を与えて動かしてみる
  3. 「✔ 採点する」で、用意したいくつかの入力に対して出力が正しいかを判定
  4. 行き詰まったら「ヒント」、解けたら「模範解答と解説」で確認
採点のルール
  • 出力は実行例と同じ形で表示する。「数を入力してください」のような入力を促すメッセージは表示しない
  • 行末の空白と、最後の空行の有無は区別しない
  • 書いたコードはこのブラウザに自動保存されます

※ 実行と採点では、コードを外部の無料コンパイルサービス Wandbox に送信して gcc で動かします。

全12問(初級 4・中級 4・上級 4)

再帰関数の練習問題(12問)

R01 初級

問1. 1からNまでの合計(再帰)

0 以上の整数 n(0 ≤ n ≤ 1000)を受け取り、1 から n までの合計を返す関数 int sum_to(int n) を、ループを使わず再帰で作れ。n が 0 のときは 0 を返す。main 関数は用意してある。
実行例(下線が入力)
10 合計: 55
コードを書く
「実行」で入力を与えて動かす/「採点する」で 4 個のテストを自動で試します
「1〜n の合計」=「1〜(n−1) の合計」+ n と考えます。つまり return sum_to(n - 1) + n; です。これだけだと止まらないので、n == 0 のときは 0 を返す終了条件を先に書きます。
#include <stdio.h>

int sum_to(int n) {
    if (n == 0) {
        return 0;
    }
    return sum_to(n - 1) + n;
}

int main(void) {
    int n;
    scanf("%d", &n);
    printf("合計: %d\n", sum_to(n));
    return 0;
}
解説:再帰関数は「終了条件(ベースケース)」と「1 つ小さい問題に分けて自分を呼ぶ部分」の2つでできています。終了条件を忘れると呼び出しが止まらず、最後はスタックオーバーフローでプログラムが異常終了します。
R02 初級

問2. 累乗(再帰)

整数 x(−9 ≤ x ≤ 9)と 0 以上の整数 n(0 ≤ n ≤ 18)を受け取り、x の n 乗を返す関数 long long power(int x, int n) を再帰で作れ。x0 は 1 とする(00 も 1)。main 関数は用意してある。
実行例(下線が入力)
2 10 2^10 = 1024
コードを書く
「実行」で入力を与えて動かす/「採点する」で 5 個のテストを自動で試します
xn = x × xn−1 です。n == 0 なら 1 を返し、それ以外は return x * power(x, n - 1); とします。
#include <stdio.h>

long long power(int x, int n) {
    if (n == 0) {
        return 1;
    }
    return x * power(x, n - 1);
}

int main(void) {
    int x, n;
    scanf("%d %d", &x, &n);
    if (x < 0) printf("(%d)^%d = %lld\n", x, n, power(x, n));
    else       printf("%d^%d = %lld\n", x, n, power(x, n));
    return 0;
}
解説:掛け算の積み重ねなので、終了条件で返す値は 0 ではなく 1 です(0 を返すと何を掛けても 0 になります)。918 は約 1.5×1017 で int に入らないため、戻り値を long long にしています。x * power(...) では x が long long に変換されてから掛け算されます。
R03 初級

問3. 桁数を数える(再帰)

0 以上の整数 n(0 ≤ n ≤ 2000000000)を受け取り、その桁数を返す関数 int count_digits(int n) を再帰で作れ。0 は 1 桁とする。main 関数は用意してある。
実行例(下線が入力)
12345 桁数: 5
コードを書く
「実行」で入力を与えて動かす/「採点する」で 5 個のテストを自動で試します
n が 1 桁(n < 10)なら 1 を返します。そうでなければ、一の位を捨てた n / 10 の桁数に 1 を足したものが答えです。
#include <stdio.h>

int count_digits(int n) {
    if (n < 10) {
        return 1;
    }
    return 1 + count_digits(n / 10);
}

int main(void) {
    int n;
    scanf("%d", &n);
    printf("桁数: %d\n", count_digits(n));
    return 0;
}
解説:終了条件を n == 0 で 0 を返す形にすると、入力が 0 のときに桁数が 0 になってしまいます。どの値で止めるかは、いちばん小さい入力(ここでは 0)で答えが合うかで確かめましょう。
R04 初級

問4. フィボナッチ数

フィボナッチ数 F(0) = 0、F(1) = 1、F(n) = F(n−1) + F(n−2) の n 番目を返す関数 int fib(int n) を再帰で作れ(0 ≤ n ≤ 30)。main 関数は用意してある。
実行例(下線が入力)
10 fib(10) = 55
コードを書く
「実行」で入力を与えて動かす/「採点する」で 5 個のテストを自動で試します
定義をそのまま書きます。n == 0 なら 0、n == 1 なら 1 を返し、それ以外は return fib(n - 1) + fib(n - 2); です。
#include <stdio.h>

int fib(int n) {
    if (n == 0) return 0;
    if (n == 1) return 1;
    return fib(n - 1) + fib(n - 2);
}

int main(void) {
    int n;
    scanf("%d", &n);
    printf("fib(%d) = %d\n", n, fib(n));
    return 0;
}
解説:自分を2回呼ぶ再帰なので、終了条件も 2つ(n=0 と n=1)必要です。n=1 の終了条件がないと fib(1) が fib(-1) を呼んで止まりません。なお、この書き方は同じ計算を何度もくり返すため、n が 1 増えるごとに計算量が約 1.6 倍になります(n=30 で約 270 万回の呼び出し)。
R05 中級

問5. 文字列を逆順に表示

文字列 s と位置 i を受け取り、s[i] から文字列の終わりまでを逆順に表示する関数 void print_reverse(const char s[], int i) を再帰で作れ(ループは使わない)。main 関数は用意してあり、空白を含まない英数字・記号の文字列(100 文字以内)を読んで print_reverse(s, 0) を呼び、最後に改行する。
実行例(下線が入力)
hello olleh
コードを書く
「実行」で入力を与えて動かす/「採点する」で 5 個のテストを自動で試します
s[i] が終端文字 '\0' なら何もせずに戻ります。そうでなければ、先に print_reverse(s, i + 1) で残りを逆順に表示し、そのあとで s[i] を表示します。
#include <stdio.h>

void print_reverse(const char s[], int i) {
    if (s[i] == '\0') {
        return;
    }
    print_reverse(s, i + 1);
    printf("%c", s[i]);
}

int main(void) {
    char s[101];
    scanf("%100s", s);
    print_reverse(s, 0);
    printf("\n");
    return 0;
}
解説:再帰呼び出しと表示の順番がポイントです。printf を再帰呼び出しの前に書くと、そのままの順で表示されます。後ろに書くと、いちばん深い呼び出し(最後の文字)から戻ってくる順に表示されるので逆順になります。
R06 中級

問6. 2進数で表示

0 以上の整数 n(0 ≤ n ≤ 1000000000)を受け取り、2進数で表示する関数 void print_binary(int n) を再帰で作れ(ループや配列は使わない)。余分な 0 を先頭に付けないこと。0 は 0 と表示する。main 関数は用意してあり、関数を呼んだあと改行する。
実行例(下線が入力)
10 1010
コードを書く
「実行」で入力を与えて動かす/「採点する」で 6 個のテストを自動で試します
2進数の一番下の桁は n % 2、それより上の桁は n / 2 の2進数です。上の桁から表示したいので、n >= 2 なら先に print_binary(n / 2) を呼び、そのあとで n % 2 を表示します。
#include <stdio.h>

void print_binary(int n) {
    if (n >= 2) {
        print_binary(n / 2);
    }
    printf("%d", n % 2);
}

int main(void) {
    int n;
    scanf("%d", &n);
    print_binary(n);
    printf("\n");
    return 0;
}
解説:ループで「2 で割った余り」を順に出すと、下の桁から(逆向きに)表示されてしまいます。再帰で先に上の桁を処理してから自分の桁を出せば、配列なしで正しい順に並びます。終了条件を「n == 0 なら何もしない」にすると、入力 0 のときに何も表示されません。
R07 中級

問7. 配列の最大値(再帰)

配列 a の先頭 n 個(n ≥ 1)の中の最大値を返す関数 int array_max(const int a[], int n) を再帰で作れ(ループは使わない)。main 関数は用意してあり、n(1 ≤ n ≤ 100)と n 個の整数(絶対値 10000 以下)を読む。
実行例(下線が入力)
5 3 8 -2 7 4 最大値: 8
コードを書く
「実行」で入力を与えて動かす/「採点する」で 5 個のテストを自動で試します
要素が1個(n == 1)なら a[0] が最大です。そうでなければ、「先頭 n−1 個の最大値」array_max(a, n - 1) と最後の要素 a[n - 1] の大きい方を返します。
#include <stdio.h>

int array_max(const int a[], int n) {
    if (n == 1) {
        return a[0];
    }
    int m = array_max(a, n - 1);
    if (a[n - 1] > m) {
        return a[n - 1];
    }
    return m;
}

int main(void) {
    int a[100];
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%d", &a[i]);
    }
    printf("最大値: %d\n", array_max(a, n));
    return 0;
}
解説:配列の再帰は「n 個の問題を n−1 個の問題に縮める」と考えます。最後の要素は a[n] ではなく a[n - 1] です(a[n] は範囲外)。再帰呼び出しの結果を変数 m に受けておくと、同じ呼び出しを2回書かずに済みます。
R08 中級

問8. 回文の判定(再帰)

文字列 s の s[left]〜s[right] の部分が回文(前から読んでも後ろから読んでも同じ)なら 1、そうでなければ 0 を返す関数 int is_palindrome(const char s[], int left, int right) を再帰で作れ。main 関数は用意してあり、空白を含まない英小文字の文字列(1〜100 文字)を読んで、文字列全体について判定する。
実行例(下線が入力)
level level は回文です
コードを書く
「実行」で入力を与えて動かす/「採点する」で 6 個のテストを自動で試します
left >= right(残りが 1 文字以下)なら回文なので 1 を返します。両端 s[left] と s[right] が違えば 0。同じなら、内側 (left + 1, right - 1) について同じ判定を再帰で行い、その結果を返します。
#include <stdio.h>
#include <string.h>

int is_palindrome(const char s[], int left, int right) {
    if (left >= right) {
        return 1;
    }
    if (s[left] != s[right]) {
        return 0;
    }
    return is_palindrome(s, left + 1, right - 1);
}

int main(void) {
    char s[101];
    scanf("%100s", s);
    int len = (int)strlen(s);
    if (is_palindrome(s, 0, len - 1)) {
        printf("%s は回文です\n", s);
    } else {
        printf("%s は回文ではありません\n", s);
    }
    return 0;
}
解説:最後の行で is_palindrome(...) を呼ぶだけで return を書き忘れると、内側の判定結果が呼び出し元に伝わりません(その値を使うのは未定義動作です)。終了条件を left == right だけにすると、偶数文字(abba など)で left と right がすれ違い、文字列の範囲外まで読みに行ってしまいます。
R09 上級

問9. ハノイの塔

3本の柱 A・B・C があり、A に大きさの違う n 枚の円盤(上から 1, 2, …, n)が積んである。「1回に1枚だけ動かす」「小さい円盤の上に大きい円盤を置かない」というルールで、全部を C に移す手順を表示する関数 void hanoi(int n, char from, char to, char via) を作れ(from から to へ、via を経由して n 枚を移す)。1回の移動ごとに「円盤k: X -> Y」と1行表示する。main 関数は用意してあり、n(1 ≤ n ≤ 10)を読んで hanoi(n, 'A', 'C', 'B') を呼ぶ。
実行例(下線が入力)
2 円盤1: A -> B 円盤2: A -> C 円盤1: B -> C
コードを書く
「実行」で入力を与えて動かす/「採点する」で 5 個のテストを自動で試します
n 枚を from から to へ移すには、①上の n−1 枚を from から via へ移す(to を経由)、②円盤 n を from から to へ動かして表示、③ via の n−1 枚を to へ移す(from を経由)。①と③が再帰呼び出しです。n == 0 なら何もしない、が終了条件です。
#include <stdio.h>

void hanoi(int n, char from, char to, char via) {
    if (n == 0) {
        return;
    }
    hanoi(n - 1, from, via, to);
    printf("円盤%d: %c -> %c\n", n, from, to);
    hanoi(n - 1, via, to, from);
}

int main(void) {
    int n;
    scanf("%d", &n);
    hanoi(n, 'A', 'C', 'B');
    return 0;
}
解説:「n−1 枚を動かす方法はもう分かっている」と信じて組み立てるのが再帰の考え方です。よくある間違いは、再帰呼び出しで柱の引数の順番を取り違えることです(①は to と via を入れ替え、③は from と via を入れ替えます)。移動回数は 2n−1 回で、n=10 では 1023 行になります。
R10 上級

問10. 二分探索(再帰版)

小さい順に並んだ配列 a の a[lo]〜a[hi] の範囲から key を探し、見つかればその添字を、なければ −1 を返す関数 int bsearch_rec(const int a[], int lo, int hi, int key) を、二分探索の再帰で作れ。main 関数は用意してあり、n(1 ≤ n ≤ 100)、狭義の昇順(同じ値はない)に並んだ n 個の整数、探す値 key の順に読む。添字は 0 から数える。
実行例(下線が入力)
7 2 5 8 12 16 23 38 16 16 は a[4] にあります
コードを書く
「実行」で入力を与えて動かす/「採点する」で 6 個のテストを自動で試します
lo > hi(探す範囲が空)なら −1 を返します。真ん中 mid = (lo + hi) / 2 を見て、a[mid] == key なら mid を返し、key の方が小さければ左半分 (lo, mid - 1)、大きければ右半分 (mid + 1, hi) を再帰で探して、その結果を return します。
#include <stdio.h>

int bsearch_rec(const int a[], int lo, int hi, int key) {
    if (lo > hi) {
        return -1;
    }
    int mid = (lo + hi) / 2;
    if (a[mid] == key) {
        return mid;
    } else if (key < a[mid]) {
        return bsearch_rec(a, lo, mid - 1, key);
    } else {
        return bsearch_rec(a, mid + 1, hi, key);
    }
}

int main(void) {
    int a[100];
    int n, key;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%d", &a[i]);
    }
    scanf("%d", &key);
    int pos = bsearch_rec(a, 0, n - 1, key);
    if (pos >= 0) {
        printf("%d は a[%d] にあります\n", key, pos);
    } else {
        printf("%d は見つかりません\n", key);
    }
    return 0;
}
解説:次の範囲を (lo, mid) や (mid, hi) にすると、範囲が縮まらずに同じ呼び出しをくり返して止まらないことがあります。mid はもう調べたので必ず外します。1回ごとに範囲が半分になるので、100 個でも7回ほどの呼び出しで終わります。
R11 上級

問11. 組み合わせの数 nCr

n 個から r 個を選ぶ組み合わせの数 nCr を返す関数 int comb(int n, int r) を、関係式 nCr = n−1Cr−1 + n−1Cr を使った再帰で作れ(階乗は使わない)。nC0 = nCn = 1 とする。main 関数は用意してあり、n と r(0 ≤ r ≤ n ≤ 20)を読む。
実行例(下線が入力)
5 2 5C2 = 10
コードを書く
「実行」で入力を与えて動かす/「採点する」で 6 個のテストを自動で試します
終了条件は r == 0 || r == n のとき 1 を返すことです。それ以外は return comb(n - 1, r - 1) + comb(n - 1, r);(パスカルの三角形の「左上と右上の和」)です。
#include <stdio.h>

int comb(int n, int r) {
    if (r == 0 || r == n) {
        return 1;
    }
    return comb(n - 1, r - 1) + comb(n - 1, r);
}

int main(void) {
    int n, r;
    scanf("%d %d", &n, &r);
    printf("%dC%d = %d\n", n, r, comb(n, r));
    return 0;
}
解説:終了条件を r == 0 だけにすると、comb(n - 1, r) で r が n より大きくなる呼び出しが生まれ、n が負になっても止まりません。両端(r=0 と r=n)の2つが必要です。階乗を使う方法 n!/(r!(n−r)!) は、途中の階乗が int に入らなくなりやすい(13! で int の範囲を超える)点でも、この再帰の方が安全です。
R12 上級

問12. 順列をすべて列挙

1 から n までの数字を1回ずつ使って並べた順列を、辞書順(小さい順)にすべて表示せよ。1つの順列を空白区切りで1行に表示する。関数 void permute(int perm[], int used[], int depth, int n) を再帰で作ること。perm は作りかけの順列、used[k] は数字 k+1 を使用中なら 1、depth はすでに決めた個数。main 関数は用意してあり、n(1 ≤ n ≤ 5)を読んで permute(perm, used, 0, n) を呼ぶ。
実行例(下線が入力)
3 1 2 3 1 3 2 2 1 3 2 3 1 3 1 2 3 2 1
コードを書く
「実行」で入力を与えて動かす/「採点する」で 5 個のテストを自動で試します
depth == n なら全部決まったので perm を表示して戻ります。そうでなければ、数字 1〜n のうちまだ使っていないものを小さい順に1つ選び、perm[depth] に入れて used を 1 にし、permute(perm, used, depth + 1, n) を呼びます。戻ってきたら used を 0 に戻します。
#include <stdio.h>

void permute(int perm[], int used[], int depth, int n) {
    if (depth == n) {
        for (int i = 0; i < n; i++) {
            printf("%d ", perm[i]);
        }
        printf("\n");
        return;
    }
    for (int k = 0; k < n; k++) {
        if (!used[k]) {
            used[k] = 1;
            perm[depth] = k + 1;
            permute(perm, used, depth + 1, n);
            used[k] = 0;
        }
    }
}

int main(void) {
    int perm[5];
    int used[5] = {0};
    int n;
    scanf("%d", &n);
    permute(perm, used, 0, n);
    return 0;
}
解説:これはバックトラックと呼ばれる手法です。再帰から戻ったあとに used[k] = 0; で「使用中」の印を元に戻すのを忘れると、一度使った数字が二度と選べなくなり、最初の 1 つの順列しか出てきません。配列は呼び出し先と共有されるので、印の付け外しを自分で管理する必要があります。順列の数は n! 個(n=5 で 120 個)です。

復習する講座

← 練習問題の一覧へ