-
前言
笔记是笔者个人理解,难免有误(尤其是关于算法复杂度分析的部分),还望各位大佬不吝赐教!欢迎讨论交流!
还有一些胡言乱语,纯笔者喝酒喝出来的胡话,权博看官们一乐,慎用、勿当真!
因为个人原因,没办法及时回复消息,望佬们海涵!
基本用伪代码来体现算法思路,没办法兼顾各种语言,直接copy不能用,靠各位大佬自己动手小修小改一下了!
Fibonacci数列:1,1,2,3,5,8,13,21,...
规律:
F[0] = F[1] = 1;
F[n] = F[n-1] + F[n-2],n>=2;
-
思路
乍一看到规律“F[n] = F[n-1] + F[n-2],n>=2”,很容易递归上头,不过递归也分情况好坏:老老实实地做递归就很暴力,一点都不优雅,但这也是最容易被想到的方法;还可以借鉴动态规划的备忘录思想,做记忆递归,即每个元素只计算一遍便足够确定值了,之后再需要用到该元素的时候直接用,不必再计算。
那么类似的,既然能记忆递归,那也可以直接做动态规划。
再看上面那条规律,一遍一遍地重复滚雪球,这不就是迭代的感觉么?
综上,暂时有了四种思路来解决Fibonacci数列问题:
- 普通递归
- 记忆递归
- 动态规划
- 迭代
1. 普通递归
来个函数,就叫Fibonacci吧。有一个整型形参k,表示要求斐波那契数列的第k个元素的值。所以我们定义的这个Fibonacci函数得返回一个整型,那按照递归的一般形式,肯定要调用自个儿,所以return内容那里就先写自个儿吧,传什么参数给它待会儿再说。先有了个大致雏形:
int Fibonacci(int k)
{
//等待添加代码
return Fibonacci(???);
}
那根据规律“F[n] = F[n-1] + F[n-2]”,代入到我们的Fibonacci函数来,就是说Fibonacci(k)=Fibonacci(k-1)+Fibonacci(k-2)呗。所以return内容可以确定下来了:
int Fibonacci(int k)
{
//等待添加代码
return Fibonacci(k-1)+Fibonacci(k-2);
}
递归函数的一般形式中,还要有一个用于结束递归的判断语句,这里就是k=0或k=1的情况:
int Fibonacci(int k)
{
if(k==0||k==1)
{
return 1;
}
return Fibonacci(k-1)+Fibonacci(k-2);
}
2. 记忆递归
涉及到“记忆”,那就要准备一个数组作为“备忘录”记录已经计算过值的元素,比如下面的动态一维数组F。
//数据准备动作:
int n=...;
int *F=new int[n];
//初始化动作:
for(int i=0;i<n;i++)
{
F[i]=0; //都赋0初值
}
int Fibonacci(int k)
{
//等待添加点睛之笔
if(k==0||k==1)
{
F[k]=1;
}else{
F[k]=Fibonacci[k-1]+Fibonacci[k-2];
}
return F[k];
}
至此,这段代码仍然是光记忆但没用到记忆的内容,让我们来为它添上这点睛之笔:
if(F[k]>0) //看到这个判断条件,明白之前赋初值的时候为啥要赋0了吧?
{
return F[k];
}
3. 动态规划(DP)
思想基本和记忆递归类似,每个元素最多只被计算一次,上码:
int Fibonacci(int k)
{
int *F=new F[k];
F[0]=F[1]=1;
for(int i=2;i<=k;i++)
{F[i]=F[i-1]+F[i-2];}
return F[n];
}
但是,这个有缺陷,因为它是作为子函数等待被其他函数体调用的,每个元素最多只被计算一次是针对当次调用执行而言的;而每被调用一次,F数组就需要计算一遍,还是重复计算了,浪费。改进后可以避免此种情况,哪怕子函数Fibonacci(int k)被多次调用,F数组从头到尾每个元素最多也是只被计算一次,码如下:
//全局变量:
int n=...;
int *F=new F[n];
//初始化动作:
for(int i=0;i<n;i++)
{F[i]=0;}
int Fibonacci(int k)
{
F[0]=F[1]=1;
int j=0;
if(F[k]>0)
{return F[k];}
while(F[j]>0){
j++;
} //j跳出循环后,j的值即为F数组当前尚未被计算过的首个元素的位置
for(int i=j;i<=k;i++)
{F[i]=F[i-1]+F[i-2];}
return F[k];
}
但是——虽然避免了重复计算,但是又引入了循环判断TVT。那再改进一下吧,把循环判断去掉,用一个全局变量索引index来定位F数组当前尚未被计算过的首个元素的位置,码如下:
//全局变量:
int n=...;
int *F=new F[n];
int index=2;
//初始化动作:
for(int i=0;i<n;i++)
{F[i]=0;}
F[0]=F[1]=1; //这个也提出来,能少做一次计算也好,节俭点肯定没错,跟过日子一个样嘛嘿嘿。
int Fibonacci(int k)
{
if(F[k]>0) //判断条件里也可以写index>k,由算法来看和F[k]>0是等价的
{return F[k];}
for(int i=index;i<=k;i++)
{
F[i]=F[i-1]+F[i-2];
index=i;
}
return F[k];
}
这样应该是解决上面意外引入的问题了。这个案例是不是有点取舍的味道?多花费一点内存空间来存放全局变量F[0]和F[1],能节省很多时间复杂度,有舍就有得,“舍得”二字,大概是能量守恒定律的诗意化的说法吧,谁能想到,科学和国语有一天也能学科交叉?挺妙的。
4. 迭代
这典型的“用空间换时间”,跟上面一样(作为子函数被调用一次就要重复计算一次),只需要分配三个整型变量的空间给Fn、Fn1、Fn2,然后就撒手不管让他仨滚雪球去,迭来迭去 ,颇有一股不顾人死活的劲头。码如下:
int Fibonacci(int k)
{
if(k==0||k==1)
{return 1;}
int Fn=0,Fn1=1,Fn2=1;
for(int i=3;i<=n;i++)
{
Fn=Fn2+Fn1;
Fn1=Fn2;
Fn2=Fn;
}
return Fn;
}