Java | フィボナッチを高速化する方法

Java Java
スポンサーリンク

Javaで学ぶ「フィボナッチを高速化する方法」(前半)

フィボナッチ数列は、アルゴリズム学習の定番テーマです。 しかし、素直な再帰で書いたフィボナッチは「とんでもなく遅い」コードになります。 前半では、まず「なぜ遅いのか」を丁寧にほどき、そのうえで ループ版・メモ化版という、初心者でもすぐに使える高速化の入り口を解説していきます。 後半では、さらに一歩進んだ高速化(動的計画法・行列累乗など)に踏み込みます。

フィボナッチ数列の定義をコードで確認する

フィボナッチ数列は次のように定義されます。

F(0)=0 F(1)=1 F(n)=F(n1)+F(n2) (n ≥ 2)

Javaで最も教科書的な書き方をすると、こうなります。

public static int fibRecursive(int n) {
    if (n <= 1) {
        return n;
    }
    return fibRecursive(n - 1) + fibRecursive(n - 2);
}
Java

見た目はとても美しいですが、このコードには大きな問題があります。 それが「計算量が爆発する」という点です。

なぜ素直な再帰フィボナッチは遅いのか

fibRecursive(40)fibRecursive(45) を呼んでみると、 「いつまで経っても終わらない」感覚を味わうことになります。

理由は、同じ値を何度も何度も計算しているからです。

例えば fibRecursive(5) を考えてみましょう。

fib(5)fib(4)fib(3) を呼びます。 fib(4)fib(3)fib(2) を呼びます。 fib(3)fib(2)fib(1) を呼びます。

ここで注目すべきなのは、fib(3)fib(2) が何度も登場していることです。 同じ値を何度も再計算しているため、 呼び出し回数が指数関数的に増えていきます。

この「同じ計算の繰り返し」が、 素直な再帰フィボナッチを「遅いコード」にしている本質です。

まずはループ版に書き換えてみる(O(n) への改善)

最初の高速化として、再帰をやめてループで書く方法があります。 これは「下から順番に計算していく」発想です。

public static int fibLoop(int n) {
    if (n <= 1) return n;

    int a = 0; // F(0)
    int b = 1; // F(1)

    for (int i = 2; i <= n; i++) {
        int c = a + b; // F(i) = F(i-1) + F(i-2)
        a = b;
        b = c;
    }
    return b;
}
Java

このコードは、次のような流れで動きます。

最初に F(0) = 0, F(1) = 1 を用意する F(2), F(3), …, F(n) を順番に計算していく 常に「直前2つの値」だけを持ち歩く

重要なのは、「同じ値を二度と計算しない」という点です。 これにより、計算量は O(n) に改善されます。

深掘り:ループ版の動きを具体的に追ってみる

例えば fibLoop(5) を呼び出したときの動きを追ってみます。

最初に a = 0(F(0)) b = 1(F(1))

i = 2 のとき c = a + b = 0 + 1 = 1(F(2)) a = b = 1 b = c = 1

i = 3 のとき c = a + b = 1 + 1 = 2(F(3)) a = b = 1 b = c = 2

i = 4 のとき c = a + b = 1 + 2 = 3(F(4)) a = b = 2 b = c = 3

i = 5 のとき c = a + b = 2 + 3 = 5(F(5)) a = b = 3 b = c = 5

最後に b を返すので、結果は 5 になります。 このように、ループ版は「一度通るだけ」で目的の値に到達します。

メモ化(Memoization)という考え方を知る

ループ版は十分速いですが、 「再帰の形を保ったまま高速化したい」という場面もあります。

そこで登場するのが メモ化(Memoization) です。 メモ化とは、「一度計算した結果を覚えておいて、次回は再利用する」というテクニックです。

フィボナッチにメモ化を適用すると、 「同じ値を何度も計算する」という問題を解消できます。

Javaで書くメモ化付きフィボナッチ(再帰+配列)

メモ化付きフィボナッチの基本形は次のようになります。

public static int fibMemo(int n, int[] memo) {
    if (n <= 1) return n;

    if (memo[n] != -1) {
        return memo[n];
    }

    memo[n] = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);
    return memo[n];
}

public static int fibMemoMain(int n) {
    int[] memo = new int[n + 1];
    for (int i = 0; i <= n; i++) {
        memo[i] = -1;
    }
    return fibMemo(n, memo);
}
Java

ここで重要なのは、次の二点です。

一度計算した値は memo[n] に保存する 次に同じ n が来たときは、再帰せずに memo[n] を返す

これにより、各 n について計算は一度だけになり、 計算量は O(n) まで改善されます。

深掘り:メモ化の効果を具体例で感じる

例えば fibMemoMain(5) を呼び出したとき、 fibMemo(5)fibMemo(4)fibMemo(3) を呼びます。

fibMemo(4)fibMemo(3)fibMemo(2) を呼びますが、 ここで fibMemo(3) は一度計算された後、memo[3] に保存されます。

そのため、二度目以降の fibMemo(3) は 再帰せずに memo[3] を返します。

この「二度目以降は即座に返す」という動きが、 メモ化による高速化の本質です。

再帰版・ループ版・メモ化版の違いを直感で整理する

前半の最後に、三つのバージョンの違いを直感でまとめておきます。

再帰版 定義そのまま 見た目は美しい 同じ値を何度も計算する 計算量は O(2ⁿ) で非常に遅い

ループ版 下から順番に計算する 一度通るだけ 計算量は O(n) 実務で使いやすい

メモ化版 再帰の形を保ちつつ、結果を保存する 同じ値は一度しか計算しない 計算量は O(n) 「再帰+高速化」の良いバランス

後半では、ここからさらに一歩進んで

動的計画法としてのフィボナッチ long や BigInteger を使ったオーバーフロー対策 行列累乗による O(log n) フィボナッチ セキュリティ・パフォーマンスの観点から見た「危険なフィボナッチ」

といったテーマを、具体的な Java コードとともに解説していきます。

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