Up

フィボナッチ数の計算

再帰・関数オブジェクト

 

フィボナッチ数は次式

、 

で与えられる。この式は、再帰的呼び出しの関数としてそのまま表すことができる。リスト1に例を示す。

 

========== リスト1 ===================

include <iostream>

 

using namespace std;

 

double Fibonacci(int n) {

        double v;

        if (n <= 0) {

                v = 0.0;

        }

        else if (n == 1) {

                v = 1.0;

        }

        else {

                v = Fibonacci(n - 2) + Fibonacci(n - 1);

        }

        return v;

}

 

int main() {

        cout << "n = ";

        int n;

        cin >> n;

        cout << "Fibonacci(" << n << ") = " << Fibonacci(n) << endl;

 

        cout << endl << "何か文字キーを押してから、Enterキーを押して終了。" << endl;

        char ck;

        cin >> ck;

        return 0;

}

==================================

 

上のプログラムを、Visual Studio 2015で新規作成するときは、まず、メニュ項目「ファイル|新規作成|プロジェクト」を選ぶ。

表示された「新しいプロジェクト」ダイアログウィンドウにおいて、「テンプレート|Visual C++|全般」を選んで表示されるものから「空のプロジェクト」を選んでプロジェクトを作成する。

プロジェクトが作成されたら、メニュ項目「プロジェクト|新しい項目の追加」を選ぶ。表示される「新しい項目の追加」ダイアログウィンドウにおいて「Visual C++|コード」を選び、表示されたものから「C++ファイル(.cpp)」を選ぶ。

追加項目として表示されるC++ファイルに上のコードを書き込めばよい。実行例を図1に示す。

図1

 

Visual Studio 2015におけるC++プログラムの新規作成については、ウェブサイト

http://y-okamoto-psy1949.la.coocan.jp/booksetc/introvcpp/On_vs2015/

に説明を用意している。

フィボナッチ数を再帰的に計算する場合、

というように、同じ関数が複数回再帰的に呼び出されるということが起こる。上の場合、が2回呼び出されている。このため、が大きくなると同じ関数が多数回呼び出されることが起こり、そのために計算時間が長くなる。

同じ関数の複数回の呼び出しを避けるため、計算結果を配列に保存する方法を採用したものがリスト2の関数Fibonacci_Aである。

 

=============== リスト2 ==========================

#include <iostream>

#include <time.h>               //              function time()

 

using namespace std;

 

double Fibonacci(int n) {

        double v;

        if (n <= 0) {

                v = 0.0;

        }

        else if (n == 1) {

                v = 1.0;

        }

        else {

                v = Fibonacci(n - 2) + Fibonacci(n - 1);

        }

        return v;

}

 

double Fibonacci_A(int n) {

        double v;

        if (n <= 0) {

                v = 0.0;

        }

        else if (n == 1) {

                v = 1.0;

        }

        else {

        //      v = Fibonacci(n - 2) + Fibonacci(n - 1);

                double * F = new double[n + 1];

                F[0] = 0.0;

                F[1] = 1.0;

                for (int i = 2; i <= n; i++)

                        F[i] = F[i - 2] + F[i - 1];

                v = F[n];

                delete[] F;

        }

        return v;

}

 

 

int main() {

        cout << "n = ";

        int n;

        cin >> n;

        time_t t0, t1;

        time(&t0);

        cout << "Fibonacci(" << n << ") = " << Fibonacci(n) << endl;

        time(&t1);

        cout << difftime(t1, t0) << "s" << endl;

 

        time(&t0);

        cout << "Fibonacci(" << n << ") = " << Fibonacci_A(n) << endl;

        time(&t1);

        cout << difftime(t1, t0) << "s" << endl;

 

        cout << endl << "何か文字キーを押してから、Enterキーを押して終了。" << endl;

        char ck;

        cin >> ck;

        return 0;

}

==============================================

 

実行結果は図2のようになる。

図2

 

再帰的方法を用いた場合では9秒掛かっている処理が、計算結果を配列に保存して重複計算を避けた場合では1秒以下(図2では0秒の表示)で処理されていることがわかる。リスト2のプログラムの実行においては、の値を大きくとり過ぎると計算がなかなか終わらないので注意が必要である。

再帰的方法で処理時間が掛かるのは、同じ処理を複数回呼び出すためであるので、これは2項係数の漸化式を用いる場合でも同じである。再帰的呼び出しで重複処理がない場合は、アルゴリズムの表現の分かり易さと処理の効率化が期待できる。例えば、データを何らかの基準で並べる場合、単純なソートに比べてクイックソートは高速である。

 リスト2の関数Fibonacci_Aでは、関数が呼び出されて必要な場合、配列が用意され値が設定されるが、関数の実行が終了すると配列は廃棄される。計算結果の格納された配列を関数の実行後も保持しておくと、多数回Fibonacci数を求める場合、配列に設定されたすでに計算済みの値を利用することができる。リスト3では、関数内に配列を保持するために関数をクラス型CFibonacciとして用意し、クラス型内に配列を用意している。クラス型において演算子( )を定義すれば、関数オブジェクト(ファンクター;functor)として使用できる。リスト3では、( )

double operator()(int n)

という形式で、演算子としての( ) と、引数リストを宣言する( )と2回現れていることに注意。クラス型を

CFibonacci f;

と宣言すれば、

f(n);

で、オブジェクト f の演算子 ( ) が、n を実引数として呼び出される。演算子も関数(メソッド)の一種であることに注意。

 

=============== リスト3 ===========================

#include <iostream>

#include <time.h>               //              function time()

#include <Windows.h>            //              function QueryPerformanceFrequency/Counter()

#include <cmath>                //              function pow()

 

using namespace std;

 

double Fibonacci_A(int n) {

        double v;

        if (n <= 0) {

                v = 0.0;

        }

        else if (n == 1) {

                v = 1.0;

        }

        else {

                //      v = Fibonacci(n - 2) + Fibonacci(n - 1);

                double * F = new double[n + 1];

                F[0] = 0.0;

                F[1] = 1.0;

                for (int i = 2; i <= n; i++)

                        F[i] = F[i - 2] + F[i - 1];

                v = F[n];

                delete[] F;

        }

        return v;

}

 

class CFibonacci {

        double * F;

        int isize;

public:

        CFibonacci() {

                F = new double[1001];

                F[0] = 0.0;

                F[1] = 1.0;

                isize = 1;

        }

        virtual ~CFibonacci() {

                delete[] F;

        }

        double operator()(int n) {

                double v;

                if (n <= isize) {

                        v = F[n];

                }

                else if (n > 1000) {

                        v = -1;

                }

                else {

                        for (int i = isize + 1; i <= n; i++)

                                F[i] = F[i - 2] + F[i - 1];

                        v = F[n];

                        isize = n;

                }

                return v;

        }

};

 

class MyTime {

        LARGE_INTEGER L_I;

        int freq;

        double c;

public:

        MyTime(): c(pow(2.0, 32.0))

        {

                BOOL ck = QueryPerformanceFrequency(&L_I);

                if (ck) {

                        freq = L_I.LowPart;

        //              cout << "H = " << L_I.HighPart << "   L = " << L_I.LowPart << endl << endl;

                }

                else {

                        freq = -1;

                }

        }

        //              msec

        double operator()() {

                if (freq < 0) {

                        return -1.0;

                }

                else {

                        QueryPerformanceCounter(&L_I);

                        double v = L_I.HighPart * c + L_I.LowPart;

                        return 1000.0 * v / freq;

                }

        }

};

 

 

int main() {

        double VF[1001];

        time_t t0, t1;

        double dt0, dt1;

        MyTime mt;

        time(&t0);

        dt0 = mt();

        for (int n = 0; n <= 1000; n++)

                VF[n] = Fibonacci_A(n);

        time(&t1);

        dt1 = mt();

        for (int n = 990; n <= 1000; n++)

                cout << "Fibonacci(" << n << ") = " << VF[n] << endl;

        cout << "t1 - t0 = " << difftime(t1, t0) << "s" << endl;

        cout << "dt1 - dt0 = " << dt1 - dt0 << "ms" << endl;

 

        CFibonacci f;

        time(&t0);

        dt0 = mt();

        for (int n = 0; n <= 1000; n++)

                VF[n] = f(n);

        time(&t1);

        dt1 = mt();

        for (int n = 990; n <= 1000; n++)

                cout << "CFibonacci(" << n << ") = " << VF[n] << endl;

        cout << "t1 - t0 = " << difftime(t1, t0) << "s" << endl;

        cout << "dt1 - dt0 = " << dt1 - dt0 << "ms" << endl;

 

 

        cout << endl << "何か文字キーを押してから、Enterキーを押して終了。" << endl;

        char ck;

        cin >> ck;

        return 0;

}

==========================================

 

リスト3のプログラムを実行すると図3のようになる。

図3

 

実行時間は、関数timeによる秒単位の測定の他に、ミリ秒単位での測定のためのクラス型MyTimeを用意して、関数オブジェクトの呼び出しの形で計時している。毎回必要な配列を用意して終了時に廃棄する関数Fibonacci_Aを用いる場合、約3ミリ秒掛かっている処理が、クラス型CFibonacciの関数オブジェクトf()を用いるしょりでは約0.03ミリ秒の処理時間となっている。クラス型CFibonacciでは、n の値を1000までとしている。これは、double型でのオーバーフローを避けるためである。

なお、リスト3のプログラムは圧縮ファイルFibonacci_C.zipとして用意した。ファイル名Fibonacci_C.zipをクリックしてダウンロード解凍すれば、Visual Studio 2015で開くことができる。

 

 

Up