あなたは、再帰関数は「新しい変数を作成せず、操作も少ないため、時間がかからないはずだ」と書いています。最初の主張はかなり無意味です。ローカル変数のメモリは、通常、関数に入ったときに 1 回の減算操作によって割り当てられます。これには、わずかな時間がかかります (人間が知る最速の割り当てです)。2 番目のアサーションは、C++ 実装に対して単純に false です。コンパイラで再帰関数が遅いことを測定したので、それはそれ以下ではなく、より多くのことを行います。
さて、なぜ。
各呼び出しは、戻りアドレスと実際の引数をスタックにコピーする必要があります。これには時間がかかります。さらに、デバッグと例外をサポートするために、通常、各関数呼び出しは、呼び出し前のスタックの状態に関する情報を本質的に格納する、niceスタック フレームを確立する追加の作業を行います。
ただし、再帰的なバリアントは遅くする必要はありません。しかし、ほとんど逆説的ですが、実際には反復バージョンと同じくらい高速なバリアントは、より多くのことを行うように見えます... アイデアは、コンパイラがそれを反復バージョンに変換できるように、つまり、コンパイラができるように書くことです。再帰呼び出し (時間がかかる) を単純なループに置き換えます。
唯一の問題は、私の知る限り、この種の最適化を行う C++ コンパイラはほとんどないということです。:-(
ただし、完全を期すために、再帰呼び出しが 1 つだけであること、およびそれが最後に発生することを確認することが考えられます。これは末尾再帰と呼ばれます。現在の再帰コード、
double factorial( int n )
{
if( n < 2 ) { return 1; }
return n*factorial( n-1 );
}
再帰呼び出しの後に による乗算があるため、末尾再帰ではありませんn
。
末尾再帰にするために、最後に実行する必要があることを実行するために必要な情報を引数として渡すことができます*n
。そのために必要な情報は、 の値とn
、それを実行する必要があるという事実です。これは、適切な仮引数を持つヘルパー関数を導入することを意味します。
double factorialScaledBy( double m, int n )
{
if( n < 2 ) { return m*1; }
// Same as "n*factorialScaledBy( m, n-1 )", but tail-recursive:
return factorialScaledBy( n*m, n-1 );
}
double factorial( int n )
{
return factorialScaledBy( 1, n );
}
これで、十分にスマートなコンパイラは、再帰呼び出し後の関数実行で何も起こらないことに気付くことができます。したがって、ローカル変数は使用されず、再帰呼び出しに再利用できます。したがって、シミュレートされた引数の受け渡しと関数の先頭、つまりループに戻ります。
乾杯 & hth.,