アルゴリズム

【C言語】フィボナッチ数列生成:再帰・反復・メモ化実装比較を解説

この記事では、C言語を使ってフィボナッチ数列を生成する際の3つの実装方法、再帰、反復、メモ化について比較します。

再帰は直感的な記述が可能ですが実行速度やメモリ使用量に注意が必要です。

反復はシンプルで高速な実装ができ、メモ化は計算の重複を避けつつ効率を改善できる方法です。

再帰による実装

アルゴリズムの基本

再帰的手法では、フィボナッチ数列の定義をそのままコードに反映させます。

数列は次のように定義されます。

fib(n)={0(n=0)1(n=1)fib(n1)+fib(n2)(n2)

この定義に基づき、関数 fibRecursive が自分自身を呼び出す形となります。

実装の流れと注意点

再帰による実装はシンプルですが、同じ計算を何度も行うため、注意が必要です。

以下はサンプルコードです。

#include <stdio.h>
// 再帰的にフィボナッチ数を計算する関数
int fibRecursive(int n) {
    // 基本ケース:nが0または1の場合はその値を返す
    if (n == 0) return 0;
    if (n == 1) return 1;
    // 再帰呼び出し:前の2項の和を計算する
    return fibRecursive(n - 1) + fibRecursive(n - 2);
}
int main(void) {
    int n = 10;  // 計算したいフィボナッチ数の位置
    // n番目のフィボナッチ数を出力
    printf("再帰によるフィボナッチ数列: %d 番目は %d \n", n, fibRecursive(n));
    return 0;
}
再帰によるフィボナッチ数列: 10 番目は 55

パフォーマンスと計算量の考察

再帰実装では、各関数呼び出しで多くの重複計算が発生するため、計算量は指数関数的になります。

特に n が大きい場合、処理時間が急激に増大します。

メモリ使用量はスタックフレームに依存するため、再帰深度が深くなるとスタックオーバーフローのリスクがあります。

反復による実装

アルゴリズムの基本

反復(イテレーティブ)実装は、ループを用いて順次フィボナッチ数を求めます。

基本的な考え方は、前の2つの数を保持しながら、次の値を計算する方法です。

実装の流れと注意点

ループ内で前項と前々項を更新していくため、再帰実装と比べ計算効率が良くなります。

以下はサンプルコードです。

#include <stdio.h>
// 反復的にフィボナッチ数を計算する関数
int fibIterative(int n) {
    if (n == 0) return 0;
    if (n == 1) return 1;
    int previous = 0;  // fib(0)
    int current = 1;   // fib(1)
    int next = 0;
    // 2番目以降のフィボナッチ数を順に計算
    for (int i = 2; i <= n; i++) {
        next = previous + current;
        previous = current;
        current = next;
    }
    return current;
}
int main(void) {
    int n = 10;  // 計算したいフィボナッチ数の位置
    // n番目のフィボナッチ数を出力
    printf("反復によるフィボナッチ数列: %d 番目は %d \n", n, fibIterative(n));
    return 0;
}
反復によるフィボナッチ数列: 10 番目は 55

パフォーマンスと計算量の考察

反復的実装はループで一度ずつ計算するため、計算量は線形 O(n) になります。

また、再帰実装に比べスタックフレームの使用がなく、メモリ使用量が低減されるため、より大きな n に対して安定したパフォーマンスを発揮します。

メモ化による実装

アルゴリズムの基本

メモ化実装は、再帰的手法の利便性を維持しながら、既に計算した部分問題の結果を保持して再利用します。

これにより、同じ計算を繰り返すことを防止し、効率を向上させます。

実装の流れと注意点

メモ化には配列などのキャッシュを用い、再帰呼び出し前にキャッシュを確認します。

以下はサンプルコードです。

配列の初期化や添字の管理に注意してください。

#include <stdio.h>
#include <string.h>
// メモ化のための配列サイズ。ここでは十分な大きさを確保する
#define MAX_SIZE 100
// キャッシュ用の配列。初期値は -1 に設定する
int memo[MAX_SIZE];
// メモ化再帰的にフィボナッチ数を計算する関数
int fibMemo(int n) {
    // キャッシュに値がある場合はそれを返す
    if (memo[n] != -1) return memo[n];
    // 再帰呼び出し:nが0または1の場合の基本ケース
    if (n == 0) return memo[n] = 0;
    if (n == 1) return memo[n] = 1;
    // フィボナッチ数を計算し、キャッシュに登録
    memo[n] = fibMemo(n - 1) + fibMemo(n - 2);
    return memo[n];
}
int main(void) {
    int n = 10;  // 計算したいフィボナッチ数の位置
    // キャッシュ配列を初期化(全ての要素を -1 に設定)
    for (int i = 0; i < MAX_SIZE; i++) {
        memo[i] = -1;
    }
    // n番目のフィボナッチ数を出力
    printf("メモ化によるフィボナッチ数列: %d 番目は %d \n", n, fibMemo(n));
    return 0;
}
メモ化によるフィボナッチ数列: 10 番目は 55

パフォーマンスと計算量の考察

メモ化実装は、各部分問題が一度だけ計算されるため、計算量は反復実装と同じく線形 O(n) になります。

ただし、配列を用いたキャッシュのため、追加のメモリ領域が必要となります。

再帰による実装のシンプルさと反復による効率の両方のメリットを取り入れた手法と言えます。

実装方法の比較

再帰・反復・メモ化の特徴比較

以下の表に、各実装方法の特徴をまとめます。

手法主な特徴計算量メモリ使用量
再帰定義通りに直感的に実装できるO(2n)再帰呼び出し時のスタック
反復ループ処理により効率的に計算O(n)定数レベル
メモ化再利用可能な部分問題の結果をキャッシュするO(n)配列による追加メモリ

パフォーマンスとメモリ使用量の比較

  • 再帰実装は、シンプルでわかりやすい反面、処理速度が低くスタック領域を多く消費します。
  • 反復実装は、ループを用いるため計算速度が速く、メモリのオーバーヘッドも最小限です。
  • メモ化実装は、再帰の実装の簡潔さを保ちつつ、キャッシュを利用して計算回数を削減しますが、キャッシュ用のメモリを消費します。

用途別の実装選択のポイント

  • 小規模な数列の計算や、コードの明確さを重視する場合は「再帰実装」が適しています。
  • 大きな数列やパフォーマンスが要求される場面では、計算量が低い「反復実装」を推奨します。
  • 再帰の分かりやすさを維持しながらも効率化を図りたい場合は、「メモ化実装」が有効です。

まとめ

この記事では、フィボナッチ数列を再帰、反復、メモ化の3つの手法で実装する方法と、その各々の基本アルゴリズム、実装上の手順や注意点、パフォーマンスと計算量の観点からの特徴について解説しています。

各手法のメリット・デメリットが明確になり、用途に合わせた適切な実装選択の判断材料が得られる内容となっています。

関連記事

Back to top button