从零开始学算法。
首先说说递推吧,举一个耳熟能详的例子,就是有关兔子的计划生育问题:兔子的生育能力很强啊,一对兔子出生两个月后就能生一对兔子,并且以后每月都能再生一对。简单点说,就是新兔子要出生隔月后才能生(发育得很快嘛),那么现在想想,如果所有兔子都长命百岁,一年后有多少对兔子呢?
经过月数:---0---1---2---3---4---5---6---7---8---9--10--11--12
兔子对数:---1---1---2---3---5---8--13--21--34--55--89-144-233
也就是说,这个月的兔子对数为:上个月的兔子对数+它们此月所生的小兔子对数。那么它们此月到底生了多少对小兔子呢?很明显,刚刚我们分析过,在上个月的兔子中,只有已经出生一月以上的老兔子才能生,其实就是上上个月的兔子对数,且每对生一对。于是就有了:本月兔子=上月兔子+上上月兔子。
原来就是Fibonacci数列(其实人家本来就叫兔子数列)。
F(1)=F(2)=1 (n=1对应上面的第0月)
F(n)=F(n-1)+F(n-2) (n≥3)
其实这是一个线性递推数列。那么我们如何写一个程序来求F(n)呢,相信用递归再简单不过了,看看下面的程序:
1. //程序1:Fibonacci数列的递归实现 Code in C/C++
2. 3. int Fibonacci(int
4. {
5. if(n < 3)
6. return
7. else
8. return
9. }
OK,搞定,轻松解决!
慢点,跑跑试一试,n=12,OK! n=40,还行。n=50,够慢了吧!
怎么会这么慢呢?我们来仔细分析分析。看看这句:
- return
注意了,Fibonacci(n-1) 这个递归调用其实将会产生:Fibonacci(n-2)+Fibonacci(n-3) (当然n大于3的时候)。看出问题了吧,对于上面这个Fibonacci(n-1)+Fibonacci(n-2),其实在展开之后我们调用了两次Fibonacci(n-2),也就是说我们重复计算了两次F(n-2),这还仅仅是第一次的情况,随着计算的深入,我们还会重复计算更多,请看下图:
自底向上计算F(1)到F(4)就行了。然而递归的时候却自顶向下作了大量的重复计算,当然慢了!
现在清楚了问题之后,我们再来换种方法,在计算过程中将已经计算过的F(n)保存起来,下次就不用重复计算了,看下面程序。
1. //程序2:自底向上求Fibonacci数列 Code in C/C++
2. int
3. 4. int Fibonacci(int
5. {
6. if(n < 3)
7. return
8. else
9. {
10. F[n] = (F[n-1]>0 ? F[n-1]: Fibonacci(n-1))
11. + (F[n-2]>0 ? F[n-2]: Fibonacci(n-2));
12. return
13. }
14. }
好,现在再来算,快多了吧!其实就是个动态规划的思想,用空间换时间。
行了,OK! 给我来个F(999)!
。。。果然够快。。。但是。。。结果却是错的,负值都出来了。
当然会错了,一个Int最多也就32位(看操作系统),那么换成long long,64位,暂时够用了,但是怎么输出任意大的F(n)呢,这个。。。咱们留到以后专题讨论“大数”,先做个预告,打打广告。
排除溢出的问题,上面的程序还有什么问题吗?
。。。
当然有,想一想哈,假设现在我们现在解决了溢出的问题,那么当第一次调用这个函数时输入一个很大的n时会出现什么情况呢?
注意,现在是第一次调用,也就是说我们的缓存数组F[n]里面是空的,这个时候函数会出现多少次递归调用呢?(n-1)-2次! 也就是说n-3次,当n很大时,这个开销还是不小的。
为什么呢?因为函数的调用时有开销的。当一个函数调用时,需要将函数参数入栈,并且保存当前程序上下文信息(也就是储存调用点的地址等等),还有参数传递的开销(传递一个int开销不大,那如果不幸需要传递并拷贝一个类,当然你可以将类的引用传递进去)当n很大时,这个开销其实是可以避免的。
怎么避免呢?看看下面的程序
1. /*********************
2. * 程序3:递归和迭代
3. **********************/
4. 5. //递归版计算n的阶乘
6. long Factorial(int
7. {
8. if(n == 0)
9. {
10. return
11. }
12. else
13. {
14. return
15. }
16. }
17. 18. //迭代版计算n的阶乘
19. long Factorial(int
20. {
21. int
22. while(n != 0)
23. {
24. fac *= n;
25. n--;
26. }
27. return
28. }
上面的程序告诉我们,递归的优美是有代价的~~~
好了,现在我们在回头看看我们的兔子,将我们的程序也换成迭代版的试一试:
1. //程序4:迭代版Fibonacci数列 Code in C/C++
2. int
3. 4. int Fibonacci(int
5. {
6. if(n < 3)
7. return
8. else
9. {
10. int
11.
12. while(F[tmp] == 0)
13. --tmp;
14.
15. for(int
16. F[i] = F[i-1] + F[i-2];
17.
18. return
19. }
20. }
好了到此为止,兔子们的计划生育貌似解决了(其实还是在生-_||)。
不过。。。程序里面其实还有很多问题,比如要是n超过1000怎么办,等等等等,这里我们仅仅谈谈解决的思路,就不深入这么多了,留到以后再讲。
OK,从零开始学算法,迈出第一步了,谢谢!!!