话说有一只青蛙,想要跳下n
级台阶下水塘,它每次能够跳1个台阶或者2个台阶,那么请问它一共有多少种跳法下水塘(好比,n=30
时)?python
用数学的语言来看,咱们须要求一个青蛙跳的函数f(n)
,对这种自变量取值为非负整数的函数,咱们能够从比较小的状况开始考虑,不可贵到f(1)=1, f(2)=2
,问题是之后的穷举愈来愈麻烦。面试
想象你就是那只青蛙,面对n
级台阶,第一次你能够先跳1级,那么剩下n-1
级,有f(n-1)
种跳法,第一次也能够跳两级,那么剩下n-2
级,有f(n-2)
种跳法,因此这个问题的答案并不陌生,是神奇的斐波拉契数列:算法
解决这类求函数值问题的第一步,是找到一个递推式。咱们把递推式翻译成python代码:数组
def fib(n):
if n==0:
return 1
if n==1:
return 1
return fib(n-1)+fib(n-2)
复制代码
%%time
fib(30)
Wall time: 269 ms
832040
复制代码
运行时间284ms
,有够慢的,为何慢?由于重复计算实在太多,以计算f(5)
为例,调用关系以下:缓存
f(5)==>f(4), f(3)
f(4)==>f(3), f(2), f(3)==>f(2), f(1)
f(3)==>f(2), f(1), f(2)==>f(1), f(0), f(2)==>f(1), f(0), f(1)
f(2)==>f(1), f(0), f(1), f(1), f(0), f(1), f(0), f(1)
f(1), f(0), f(1), f(1), f(0), f(1), f(0), f(1)
复制代码
那么一个很天然的想法是咱们把中间计算结果都缓存下来,幸运的是,python中自带了这个“电池”。bash
from functools import lru_cache
@lru_cache()
def fib(n):
if n==0:
return 1
if n==1:
return 1
return fib(n-1)+fib(n-2)
复制代码
%%time
fib(30)
Wall time: 0 ns
832040
复制代码
快到没计量出时间来。python中lru_cache
的基本原理是构建一个字典,字典的key
为调用参数,value
就是该参数的计算结果。大体等价于以下代码:函数
def fib(n):
if n in fib.cache:
return fib.cache[n]
if n==0:
ans = 1
elif n==1:
ans = 1
elif:
ans = fib(n-1)+fib(n-2)
fib.cache[n] = ans
return ans
fib.cache = {}
复制代码
固然,针对这个问题,咱们可使用更加细致的缓存方法, 乃至去掉递归改用循环(至关于只保留两个缓存,大大减小了空间占用,可是若是咱们要反复计算各个n
值,那么或许前一个方法才更合适):spa
def fib(n):
a, b = 0, 1
for i in range(n):
a, b = b, a+b
return a
复制代码
本题等同于 leetcode 70, 在leetcode上的python3解答以下:翻译
from functools import lru_cache
class Solution:
@lru_cache()
def climbStairs(self, n: int) -> int:
if n==0:
return 1
if n==1:
return 1
return self.climbStairs(n-1)+self.climbStairs(n-2)
复制代码
执行用时52 ms,内存消耗13.2MB。设计
咱们从这只青蛙中取得比较通用的启示,解决相似的可构造递推函数的问题:
在通常的算法教材或面试题解中,会花很多时间来设计这个缓存结构,在实际的工程问题中,咱们可能对多使用一些缓存空间没有那么敏感,所以只须要开发递归函数,再加上通用的缓存方案就基本解决问题了。只有在缓存空间成为问题时,咱们才须要进一步去考虑适应问题的更小的缓存。
为了检验这套方案,咱们再看几道题,直接在leetcode上再找几个来刷。
给定一个整数数组 nums
,找到一个具备最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4], 输出: 6 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
咱们考虑数组中每个位置结尾能获得的最大和的递推关系。
基于此不可贵到最终结果为
在leetcode中翻译成python3代码以下:
from functools import lru_cache
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
self.nums = nums
return max(self.f(i) for i in range(len(nums)))
@lru_cache()
def f(self, k):
if k == 0:
return self.nums[0]
else:
return max(self.f(k-1), 0) + self.nums[k]
复制代码
执行耗时76 ms,内存消耗13.7 MB。
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
示例:
输入: [ [1,3,1], [1,5,1], [4,2,1] ] 输出: 7 解释: 由于路径 1→3→1→1→1 的总和最小。
将矩阵中每一个位置做为右下角,求最小路径和,不可贵到以下递推公式:
在leetcode中翻译成python3代码以下:
from functools import lru_cache
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
self.grid = grid
return self.f(len(grid)-1, len(grid[0])-1)
@lru_cache()
def f(self, x, y):
if x == 0 and y == 0:
return self.grid[0][0]
elif y == 0:
return self.f(x-1, 0) + self.grid[x][0]
elif x == 0:
return self.f(0, y-1) + self.grid[0][y]
else:
return min(self.f(x-1,y), self.f(x,y-1)) + self.grid[x][y]
复制代码
执行耗时1052ms,内存消耗13.9M。