斐波那契数列问题解析

题目描述

大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)n<=39

 

题目来源:剑指offer-斐波那契数列

补充说明:   斐波那契数,亦称之为斐波那契数列,指的是这样一个数列:1、1、2、3、5、8、13、21、……

        用文字来说,就是斐波那契数列由 0 和 1 开始,之后的斐波那契数列系数就由之前的两数相加,在数学上,斐波那契数列以如下被以递归的方法定义:F0=0,F1=1,Fn=Fn-1+Fn-2(n>=2,n∈N*).

 

一、递归解法:  

          斐波那契数列具有天然的递归性,根据斐波那契数列数学上的定义,我们可以得出其递推公式为:Fn=Fn-1+Fn-2(n>=2,n∈N*),基础情况为 F0=1,F1=1.

public class Solution {
    public int Fibonacci(int n) {
        //base case
        if (n==0){
           return 0;
       }
        if(n==1){
            return 1;
        }
       return Fibonacci(n-1)+Fibonacci(n-2);
    }
}

 斐波那契数列问题解析_第1张图片

PS:对于递归问题,可画出递归树,帮助分析算法的复杂度。

对于递归解法,我们可以把问题转化为规模缩小了的同类问题的子问题,找出明确的不需要继续进行递归的条件(即基本情况base case),在本题中的基本情况为F0=0,F1=1, 当递归至基本情况后,无需继续递归,而是返回计算结果,最后把子问题的解汇聚成大问题的解.

复杂度分析:

子问题个数,即递归树中节点的总数。显然二叉树节点总数为指数级别,所以子问题个数为 O(2^n).

解决一个子问题的时间,在本算法中,没有循环,只有 f(n - 1) + f(n - 2)f(n−1)+f(n−2) 一个加法操作,时间为O(1)。

所以,这个算法的时间复杂度为 O(2^n),指数级别. 递归过程中,需要在存储递归过程中的运算结果,最大空间为树的高度h(即n).

           时间复杂度:O(2^n)

           空间复杂度:O(h) 即O(n)

问题分析:

   观察递归树,很明显发现了算法低效的原因:存在大量重复计算,运算的规模与n的大小成指数关系,因此这个暴力递归算法虽然简洁明了,但运行效率低下.

 

二、动态规划

       由于暴力递归存在大量的重复运算,降低了算法的性能.我们可以把运算结果存储起来,从第0项推导至第n项,避免重复运算.

public class Solution {
      public  int Fibonacci(int n) {
        //基本情况
        if (n==0){
            return 0;
        }
        if (n<=1){
            return 1;
        }
        int dp[]=new int[n+1];
        dp[0]=0;
        dp[1]=1;
        for(int i=2;i

       在代码中,我们把 dp[i]=dp[i-1]+dp[i-2]  称作状态转移方程, 原因在于我们把 f(n) 当做一个状态 n,这个状态 n 是由状态n−1 和状态 n−2 相加转移而来. 我们不难看出该状态转移方程正代表着暴力递归的过程,只是我们把运算过程的信息保存起来,根据这些信息指导后续的运算,避免重复运算.

       面对动态规划问题,我们可以先思考其暴力递归的解法,把暴力递归的过程抽象成状态转移方程,确定可变参数,从基本情况(base case)开始推理,通过状态转移方程,得出最优解,减少冗余运算.

复杂度分析:

      需要开辟长度为n+1的dp数组,同时遍历整个数组.

         时间复杂度:O(n)

         空间复杂度:O(n)

  练习:

         细心的你会发现,根据斐波那契数列的状态转移方程,当前状态只和之前的两个状态有关,其实并不需要那么长的一个 DP 数组 来存储所有的状态,只要想办法存储之前的两个状态就行了。所以可以进一步优化,把空间复杂度降为 O(1). 请您写出优化后的代码.

三、斐波那契数列通项公式 

       这是一个数学方法,通过斐波那契数列通项公式求解,但仅限于标准的斐波那契数列问题求解,无法应对斐波那契数列的变种问题,在此不做展示. 公式如下:
u=2524561525,4505879&fm=58.jpg

public class Solution {
    public  int Fibonacci(int n) {
      Long fib = Math.round((Math.pow((1 + Math.sqrt(5)) / 2, n) 
                             - Math.pow((1 - Math.sqrt(5)) / 2, n)) 
                            / Math.sqrt(5));
         return fib.intValue();
    }
}

复杂度分析:

   将n代入公式即可得到答案,其中Math.pow(a,b)为求a的b次方、Math.sqrt(a)为求a的正平方根、Math.round(a)为取a最接近的整数(可简单理解为四舍五入取整).对于pow、sqrt、round方法,我们都可以放心地认为其时间复杂度为O(1),因而总的时间复杂度为O(1).我们使用变量fib存储运算结果,因此空间复杂度为O(1).

         时间复杂度:O(1)

         空间复杂度:O(1)

总结:

      在计算机发明以前,人们解决问题的方式就是通过经过严格数学证明的公式,找寻答案(如答案三),这确实是一个正确的方式,但也有着许多局限性,如归纳公式并不简单,实际问题复杂多变,难以与公式对应等.计算机的出现使我们多了一种解决问题的方式,即穷举,凭借计算机强大的算力,穷举所有可能性.

      十分感谢你看到这里,如果你原本不太了解动态规划,相信现在已经掌握了这个算法的一些设计技巧.算法设计无非就是先思考 “如何穷举”,然后再追求 “如何聪明地穷举”。 在本题中,我们思考出暴力递归的解法,找出问题的状态转移方程,利用状态转移方程实现"聪明地穷举"!

      在以后的学习中,我们还会学习更多的算法思想, 但它们无非就是在实现"聪明的穷举".用变量存储运行时的信息,利用这些信息指导后续的运算,这种用空间换时间的思路,是降低时间复杂度的不二法门.

 

拓展:

        斐波那契数列来自实现生活,有着诸多的变种问题,相信聪明的你能灵活运用动态规划的思想,顺利解决这些问题.

        剑指offer-跳台阶

        剑指offer-变态跳台阶

        

 

你可能感兴趣的:(斐波那契数列问题解析)