记忆的定义
“memoization”一词是由 Donald Michie 在 1968 年提出的。它基于拉丁词备忘录,意思是“被记住”。这不是记忆这个词的拼写错误,尽管在某种程度上它有一些共同点。记忆化是一种用于计算以加速程序的技术。这是通过记住处理输入的计算结果(例如函数调用的结果)来实现的。如果使用相同的输入或具有相同参数的函数调用,则可以再次使用先前存储的结果并且避免不必要的计算。在许多情况下,一个简单的数组用于存储结果,但也可以使用许多其他结构,例如关联数组,在 Perl 中称为哈希或在 Python 中称为字典。
记忆可以由程序员显式编程,但一些编程语言(如 Python)提供了自动记忆函数的机制。
使用函数装饰器记忆
你也可以参考我们关于装饰器的章节。特别是,如果您在理解我们的推理时可能有问题。
在我们之前关于递归函数的章节中,我们制定了一个迭代和一个递归版本来计算斐波那契数。我们已经证明,将数学定义直接实现为如下递归函数具有指数运行时行为:
def fib(n):
如果 n == 0:
返回 0
elif n == 1:
返回 1
别的:
返回 fib(n-1) + fib(n-2)
我们还提出了一种改进递归版本运行时行为的方法,即通过添加字典来记住函数先前计算的值。这是一个明确使用记忆化技术的例子,但我们并没有这样称呼它。这种方法的缺点是失去了原始递归实现的清晰性和美感。
“问题”在于我们更改了递归 fib 函数的代码。下面的代码不会改变我们的 fib 函数,所以它的清晰度和易读性没有受到影响。为此,我们定义并使用了一个我们称之为 memoize 的函数。memoize() 将函数作为参数。函数 memoize 使用字典“备忘录”来存储函数结果。尽管变量 "memo" 和函数 "f" 是 memoize 的局部变量,但它们通过辅助函数被闭包捕获,该辅助函数由 memoize() 作为引用返回。因此,调用 memoize(fib) 返回对 helper() 的引用,该 helper() 正在执行 fib() 本身会执行的操作以及保存计算结果的包装器。对于整数 'n' fib(n) 只会被调用,如果 n 不在备忘录字典中。如果它在里面,
DEF memoize的(˚F ):
备忘录 = {}
DEF 助手(X ):
如果 X 不 在 备忘录:
备忘录[ X ] = ˚F (X )
返回 备忘录[ X ]
返回 辅助
DEF FIB (Ñ ):
如果 Ñ == 0 :
返回 0
elif n == 1 :
返回 1
else :
返回 fib ( n - 1 ) + fib ( n - 2 )
fib = memoize ( fib )
print ( fib ( 40 ))
输出:
102334155
让我们看一下代码中使用 fib 作为参数调用 memoize 的那一行:
fib = memoize(fib)
这样做,我们将 memoize 变成了装饰器。有人说 fib 函数是由 memoize() 函数修饰的。我们将用下图说明装饰是如何完成的。第一个图说明了装饰之前的状态,即在我们调用 fib = memoize(fib) 之前。我们可以看到引用它们主体的函数名称:
执行后 fib = memoize(fib) fib 指向辅助函数的主体,该主体已由 memoize 返回。我们也可以看出,原来 fib 函数的代码,以后只能通过辅助函数的“f”函数才能到达。没有其他方法可以直接调用原始 fib,即没有其他引用。修饰的斐波那契函数在 return 语句 return fib(n-1) + fib(n-2) 中被调用,这意味着 memoize 返回的辅助函数的代码:
装饰器上下文中的另一点值得特别注意:我们通常不会为一个用例或函数编写装饰器。我们宁愿将它多次用于不同的功能。所以我们可以想象有更多的函数func1、func2、func3等等,它们也消耗了很多时间。因此,用我们的装饰器函数“memoize”来装饰每一个是有意义的:
fib = memoize(fib)
func1 = memoize(func1)
func2 = memoize(func2)
func3 = memoize(func3)
# 等等
我们还没有使用 Pythonic 的方式来编写装饰器。而不是写声明
fib = memoize(fib)
我们应该“装饰”我们的 fib 函数:
@memoize
但是在我们的示例 fib() 中,这一行必须直接位于装饰函数的前面。Pythonic 方式的完整示例现在如下所示:
DEF memoize的(˚F ):
备忘录 = {}
DEF 助手(X ):
如果 X 不 在 备忘录:
备忘录[ X ] = ˚F (X )
返回 备忘录[ X ]
返回 辅助
@memoize
DEF FIB (Ñ ):
如果 Ñ == 0 :
返回 0
elif n == 1 :
返回 1
否则:
返回 fib ( n - 1 ) + fib ( n - 2 )
打印( fib ( 40 ))
输出:
102334155
使用可调用类进行记忆
到目前为止还不了解面向对象的人可以毫无问题地跳过这一章。
我们也可以将结果缓存封装在一个类中,如下例所示:
类 Memoize :
def __init__ ( self , fn ):
self 。fn = fn
自我。备忘录 = {}
DEF __call__ (自, * ARGS ):
如果 ARGS 未 在 自我。备忘录:
自我。备忘录[ args ] = self 。fn ( * args )
返回 self 。备忘录[ args ]
@Memoize
def fib ( n ):
if n == 0 :
return 0
elif n == 1 :
return 1
else :
return fib ( n - 1 ) + fib ( n - 2 )
print ( fib ( 40 ))
输出:
102334155
当我们使用字典时,我们不能使用可变参数,即参数必须是不可变的。
锻炼
- 我们的练习是一个古老的谜语,可以追溯到 1612 年。法国耶稣会士 Claude-Gaspar Bachet 提出了它。我们必须称量 1 到 40 磅的数量(例如糖或面粉)。可以在天平上使用的最少砝码数量是多少?
第一个想法可能是使用 1、2、4、8、16 和 32 磅的重量。这是一个最小的数字,如果我们限制自己在一侧放重物而在另一侧放东西,例如糖。但是可以在秤的两个盘子上放置砝码。现在,我们只需要四个权重,即 1, 3, 9, 27
编写一个 Python 函数 weight(),它计算所需的重量及其在平底锅上的分布,以称量从 1 到 40 的任何数量。
解决方案
我们需要线性组合一章中的函数 linear_combination() 。
def factor_set ():
for i in [ - 1 , 0 , 1 ]:
for j in [ - 1 , 0 , 1 ]:
for k in [ - 1 , 0 , 1 ]:
for l in [ - 1 , 0 , 1 ]:
产量 ( i , j , k , l )
def memoize的(˚F ):
结果 = {}
DEF 助手(Ñ ):
如果 ñ 未 在 结果:
结果[ Ñ ] = ˚F (Ñ )
返回 的结果[ Ñ ]
返回 辅助
@memoize
DEF linear_combination (Ñ ):
“”,”返回元组 (i,j,k,l) 满足
n = i*1 + j*3 + k*9 + l*27 """ 权
重 = ( 1 , 3 ,9 ,27 )
为 因子 在 factors_set ():
总和 = 0
对于 我 在 范围(len个(因素)):
总和 + = 因素[我] * 晕死[我]
如果 总和 == Ñ :
返回 因素
有了这个,很容易编写我们的函数 weight()。
DEF 称重(磅):
权重 = (1 , 3 , 9 , 27 )
的标量 = linear_combination (磅)
左 = “”
右 = “”
为 我 在 范围(len个(标量)):
如果 标量[我] == - 1 :
left += str ( weights [ i ]) + " "
elif 标量[ i ] == 1 :
right += str ( weights [ i ]) + " "
return ( left , right )
for i in [ 2 , 3 , 4 , 7 , 8 , 9 , 20 , 40 ]:
pans = weight ( i )
print ( "Left pan:" + str ( i ) + " plus " + pans [ 0 ])
print ( "Right pan:" + pans [ 1 ] + " \n " )
输出:
左锅:2加1 右锅:3 左锅:3加 右锅:3 左锅:4 加 右锅:1 3 左锅:7 加 3 右锅:1 9 左锅:8 加 1 右锅:9 左锅:9加 右锅:9 左锅:20 加 1 9 右锅:3 27 左锅:40+ 右锅:1 3 9 27