从零开始学算法。

        首先说说递推吧,举一个耳熟能详的例子,就是有关兔子的计划生育问题:兔子的生育能力很强啊,一对兔子出生两个月后就能生一对兔子,并且以后每月都能再生一对。简单点说,就是新兔子要出生隔月后才能生(发育得很快嘛),那么现在想想,如果所有兔子都长命百岁,一年后有多少对兔子呢?

        经过月数:---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,够慢了吧!

        怎么会这么慢呢?我们来仔细分析分析。看看这句:

  1. 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,从零开始学算法,迈出第一步了,谢谢!!!