斐波那契计算时间
Fibonacci Computation Time
递归式 Fibonacci 与循环式 Fibonacci 之间是否存在明显的计算时间差异?我使用递归将 运行 Fibonacci 保持在 40 个位置,然后直接使用循环。好像计算时间差只有academic.
用C语言编写
递归求解:
int main(int argc, const char * argv[]) {
int n, i = 0, c;
printf("Please enter an integer:\n");
scanf("%d", &n);
for ( c = 1 ; c <= n ; c++ )
{
printf("%lu ", fibonacci(i));
i++;
}
return 0;
}
long fibonacci(long n)
{
if ( n == 0 )
return 0;
else if ( n == 1 )
return 1;
else
return ( fibonacci(n-1) + fibonacci(n-2) );
};
For循环解决方案:
int main(int argc, const char * argv[]) {
int n, first = 0, second = 1, next, c;
printf("Please enter an integer:\n");
scanf("%d", &n);
for ( c = 0 ; c < n ; c++ )
{
if ( c <= 1 )
next = c;
else
{
next = first + second;
first = second;
second = next;
}
printf("%d ",next);
}
return 0;
};
For 循环解决方案更快。原因:
- 没有函数调用(假设函数调用很昂贵)
- 计算第
n
次使用 n
次加法(循环迭代 n
次),而递归解决方案每次函数调用使用一次加法,总和为 O(1.6
n
)
调用,因此 O(1.6
n
)
添加。成本来自双重递归调用——当递归函数请求第 n
个元素时,它必须从头开始再次计算第 n-1
个和第 n-2
个元素,它不记得了。
一个for循环不一定更快。在 Java、C 和 Python 等通用语言中,与迭代相比,递归的开销相当大,因为它需要分配新的堆栈帧。
可以在 C/C++ 中消除这种开销,使编译器优化能够执行尾递归,将某些类型的递归(实际上是某些类型的尾调用)转换为跳转而不是函数调用。为了让编译器执行此优化,一个函数在它之前做的最后一件事 returns 是调用另一个函数(在本例中是它自己)是必要的。
斐波那契函数的一个例子可能是这样的:
int fib_tail(int n, int res, int next)
{
if (n == 0) {
return res;
}
return fib_tail(n - 1, next, res + next);
}
并且在汇编级别,启用编译器优化,它将作为循环实现,例如在调用之间共享相同的堆栈帧。
我最近写了一篇关于它的 article。
希望对您有所帮助。
与尾递归和迭代版本相比,传统的递归方法非常慢。在下面的迭代版本示例代码中,使用展开循环和 Duff's Device 进入循环。对于 32 位无符号整数,限制为 fib(47),对于 64 位无符号整数,限制为 fib(93)。
计时是使用 Intel 2600K 3.4ghz、XP X64、64 位模式完成的。 XP 或 XP X64 高性能计数器频率与 cpu 时钟相同,3.4ghz,但操作系统开销(如中断),如果持续时间较小,会影响时序。
fib(40) 的时间:
fibr() # of microseconds 485468.8
fibt() # of microseconds 0.2
fibi() # of microseconds 0.2
94 次循环的计时,n = 0 到 93:
fibt() # of microseconds 7
fibi() # of microseconds 5
示例代码:
typedef unsigned long long UI64;
UI64 fibr(UI64 n)
{
if(n < 2)
return n;
return fibr(n-1) + fibr(n-2);
}
// call with fibt(n, 0, 1)
UI64 fibt(UI64 n, UI64 res, UI64 next)
{
if (n == 0)
return res;
return fibt(n - 1, next, res + next);
}
UI64 fibi(UI64 n)
{
UI64 f0, f1, i;
if(n < 2)
return n;
n -= 2;
f1 = f0 = 1;
i = 0;
switch(n%8){
do{
f1 += f0;
case 7:
f0 += f1;
case 6:
f1 += f0;
case 5:
f0 += f1;
case 4:
f1 += f0;
case 3:
f0 += f1;
case 2:
f1 += f0;
case 1:
f0 += f1;
case 0:
continue;
}while(n >= (i += 8));
}
return f0;
}
fibi() 的替代版本,没有 n<2 检查。 f0 和 f1 代表循环中的变化,旨在以 f0 中的最终和结束,因此 f0 和 f1 代表的初始状态取决于 n 是偶数还是奇数。如果 n 是偶数,则 f0 = fib(0) = 0, f1 = fib(-1) = 1,如果 n 是奇数,则 f1 = fib(0) = 0, f0 = fib(-1) = 1 。 (如果你好奇的话,fib(-1) = 1, fib(-2) = -1, fib(-3) = 2, fib(-4) = -3, fib(-5) = 5, fib(-6) = -8, ... ).
解释一下这里的逻辑,对于n偶数情况,fib(-1) = f1 = 1, fib(0) = f0 = 0, 那么fib(1) = (f1 += f0), fib (2) = (f0 += f1), fib(3) = (f1 += f0), fib(4) = (f0 += f1), ...
UI64 fibi(UI64 n)
{
UI64 f0, f1, i;
f0 = n & 1; // if n even, f0=0, f1=1
f1 = 1 - f0; // else f1=0, f0=1
i = 0;
switch(n%8){
do{
f1 += f0;
case 7:
f0 += f1;
case 6:
f1 += f0;
case 5:
f0 += f1;
case 4:
f1 += f0;
case 3:
f0 += f1;
case 2:
f1 += f0;
case 1:
f0 += f1;
case 0:
continue;
}while(n >= (i += 8));
}
return f0;
}
你是如何测量速度差异的?
Fibonacci 函数的朴素递归实现需要大约 1 亿次函数调用来计算 f (40)。在速度足够快的现代计算机上,您无法用秒表计时。
计算 f (50) 需要大约 100 亿次函数调用,这将是一个明显的延迟。 f (60) 接管了一万亿次函数调用,或大约一个小时。 f (70) 需要大约 200 万亿次函数调用或几天时间。 f (80) 需要大约 20 千万亿次函数调用或大约一年。
我不会将这种差异称为学术差异。
也许下面的方法会花费更少的时间?
您可以编写生成斐波那契数列的代码,避免打印零和一的 if-else 语句,并避免在循环外打印它们。您可以通过使用 -1 和 1 初始化 'first' 和 'second' 变量来实现,因此它们之间的总和将为您提供 0,这是该系列的第一个器官,循环将执行剩下的工作。
#include <iostream>
using namespace std;
int main()
{
int i, num, a = -1, b = 1, temp;
cout << "enter a number:" << endl;
cin >> num;
for ( i = 0 ; i < num + 1 ; i++ )
{
cout << a + b << " ";
temp = a + b;
a = b;
b = temp;
}
cout << endl;
return 0;
}
递归式 Fibonacci 与循环式 Fibonacci 之间是否存在明显的计算时间差异?我使用递归将 运行 Fibonacci 保持在 40 个位置,然后直接使用循环。好像计算时间差只有academic.
用C语言编写
递归求解:
int main(int argc, const char * argv[]) {
int n, i = 0, c;
printf("Please enter an integer:\n");
scanf("%d", &n);
for ( c = 1 ; c <= n ; c++ )
{
printf("%lu ", fibonacci(i));
i++;
}
return 0;
}
long fibonacci(long n)
{
if ( n == 0 )
return 0;
else if ( n == 1 )
return 1;
else
return ( fibonacci(n-1) + fibonacci(n-2) );
};
For循环解决方案:
int main(int argc, const char * argv[]) {
int n, first = 0, second = 1, next, c;
printf("Please enter an integer:\n");
scanf("%d", &n);
for ( c = 0 ; c < n ; c++ )
{
if ( c <= 1 )
next = c;
else
{
next = first + second;
first = second;
second = next;
}
printf("%d ",next);
}
return 0;
};
For 循环解决方案更快。原因:
- 没有函数调用(假设函数调用很昂贵)
- 计算第
n
次使用n
次加法(循环迭代n
次),而递归解决方案每次函数调用使用一次加法,总和为O(1.6
n
)
调用,因此O(1.6
n
)
添加。成本来自双重递归调用——当递归函数请求第n
个元素时,它必须从头开始再次计算第n-1
个和第n-2
个元素,它不记得了。
一个for循环不一定更快。在 Java、C 和 Python 等通用语言中,与迭代相比,递归的开销相当大,因为它需要分配新的堆栈帧。
可以在 C/C++ 中消除这种开销,使编译器优化能够执行尾递归,将某些类型的递归(实际上是某些类型的尾调用)转换为跳转而不是函数调用。为了让编译器执行此优化,一个函数在它之前做的最后一件事 returns 是调用另一个函数(在本例中是它自己)是必要的。
斐波那契函数的一个例子可能是这样的:
int fib_tail(int n, int res, int next)
{
if (n == 0) {
return res;
}
return fib_tail(n - 1, next, res + next);
}
并且在汇编级别,启用编译器优化,它将作为循环实现,例如在调用之间共享相同的堆栈帧。
我最近写了一篇关于它的 article。
希望对您有所帮助。
与尾递归和迭代版本相比,传统的递归方法非常慢。在下面的迭代版本示例代码中,使用展开循环和 Duff's Device 进入循环。对于 32 位无符号整数,限制为 fib(47),对于 64 位无符号整数,限制为 fib(93)。
计时是使用 Intel 2600K 3.4ghz、XP X64、64 位模式完成的。 XP 或 XP X64 高性能计数器频率与 cpu 时钟相同,3.4ghz,但操作系统开销(如中断),如果持续时间较小,会影响时序。
fib(40) 的时间:
fibr() # of microseconds 485468.8
fibt() # of microseconds 0.2
fibi() # of microseconds 0.2
94 次循环的计时,n = 0 到 93:
fibt() # of microseconds 7
fibi() # of microseconds 5
示例代码:
typedef unsigned long long UI64;
UI64 fibr(UI64 n)
{
if(n < 2)
return n;
return fibr(n-1) + fibr(n-2);
}
// call with fibt(n, 0, 1)
UI64 fibt(UI64 n, UI64 res, UI64 next)
{
if (n == 0)
return res;
return fibt(n - 1, next, res + next);
}
UI64 fibi(UI64 n)
{
UI64 f0, f1, i;
if(n < 2)
return n;
n -= 2;
f1 = f0 = 1;
i = 0;
switch(n%8){
do{
f1 += f0;
case 7:
f0 += f1;
case 6:
f1 += f0;
case 5:
f0 += f1;
case 4:
f1 += f0;
case 3:
f0 += f1;
case 2:
f1 += f0;
case 1:
f0 += f1;
case 0:
continue;
}while(n >= (i += 8));
}
return f0;
}
fibi() 的替代版本,没有 n<2 检查。 f0 和 f1 代表循环中的变化,旨在以 f0 中的最终和结束,因此 f0 和 f1 代表的初始状态取决于 n 是偶数还是奇数。如果 n 是偶数,则 f0 = fib(0) = 0, f1 = fib(-1) = 1,如果 n 是奇数,则 f1 = fib(0) = 0, f0 = fib(-1) = 1 。 (如果你好奇的话,fib(-1) = 1, fib(-2) = -1, fib(-3) = 2, fib(-4) = -3, fib(-5) = 5, fib(-6) = -8, ... ).
解释一下这里的逻辑,对于n偶数情况,fib(-1) = f1 = 1, fib(0) = f0 = 0, 那么fib(1) = (f1 += f0), fib (2) = (f0 += f1), fib(3) = (f1 += f0), fib(4) = (f0 += f1), ...
UI64 fibi(UI64 n)
{
UI64 f0, f1, i;
f0 = n & 1; // if n even, f0=0, f1=1
f1 = 1 - f0; // else f1=0, f0=1
i = 0;
switch(n%8){
do{
f1 += f0;
case 7:
f0 += f1;
case 6:
f1 += f0;
case 5:
f0 += f1;
case 4:
f1 += f0;
case 3:
f0 += f1;
case 2:
f1 += f0;
case 1:
f0 += f1;
case 0:
continue;
}while(n >= (i += 8));
}
return f0;
}
你是如何测量速度差异的?
Fibonacci 函数的朴素递归实现需要大约 1 亿次函数调用来计算 f (40)。在速度足够快的现代计算机上,您无法用秒表计时。
计算 f (50) 需要大约 100 亿次函数调用,这将是一个明显的延迟。 f (60) 接管了一万亿次函数调用,或大约一个小时。 f (70) 需要大约 200 万亿次函数调用或几天时间。 f (80) 需要大约 20 千万亿次函数调用或大约一年。
我不会将这种差异称为学术差异。
也许下面的方法会花费更少的时间? 您可以编写生成斐波那契数列的代码,避免打印零和一的 if-else 语句,并避免在循环外打印它们。您可以通过使用 -1 和 1 初始化 'first' 和 'second' 变量来实现,因此它们之间的总和将为您提供 0,这是该系列的第一个器官,循环将执行剩下的工作。
#include <iostream>
using namespace std;
int main()
{
int i, num, a = -1, b = 1, temp;
cout << "enter a number:" << endl;
cin >> num;
for ( i = 0 ; i < num + 1 ; i++ )
{
cout << a + b << " ";
temp = a + b;
a = b;
b = temp;
}
cout << endl;
return 0;
}