一、暴力递归
斐波那契数列,是典型的利用子问题的类型,可用递归实现:
class Solution:
def fib(self, n: int) -> int:
if n == 0: return 0
if n == 1: return 1
return self.fib(n - 1) + self.fib(n - 2)
if __name__ == '__main__':
x = Solution().fib(10)
print(x)
而暴力递归的时间复杂度,即为每次递归操作的时间复杂度 * 递归次数。前者为 O(1),后者为 math.pow(2, N),因此为 O(math.pow(2, N))。
二、带备忘录的递归
而子问题会有很多重复计算,例如下图的 f(18) 就计算了两次:
为了避免重复计算,可用带备忘录的递归实现,其中 memo 是备忘录,其存储了已计算过的结果,避免重复计算:
class Solution:
def fib(self, n: int) -> int:
memo = [0 for i in range(n+1)]
return self.helper(memo, n)
def helper(self, memo: list, n: int):
if n == 0 or n == 1:
return n
if memo[n] != 0:
return memo[n]
memo[n] = self.helper(memo, n-1) + self.helper(memo, n-2)
return memo[n]
if __name__ == '__main__':
x = Solution().fib(4)
print(x)
2.1 时间复杂度
下图中绿色的 f(18) 和 蓝色的 f(17),均被剪枝了:
剪枝后,树状即退化成了链表,示例如下:
而递归的时间复杂度,即为每次递归操作的时间复杂度 * 递归次数。前者为 O(1),后者为 N,因此为 O(N)。
另外有 N(N) 的空间复杂度,来存放 memo 数组。
三、动态规划
然而,上述递归算法本质是先从顶向下调用,再从底向上求解。毕竟 f(20) 需计算 f(19) 和 f(18),而 f(18) 又需计算 f(1) 和 f(0)。
然而,递归有调用栈的开销,我们可完全只用从底向上的方式求解,如已知 f(0) 和 f(1),推导出 f(2), 再推导出 f(3), 最终推导出 f(20),示意图如下:
class Solution:
def fib(self, n: int) -> int:
dp = [0 for i in range(n + 1)]
dp[0] = 0
dp[1] = 1 # base case
for i in range(2, n + 1):
dp[i] = dp[i - 2] + dp[i - 1] # 状态转移方程
return dp[n]
if __name__ == '__main__':
x = Solution().fib(4)
print(x)
时间复杂度O(N),空间复杂度O(N)。
3.1 优化空间复杂度
然而每次迭代,并不需要整个 dp 数组,而只需前两个数即可,可优化代码如下:
class Solution:
def fib(self, n: int) -> int:
if n < 2: return n
a, b = 0, 1 # base case
ans = 0
for i in range(2, n + 1):
ans = a + b # 状态转移方程
a, b = b, ans
return ans
if __name__ == '__main__':
x = Solution().fib(20)
print(x)
时间复杂度O(N),空间复杂度O(1)。