[极客时间-每日一课]如何优雅地计算斐波那契数列?
课程:https://time.geekbang.org/dailylesson/detail/100028406
问题:计算斐波那契数列的第n项的值,数列表达式:F[n]=F[n-1]+F[n-2] (n>=2,F[0]=0,F[1]=1)
winter(讲师)认为这是一道很好的面试题,
1. 答案简单。
2. 每个面试者都能写出点东西。
3. 区分度高,不同水平的人写出来的代码水平不同。
4. 马甲众多(很少有面试官直接上来说给我撸一个斐波那契数列,都会问一个具体的问题,最后归根到底就是斐波那契数列问题)。
好,接下来看一下winter认为不同水平的代码长啥样。
Lev1. 递归
int Fibnacci(int n){ if(n < 2){ return n; } return Fibnacci(n - 1) + Fibnacci(n - 2); }
可以简单算一下它的时间复杂度是指数级的。它会把子问题重复计算多遍。如果我要计算数列第5项的值,会计算如下中间结果。
可以观察到纯在大量重复的节点计算。优化一下。
Lev2. 带备忘录的递归
int Fibnacci(int n){ if(map.ContainsKey(n)){ return map[n]; } if(n < 2){ return n; } int res = Fibnacci(n - 1) + Fibnacci(n - 2); map.Add(n, res); return res; }
时间复杂度O(n), 空间复杂度O(n)。到这有人说递归自带性能消耗,再优化一下。
Lev3. DP (动态规划)
当发现存在大量重复子问题的时候,通常我们会想到DP.
首先我们确定状态转移方程 DP[n] = DP[n-1] + DP[n-2].
(DP是一种自下向上的解决问题的思路,先解出f(2), 那f(3)就得解,接着f(4)也就得解,直到f(n),而递归是自上而下)
int Fibnacci(int n){ if(n < 2){ return n; } int[] dp = new int[n + 1]; dp[0] = 0; dp[1] = 1; for(int i = 2; i <= n; i++){ dp[i] = dp[i - 1] + dp[i - 2]; } return dp[n]; }
时间复杂度O(n), 空间复杂度O(n)。
可以继续优化把DP数组去掉,空间复杂度优化成O(1),这里就不演示了。
到这,其实我认为这已经是极限了,最起码是我的极限。
Lev4. 通项公式
没错,数学家给出了数列的通项公式,我们老老实实套公式即可。
感兴趣的可以自己推导一遍。
let fibnacci = (n) => ((Math.pow(1 + Math.sqrt(5))/2, n) - Math.pow((1 - Math.sqrt(5))/2, n))/Math.sqrt(5);
问题来了,现在的时间复杂度是O(1)吗?严格意义上说不是,这里调用了系统的幂函数,winter没有指出该函数在V8的具体实现,但是结论一定不是O(n), 更不是O(1)。
后面他提供了自己实现的O(log(N))幂运算版本:
let pow = (x, n) => { var r = 1; var v = x; while(n) { if(n % 2 == 1){ r *= v; n-= 1; } v = v * v; n = n /2; } return r; }
Lev N: ????
考虑到浮点误差,winter再次提出借助线性代数的矩阵运算来表示斐波那契数列的通项。
到这里,我已经彻底放飞自我,觉得他说的都对。
总结
这节课的后半段,体验不是很好,毕竟数学知识全还了,思路已经跟不上了。
常见的斐波那契数列题型:
1. 爬台阶:有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶。实现一种方法,计算小孩有多少种上楼梯的方式(还有什么青蛙跳台问题,换一下主语)
2. 爬台阶+: 有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶、3阶。实现一种方法,计算小孩有多少种上楼梯的方式
3. 兔子繁殖问题: 一对兔子每个月能生出一对小兔子来。如果所有兔子都不死,那么一年以后可以繁殖多少对兔子?
PS: 和斐波那契数列相似的著名数列:卡塔兰数列
具体问题:给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种?
假如n=3,结果就是5种。
posted on 2020-03-04 17:18 qingMing01 阅读(958) 评论(0) 编辑 收藏 举报