斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波纳契数列以如下被以递归的方法定义:F(0)=0,F(1)=1,F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)
1.1 基本递归实现
def fibrcs(n):
if n <= 0:
return 0
if n == 1:
return 1
return fibrcs(n-1) + fibrcs(n-2)
import time
t = time.clock()
res = fibrcs(40)
e = time.clock()
print('result:',res)
print('Time consumed:',e-t)
result: 102334155
Time consumed: 38.44381827542191
知识关联:
1.递归算法设计:依照数学归纳法思路,设置只考虑k=1,2的情况和k = n-1,n的情况(待另写文章归纳总结)
2.函数调用关系以栈实现.
虽然斐波那契数列是大多数人学习递归思想的入门函数,但是以递归实现斐波那契数列存在明显的问题.如计算fib(10)的时候需要计算fib(8) + fib(9),但在fib(9)中又要再次计算fib(8),加上递归本身也不是高效的实现,因此从上面的结果可以看到计算fib(40)的效率触目惊心(计算fib(50)已经慢到我等不及了).
解决方法:
加入全局变量保存已经计算过的变量
###1.2 尾递归实现(2018.1.19)
学习递归的过程中学到了尾递归与非尾递归的概念,发现一直没有看到相关的递归知识的讲解,除了技术博客的文章以外很少有算法教材专门系统的讲解递归的实现.直到了解了尾递归与非尾递归的概念,结合数学归纳法才对递归有一种系统的了解.
尾递归可以理解为每一层都已经包含了计算所需要的所有变量,每次的递归是线性的而不是像1.1的解法一样形成一个树形结构,从代码逻辑上看更像是迭代算法的改写.(但要加深理解还是逃不过多打代码多用递归实现不同的问题)
尾递归的概念可以参考这位知乎用户的回答,基本解释清楚.
[什么是尾递归? - 知乎]
(https://www.zhihu.com/question/20761771/answer/19996299)
另外,尽管python并没有为尾递归专门优化,但是实现方法与算法效率还是贴出来.
def fibrcs_tail(n):
def fib_tail(a,b,n):
if n == 0:
return a
else:
a, b = b, a+b
return fib_tail(a,b,n-1)
return fib_tail(0,1,n)
result: 102334155
Time consumed: 5.3597748895128916e-05
可以看到时间复杂度基本跟迭代方法是同一个量级的
2.保存计算结果
2.1 使用dict保存
dic = {0:0,1:1}#以字典保存,实现O(1)的时间复杂度
def fibdic(n):
if n in dic:
return dic[n]
else:
res = fibdic(n-1) + fibdic(n-2)
dic.update({n:res})
return dic[n]
知识关联:
1.判断处理字典中的有无指定key的情况,可使用dic.setdefault()规定找不到key时的操作,取代if else使代码更加pythonic,在oj刷题时很实用.
2.dic变量在函数外仍有效,python当中函数的变量作用域不止函数中的变量,在函数外仍可查找到.(搜索范围有一定顺序,待以后展开)
t = time.clock()
res = fibdic(40)
e = time.clock()
print('result:',res)
print('Time consumed:',e-t)
result: 102334155
Time consumed: 0.00011636353383437381
2.2. 使用functools.lru_cache (2019.6.29更新)
@lru_cache(maxsize=None)
def fibonacci(n):
if n <= 1:
return n
else:
return fibonacci(n-1) + fibonacci(n-2)
知识归纳:
- functool.lru_cache直接以装饰器方式即可作用于函数,
- 作用类似于我们上面的dict保存函数输出的结果. 但 functool.lru_cache内部实现了更完整的缓存机制, 使用LRU(least recently used, 最近最少使用)机制淘汰很久未被使用的缓存.
- 第一个参数maxsize为必须, 指定缓存当中保存函数值的个数. maxsize=None为无限制.
- lru_cache装饰后的函数具有cache_info()和cache_clear(), 分别可以查看当前缓存状态(缓存值个数, 命中数与缺失数等)和清除缓存.
fibonacci.cache_info() # 查看缓存状态
fibonacci.cache_clear() # 清除缓存
3.基本迭代方法
def fibiter(n):
a, b = 0, 1
for _ in range(n):
#编程习惯:如果循环对象在for当中没有用到,以_命名
a, b = b, a + b
return a
t = time.clock()
res = fibiter(40)
e = time.clock()
print('result:',res)
print('Time consumed:',e-t)
result: 102334155
Time consumed: 6.417625172616681e-05
果然还是和递归的效率不是一个量级,在实际开发当中应该尽可能避开递归实现
4.结合特征值与快速幂实现O(logn)算法
解算法题目都有两种方式:
1) 模拟算法过程: 这是最常用也是通常作为算法标准解法的方法,如4皇后问题,背包问题,包括上面实现的斐波那契数列方法.
2) 总结算法规律: 这方面通常可以通过数学归纳法进行分析证明,在笔试过程中不常用,一是因为推导过程通常更繁琐,二是总结规律更像是一种trick,有点吃力不讨好的感觉.如n-阶蛙跳问题,只要稍微写出前几项,就可以简单地得出对于n级的阶梯,只要return 2的n次方一行代码即可.
(待找到更好的例子时更新这部分内容,对比两种方式)
首先斐波那契数列可以总结为以下矩阵相乘,[[1,1],[1,0]]也是特征矩阵:
因此对于fib(n)可以总结为初始矩阵乘以A的n次方
而对于乘方问题我们有快速幂实现O(logn)的解法,应用在矩阵A上即可实现O(logn)的解法.(代码待补充)
#快速幂解法
def quickpow(base,exp):
res = 1
while exp:
if exp & 1 :
res *= base
base *= base
exp = exp >> 1
return res