1.💖利用记忆自顶向下实现斐波那契数💖
1.1 🌸斐波那契数列由以下递归式定义🌸:
f ( n ) = { n , n = 0 , 1 f ( n − 1 ) + f ( n − 2 ) , 其 他 \begin{aligned} f(n)= \begin{cases} n,&n=0,1\\ f(n-1)+f(n-2),&其他 \end{cases} \end{aligned} f(n)={n,f(n−1)+f(n−2),n=0,1其他
1.2 当n=5时的斐波那契数列递归树🌲
为了便于理解,将利用时间复杂度 O(2n) 递归算法计算n=5时的 fib(5) 斐波那契数列执行过程表示为一棵递归树(如下图所示)。树中每一个结点表示一次函数调用,不难发现为了计算fib(5),需要深度优先依次遍历树中每一个结点,然而这棵树存在许多重复结点,如2个fib(3)结点和3个fib(2)结点。这些重复的结点在求解过程中都会被重复的展开进行计算,这是导致递归算法求解斐波那契数列效率低下的主要原因。
斐波那契数列递归树
1.3 🌸记忆优化重复递归子问题🌸
既然算法的实现过程存在诸多重复的函数调用,那么为了提高算法执行效率,应该考虑优化这些重复的函数调用。我们采用的方法非常简单,记忆。也就是说,在计算出一个输入参数为n的斐波那契数fib(n)后,就把fib(n)用表的形式存储下来。在函数递归调用前,首先在表中查找函数对应参数的值是否在表中。如果表中没有对应的值,说明该参数对应的函数还未被调用,那么就调用该参数对应的递归函数;否则,说明该参数的斐波那契数已经计算出来,这时就不用调用该参数对应的递归函数,而是直接将表中存储的斐波那契数返回即可。
1.4 🌸Python自顶向下实现斐波那契数🌸
代码1.4
def fib_top_bottom(nth):
"""
自顶向下求解斐波那契数列第nth项
:param nth: 第n项
:return: the nth item of fibonacci sequence
"""
if nth in memo: # 判断第n项是否已经求出,若是则直接返回
return memo[nth]
else:
if nth <= 2: # boundary conditions
fib = 1
else:
fib = fib_top_bottom(nth - 1) + fib_top_bottom(nth - 2)
memo[nth] = fib
return memo[nth]
2.🌹自底向上实现斐波那契数🌹
2.1 🎆自底向上实现递归🎆
除了利用记忆实现递归外,还可以用自底向上的方法来实现递归,如代码2.3所示。代码直接采用循环来代替递归函数调用。fib(0)和fib(1)是边界条件,有了它们就可以求出fib(2)。有了fib(1)和fib(2),则可以求fib(3)。因此,索引i从2依次递增到n,根据递归式仅仅使用循环,而非递归函数实现求解斐波那契数。
代码2.3的实现可看作如图2.2所示的执行过程。图中每一结点代表一个参数对应的斐波那契数。任意一个结点求值所需要的信息,都是已经求出的结点值。也就是说,当前结点的值只与该结点左边的结点有关,与该结点右边结点无关,而当前结点左边结点的值均已经算出。具体而言,如果要求fib(5)这个结点的值,需要知道这个结点左边结点的值,而该结点左边结点的值在求结点fib(5)之前便已经得到。
2.2 🎇斐波那契数列有向无环图🎇
如果将代码1.4看作是自顶向下的求解斐波那契数,那么代码2.3就是自底向上求解斐波那契数。之所以称为自底向上,是因为在求解fib(n)的值时,我们从fib(0),fib(1),fib(2)开始直到fib(n)。自底向上的实现递归,总是利用已知的信息去求未知的信息,这相当于对图2.2进行拓扑排序后得到的结点顺序。
图2.2 求解斐波那契数列的有向无环图
2.3 🎉Python自底向上实现斐波那契数🎉
代码2.3
# 定义记忆字典
memo = {0: 0}
def fib_bottom_top(nth):
"""
自底向上求解斐波那契数列第nth项
:param nth: 第n项
:return: the nth item of fibonacci sequence
"""
fib = {0: 0}
for i in range(1, nth + 1):
if i <= 2: # boundary conditions
fib_i = 1
else:
fib_i = fib[i - 1] + fib[i - 2]
fib[i] = fib_i
return fib[nth]