LeetCode|斐波那契数的n中解法

509. 斐波那契数 递归 很快啊!看到这个名字直接递归处理

class Solution { public: int fib(int n) { if(n==0)return 0; if(n==1)return 1; return fib(n-1)+fib(n-2); } };

其实仔细发现,这里的处理是非常低效的,会重复计算,如下图,可以看到重复计算了f(18)、f(17)等数据。这是动态规划问题的第一个性质:重叠子问题。
LeetCode|斐波那契数的n中解法
文章图片

时间复杂度:为二叉树的所有节点个数,即为O(2^n)
空间复杂度:为二叉树的高度,即为O(n)
这里参考一篇博客–程序员算法面试中,递归算法的空间复杂度你知道怎么求么?
递归算法的空间复杂度 = 每次递归的空间复杂度 * 递归深度
首先看每次递归的空间复杂度,因为这个算法中我们可以看出每次递归所需要的空间大小都是一样的
而且就算是第N次递归,每次递归所需的栈空间也是一样的。
所以每次递归中需要的空间是一个常量,并不会随着n的变化而变化,每次递归的空间复杂度就是O(1)
求递归的空间复杂度,那么为什么要看递归的深度呢
每次递归所需的空间都被压到调用栈里(这是内存管理里面的数据结构,和我们算法里的栈原理是一样的)
看递归算法的空间消耗,就是要看调用栈所占用的大小
一次递归结束,这个栈就是就是把本次递归的数据弹出去。所以这个栈最大的长度就是 递归的深度。
LeetCode|斐波那契数的n中解法
文章图片

原文链接:https://blog.csdn.net/youngyangyang04/article/details/106313759
带备忘录的递归(解决重叠子问题) 利用一个数据存储数据,取31是由于题目的n的范围在0-30之间。
class Solution { public: int arr[31]={0,1}; int fib(int n) { if(n<2)return arr[n]; if(arr[n]!=0){ return arr[n]; }else{ arr[n] = fib(n-1) + fib(n-2); return arr[n]; } } };

无限接近双百的一次!
LeetCode|斐波那契数的n中解法
文章图片

其实就是通过剪枝来减少重复的工作。但是其空间复杂度仍然为n
LeetCode|斐波那契数的n中解法
文章图片

时间复杂度:O(n)
空间复杂度:O(n)
从图可以看出,在调用递归时,我们没必要在把当前信息保存下去了。于是有了一种优化方法–尾递归
尾递归 百度百科:
如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。
优化方法:由于递归调用是当前函数最后一条语句,返回栈时没有其他任务执行,因此也没有存栈的必要了。编译器遇到尾递归时,直接覆盖当前的活动记录而不是在栈中去创建一个新的空间。
class Solution { public: int fib(int n) { return temp(0,1,n); }int temp(int first,int second,int n){ if(n==0)return 0; if(n==1)return 1; if(n==2)return first+second; return temp(second,first+second,n-1); }};

时间复杂度:O(n)
空间复杂度:O(1),编译器优化,直接覆盖旧的栈空间。
滚动数组(循环求解) 下面为官方代码
class Solution { public: int fib(int n) { if (n < 2) { return n; } int p = 0, q = 0, r = 1; for (int i = 2; i <= n; ++i) { p = q; q = r; r = p + q; } return r; } };

时间复杂度:O(n)
空间复杂度:O(1)
以上题解,有篇题解写的很好,特别备注在此:动态规划套路详解
官方还给出了下面两种方法,就比较逆天了。
矩阵快速幂 时间复杂度再次降低!
构建一个递推关系:
( 1 1 1 0 ) ( F ( n ) F ( n ? 1 ) ) = ( F ( n ) + F ( n ? 1 ) F ( n ) ) = ( F ( n + 1 ) F ( n ) ) \begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix} \begin{pmatrix} F(n) \\ F(n-1) \end{pmatrix}= \begin{pmatrix} F(n)+F(n-1) \\ F(n) \end{pmatrix}= \begin{pmatrix} F(n+1) \\ F(n) \end{pmatrix} (11?10?)(F(n)F(n?1)?)=(F(n)+F(n?1)F(n)?)=(F(n+1)F(n)?)
因此:
( F ( n + 1 ) F ( n ) ) = ( 1 1 1 0 ) n ( F ( 1 ) F ( 0 ) ) \begin{pmatrix} F(n+1) \\ F(n) \end{pmatrix}= \begin{pmatrix} 1&1 \\ 1&0 \end{pmatrix}^n \begin{pmatrix} F(1) \\ F(0) \end{pmatrix} (F(n+1)F(n)?)=(11?10?)n(F(1)F(0)?)
因此我们只要能快速计算矩阵的n次幂,就能得到F(n)的值。
一开始以为到这里看代码就懂了,结果还是不懂。主要是下面中间那个函数。
class Solution { public: int fib(int n) { if (n < 2) { return n; } vector> q{{1, 1}, {1, 0}}; vector> res = matrix_pow(q, n - 1); //为什么这里是n-1,好好看一下上面递推关系的第一项,是n+1,我们要的是n。 return res[0][0]; //关于为什么这里不用再乘以(F(1),F(0))^T,是因为乘后[0][0]项也是没有改变的 }vector> matrix_pow(vector>& a, int n) { vector> ret{{1, 0}, {0, 1}}; //单位矩阵 //让人疑惑的代码begin while (n > 0) { if (n & 1) { ret = matrix_multiply(ret, a); } n >>= 1; a = matrix_multiply(a, a); } return ret; //让人疑惑的代码end }vector> matrix_multiply(vector>& a, vector>& b) { vector> c{{0, 0}, {0, 0}}; for (int i = 0; i < 2; i++) { for (int j = 0; j < 2; j++) { c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j]; } } return c; } };

一开始以为矩阵快速幂是什么现编的词,结果是一个正规的方法。这里不求明白这个解法深邃的含义,能窥探一丁,便心满意足了。
参考文章:矩阵 快速幂
矩阵快速幂,不要被这个高大上的名字吓到了,先把他理解为求一个数的幂次。
eg. 求A^156,首先想到A*A*…(省略156次)
这种太麻烦了,我们有了简化的版本:A156=>(A4)*(A8)*(A16)*(A128) 即A(4+8+16+128)
为什么这样拆呢?其实…这些对应着156的二进制10011100。
vector> ret{{1, 0}, {0, 1}}; //ret初始化为单位矩阵,相当于1 while (n > 0) { if (n & 1) { ret = matrix_multiply(ret, a); } n >>= 1; a = matrix_multiply(a, a); //相当于每次都平方一次 } return ret;

【LeetCode|斐波那契数的n中解法】LeetCode|斐波那契数的n中解法
文章图片

时间复杂度:O(log n)
空间复杂度:O(1)
通项公式 高数忘光了…待会复习归来再整理。。
待续。。

    推荐阅读