许多程序设计教学书上都用斐波那契数列(数列中第一二项都是1,其它任意一项都是其前两项之和)作为讲解递归的例子,作为教学例子,它确实十分合适,但是如果用在实际计算中,那么递归实现的斐波那契数列求值实在是太慢了,其中主要的原因是重复计算太多,这样的递归算法不仅速度效率低下,还容易造成栈溢出。如果能够保留下已经计算过的值,但需要时直接取用而不是重复计算,那么必然会提高程序性能。
对于斐波那契数列求解使用一个一维数组来保存之前的计算值,是十分合适的:
#include "stdafx.h"
#include <iostream>
using namespace std;
//递归解法
double Fibonacci(int n)
{
if(n < 0) return -1;
if(n == 0 || n == 1) return 1;
//重复计算:比如计算Fibonacci(n - 1)时,就会计算
//Fibonacci(n - 2)
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
//利用一维数组的解法
double Fibonacci_a(int n)
{
if(n < 0) return -1;
if(n == 0 || n == 1) return 1;
int m = n + 1;
double *fibo = new double[m];
fibo[0] = fibo[1] = 1;
for (int i = 2; i < m; ++i)
{
fibo[i] = fibo[i - 1] + fibo[i - 2];
}
return fibo[n];
}
上述程序中,当n为50的时候程序运行递归版就已经十分缓慢了,而数组版当n达到我的系统能接受的最大值1475时,在觉察不到的等待时间内就得出了答案。而内存占用无非就是n加一些必须的字节而已。
这个解法让我想起了刚开始接触编程那会学院里组织的一次编程比赛,其中有一个就是需要利用数组加速递归的,那个题目已经忘记了,但是凭记得的大概也可以构造出一个来(不确定是不是原题目,但是思路是一样的),当时我提交的是一段投机取巧的解法,也许对于给定的输入可以给出答案,但是其他的就没有保证了。题目大概如下:
输入m、n、p三个非负整数,计算f(m,n,p)的值,其中,当m、n、p都不为0时,f(m,n,p) = f(m - 1,n,p) + f(m,n - 1,p) + f(m,n,p - 1),如果m、n、p中任一个为0时,f(m,n,p) = 1。按照定义使用递归可以很容易地得出解法,但是当m、n、p中任意一个数达到10的时候,程序运行起来就十分缓慢,原因就是重复计算实在太多,递归分支以指数级别增加(以3为底),所以必然不能用递归的方式求值。比赛完了之后,出题人(同班同学)问我用什么方式实现的,我说是假的,他提示我应该用一个三维数组,当时也没反应过来,不知道怎么用,这段时间开始学算法(之前都学了一些花架子,悔不当初啊),总算反应过来,所以实现了一个利用三维数组替换递归的方法,解题思路和斐波那契数列是一摸一样的,只是在计算数组元素位置、初始化记录数组的时候有一点点不一样而已(主要是因为在c或C++中三维数组与一维数组的对应关系是由程序员编写造成的,否则哪那么多事),多余的就不说了,直接上代码:
//递归版
double f(int i,int j, int k)
{
if(i < 0 || j < 0 || k < 0) return -1;
if(i == 0 || j == 0 || k == 0) return 1;
return f(i - 1,j,k) + f(i,j - 1,k) + f(i,j,k - 1);
}
//三维数组版
double f_a(int m,int n, int p)
{
if(m < 0 || n < 0 || p < 0) return -1;
if(m == 0 || n == 0 || p == 0) return 1;
m++;
n++;
p++;
double *f_array = new double[m*n*p];
memset(f_array,0,m*n*p*sizeof(double));
for (int j = 0; j < n; ++j)
{
for (int k = 0; k < p; ++k)
{
f_array[j*p + k] = 1;
}
}
for (int i = 0; i < m; ++i)
{
for (int k = 0; k < p; ++k)
{
f_array[i*n*p + k] = 1;
}
}
for (int i = 0; i < m; ++i)
{
for (int j = 0; j < p; ++j)
{
f_array[(i*n + j)*p] = 1;
}
}
//这个三重循环不是按行、列、“厚度”的顺序,而是倒过来的
for (int k = 1; k < p; ++k)
{
for (int j = 1; j < n; ++j)
{
for (int i = 1; i < m; ++i)
{
f_array[(i*n + j)*p + k] = f_array[((i-1)*n + j)*p + k] + f_array[(i*n + j - 1)*p + k] + f_array[(i*n + j)*p + k - 1];
}
}
}
return f_array[m* n * p - 1];
}
int _tmain(int argc, _TCHAR* argv[])
{
int n = 100;
cout<<f_a(n,n,n)<<endl;
return 0;
}
当n取100时基本上也是立马出结果,当n取到200时,需要等待1s左右,这时候大量的计算不是花在真正的加法上,而是花在了数据的读取(包括位置计算、指针移动),计算出来的数也是快到10的300次方了(当输入较小时,我对比了两个版本的结果,所以数组版的程序还是比较可信,我只保证理论上是完全正确的,数比较大时,我没有提供任何看得到的证据)。
用《数据结构与算法分析:C语言描述》第一章里的对于递归算法设计的准则作为结语吧:
1)基准情形。必须总有某些基准情形,它无需递归就能解出。
2)不断推进。对于那些需要递归求解的情形,每一次递归调用都必须要使求解状况朝接近基准情形的方向推进。
3)设计法则。假设所有的递归调用都能运行。
4)合成效益法则。在求解一个问题的同一实例时,切勿在不同的递归调用中做重复的工作。
很明显上面两个问题的递归解法都违背了第四条准则。