私は2つの異なる方法を持っています、1つは反復を使用してフィボナッチ数列をnth要素に計算し、もう1つは再帰的方法を使用して同じことをしています。
プログラムの例は次のようになります:
import Java.util.Scanner;
public class recursionVsIteration {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
//nth element input
System.out.print("Enter the last element of Fibonacci sequence: ");
int n = sc.nextInt();
//Print out iteration method
System.out.println("Fibonacci iteration:");
long start = System.currentTimeMillis();
System.out.printf("Fibonacci sequence(element at index %d) = %d \n", n, fibIteration(n));
System.out.printf("Time: %d ms\n", System.currentTimeMillis() - start);
//Print out recursive method
System.out.println("Fibonacci recursion:");
start = System.currentTimeMillis();
System.out.printf("Fibonacci sequence(element at index %d) = %d \n", n, fibRecursion(n));
System.out.printf("Time: %d ms\n", System.currentTimeMillis() - start);
}
//Iteration method
static int fibIteration(int n) {
int x = 0, y = 1, z = 1;
for (int i = 0; i < n; i++) {
x = y;
y = z;
z = x + y;
}
return x;
}
//Recursive method
static int fibRecursion(int n) {
if ((n == 1) || (n == 0)) {
return n;
}
return fibRecursion(n - 1) + fibRecursion(n - 2);
}
}
どちらの方法が速いかを見つけようとしていました。数値が小さいほど再帰は速くなるという結論に達しましたが、nth要素の値が大きくなると、再帰が遅くなり、反復が速くなります。 3つの異なるnの3つの異なる結果を次に示します。
例#1(n = 10)
Enter the last element of Fibonacci sequence: 10
Fibonacci iteration:
Fibonacci sequence(element at index 10) = 55
Time: 5 ms
Fibonacci recursion:
Fibonacci sequence(element at index 10) = 55
Time: 0 ms
例#2(n = 20)
Enter the last element of Fibonacci sequence: 20
Fibonacci iteration:
Fibonacci sequence(element at index 20) = 6765
Time: 4 ms
Fibonacci recursion:
Fibonacci sequence(element at index 20) = 6765
Time: 2 ms
例#3(n = 30)
Enter the last element of Fibonacci sequence: 30
Fibonacci iteration:
Fibonacci sequence(element at index 30) = 832040
Time: 4 ms
Fibonacci recursion:
Fibonacci sequence(element at index 30) = 832040
Time: 15 ms
私が本当に知りたいのは、突然の反復がすべて速くなり、再帰が遅くなった理由です。この質問に対する明白な答えを逃した場合は申し訳ありませんが、私はプログラミングにまだ慣れていないので、その背後で何が起こっているのか本当に理解できません。答えを自分で見つけられるように、適切な説明を入力するか、正しい方向に向けてください。また、これがどのメソッドが高速かをテストするのに適した方法ではない場合、別のメソッドを教えてください。
ありがとうございます!
簡潔にするために、F(x)を再帰フィボナッチにしましょう
F(10) = F(9) + F(8)
F(10) = F(8) + F(7) + F(7) + F(6)
F(10) = F(7) + F(6) + F(6) + F(5) + 4 more calls.
....
だからあなたはF(8)を2回、F(7) 3回、F(6) 5回を呼び出している、F(5) 7回..など
入力が大きくなると、ツリーはますます大きくなります。
フィボナッチアルゴリズムの再帰的な実装を行う場合、同じ値を繰り返し再計算することで冗長な呼び出しを追加しています。
_fib(5) = fib(4) + fib(3)
fib(4) = fib(3) + fib(2)
fib(3) = fib(2) + fib(1)
_
fib(2)
とfib(4)
の両方について、fib(3)
が重複して計算されることに注意してください。しかし、これは メモ化 と呼ばれる手法で克服できます。これは、一度計算した値を保存することにより、再帰フィボナッチの効率を改善します。既知の値に対するfib(x)
のさらなる呼び出しは、単純なルックアップによって置き換えられ、さらなる再帰呼び出しの必要性を排除します。
これは、反復アプローチと再帰アプローチの主な違いです。興味がある場合は、フィボナッチ数を計算する他の、より多くの 効率的なアルゴリズム もあります。
なぜ再帰が遅いのですか?
関数を(再帰として)再度呼び出すと、コンパイラはその新しい関数に新しいアクティベーションレコード(通常のスタックと考えてください)を割り当てます。そのスタックは、状態、変数、およびアドレスを保持するために使用されます。コンパイラーは関数ごとにスタックを作成し、この作成プロセスは基本ケースに達するまで続きます。したがって、データサイズが大きくなると、コンパイラはプロセス全体を計算するために大きなスタックセグメントを必要とします。これらのレコードの計算と管理も、このプロセス中にカウントされます。
また、再帰では、スタックセグメントは実行中に発生です。コンパイラはコンパイル時に占有されるメモリ量を知りません。
ベースケースを適切に処理しないと、StackOverflow exception :)が発生します。
配列を使用した反復関数に興味がある場合:
public static void fibonacci(int y)
{
int[] a = new int[y+1];
a[0] = 0;
a[1] = 1;
System.out.println("Step 0: 0");
System.out.println("Step 1: 1");
for(int i=2; i<=y; i++){
a[i] = a[i-1] + a[i-2];
System.out.println("Step "+i+": "+a[i]);
}
System.out.println("Array size --> "+a.length);
}
このソリューションは、入力値0
に対してクラッシュします。
理由:配列aは0+1=1
で初期化されますが、a[1]
を連続して割り当てると、インデックスが範囲外の例外になります。
0
でy=0
を返すifステートメントを追加するか、y+2
で配列を初期化します。これにより、1
intは無駄になりますが、大きなO
は変更されません。
特定のアルゴリズムを完了するのにかかる時間を探しているときはいつでも、常に時間の複雑さのために行くことが最善です。
O(何か)の観点から、論文の時間の複雑さを評価します。
上記の2つのアプローチを比較すると、反復アプローチの時間の複雑さはO(n)です。一方、再帰アプローチの時間の複雑さはO(2 ^ n)です。
fib(4)
の時間の複雑さを見つけてみましょう
反復アプローチ、ループは4回評価されるため、時間の複雑さはO(n)
です。
再帰的アプローチ、
fib(4)
fib(3) + fib(2)
fib(2) + fib(1) fib(1) + fib(0)
fib(1) + fib(0)
したがって、fib()は9回呼び出され、nの値が大きい場合は2 ^ nよりわずかに低く、小さい場合でも(BigOh
(O)がupper bound
を処理することに注意してください).
結果として、反復アプローチはpolynomial time
で評価しているのに対し、再帰アプローチはexponential time
で評価していると言えます。
再帰を使用するのと同じように、時間の複雑さはO(fib(n))
であり、非常に高価です。反復メソッドはO(n)
です。a)テストが非常に短く、コードもコンパイルされないb)非常に小さな数を使用したため、これは表示されません。
両方の例を実行すると、より高速になります。ループまたはメソッドが10,000回呼び出されたら、ネイティブコードにコンパイルする必要があります。
使用する再帰的なアプローチは効率的ではありません。末尾再帰を使用することをお勧めします。アプローチとは対照的に、末尾再帰では、スタック内の任意の時点で1つの関数呼び出しのみが保持されます。
public static int tailFib(int n) {
if (n <= 1) {
return n;
}
return tailFib(0, 1, n);
}
private static int tailFib(int a, int b, int count) {
if(count <= 0) {
return a;
}
return tailFib(b, a+b, count-1);
}
public static void main(String[] args) throws Exception{
for (int i = 0; i <10; i++){
System.out.println(tailFib(i));
}
}
ゴールデンナンバーを使用した数学的ソリューションを使用することを好みます。楽しい
private static final double GOLDEN_NUMBER = 1.618d;
public long fibonacci(int n) {
double sqrt = Math.sqrt(5);
double result = Math.pow(GOLDEN_NUMBER, n);
result = result - Math.pow(1d - GOLDEN_NUMBER, n);
result = Math.round(result / sqrt);
return Double.valueOf(result).longValue();
}
さらに不必要な計算を避けるために、計算された値が保存される再帰的な解決策があります。コードは次のとおりです。
public static int fibonacci(int n) {
if(n <= 0) return 0;
if(n == 1) return 1;
int[] arr = new int[n+1];
// this is faster than using Array
// List<Integer> lis = new ArrayList<>(Collections.nCopies(n+1, 0));
arr[0] = 0;
arr[1] = 1;
return fiboHelper(n, arr);
}
public static int fiboHelper(int n, int[] arr){
if(n <= 0) {
return arr[0];
}
else if(n == 1) {
return arr[1];
}
else {
if( arr[n-1] != 0 && (arr[n-2] != 0 || (arr[n-2] == 0 && n-2 == 0))){
return arr[n] = arr[n-1] + arr[n-2];
}
else if (arr[n-1] == 0 && arr[n-2] != 0 ){
return arr[n] = fiboHelper(n-1, arr) + arr[n-2];
}
else {
return arr[n] = fiboHelper(n-2, arr) + fiboHelper(n-1, arr );
}
}
}