使用记忆化与不使用记忆化的递归

2024-04-26

我在学校做的作业是用递归计算加泰罗尼亚数: 第一个没有记忆

def catalan_rec(n):
res = 0
if n == 0:
    return 1
else:
    for i in range (n):
        res += (catalan_rec(i))*(catalan_rec(n-1-i))
    return res

第二个:

def catalan_mem(n, memo = None):
    if memo == None:
        memo = {0: 1}
    res = 0
    if n not in memo:
        for i in range (n):
            res += (catalan_mem(i))*(catalan_mem(n-1-i))
        memo[n] = res
    return memo[n]

最奇怪的事情发生在我身上: 记忆需要两倍的时间!当它应该是相反的时候!

有人可以向我解释一下吗?


这个问题激发了我研究各种加泰罗尼亚数字算法和各种记忆方案的相对速度。下面的代码包含问题中给出的递归算法的函数以及一种仅需要一次递归调用的更简单的算法,这也很容易迭代实现。还有一个基于二项式系数的迭代版本。所有这些算法都在维基百科文章中给出加泰罗尼亚语号码 https://en.wikipedia.org/wiki/Catalan_number.

对于大多数记忆版本来说,获得准确的时间并不容易。通常当使用timeit模块一对要测试的函数执行多个循环,但由于缓存的原因,这里并没有给出真实的结果。为了获得真实的结果需要清除缓存,虽然这可能有点混乱和缓慢,所以缓存清除需要在计时过程之外完成,以避免将缓存清除的开销添加到实际加泰罗尼亚数字的时间上计算。因此,此代码通过简单地计算一个大的加泰罗尼亚数字来生成计时信息,无需循环。

除了计时代码之外,还有一个功能,verify(),它验证所有加泰罗尼亚数字函数产生相同的结果,并且有一个函数可以打印每个加泰罗尼亚数字函数的字节码。这两个函数都已被注释掉。注意verify()填充缓存,因此调用verify() before time_test()会导致计时信息无效。

下面的代码是使用 Python 2.6.6 编写和测试的,但它也可以在 Python 3.6.0 上正确运行。

#!/usr/bin/env python

''' Catalan numbers

    Test speeds of various algorithms

    1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862, 16796, 58786, 208012, 742900, ...

    See https://en.wikipedia.org/wiki/Catalan_number
    and http://stackoverflow.com/q/33959795/4014959

    Written by PM 2Ring 2015.11.28
'''

from __future__ import print_function, division

from timeit import Timer
import dis


#Use xrange if running on Python 2
try:
    range = xrange
except NameError:
    pass

def catalan_rec_plain(n):
    ''' no memoization. REALLY slow! Eg, 26 seconds for n=16 '''
    if n < 2:
        return 1

    res = 0
    for i in range(n):
        res += catalan_rec_plain(i) * catalan_rec_plain(n-1-i)
    return res


#Most recursive versions have recursion limit: n=998, except where noted
cache = {0: 1}
def catalan_rec_extern(n):
    ''' memoize with an external cache '''
    if n in cache:
        return cache[n]

    res = 0
    for i in range(n):
        res += catalan_rec_extern(i) * catalan_rec_extern(n-1-i)
    cache[n] = res
    return res


def catalan_rec_defarg(n, memo={0: 1}):
    ''' memoize with a default keyword arg cache '''
    if n in memo:
        return memo[n]

    res = 0
    for i in range(n):
        res += catalan_rec_defarg(i) * catalan_rec_defarg(n-1-i)
    memo[n] = res
    return res


def catalan_rec_funcattr(n):
    ''' memoize with a function attribute cache '''
    memo = catalan_rec_funcattr.memo
    if n in memo:
        return memo[n]

    res = 0
    for i in range(n):
        res += catalan_rec_funcattr(i) * catalan_rec_funcattr(n-1-i)
    memo[n] = res
    return res

catalan_rec_funcattr.memo = {0: 1}


def make_catalan():
    memo = {0: 1}
    def catalan0(n):
        ''' memoize with a simple closure to hold the cache '''
        if n in memo:
            return memo[n]

        res = 0
        for i in range(n):
            res += catalan0(i) * catalan0(n-1-i)
        memo[n] = res
        return res
    return catalan0

catalan_rec_closure = make_catalan()
catalan_rec_closure.__name__ = 'catalan_rec_closure'


#Simple memoization, with initialised cache
def initialise(memo={}):    
    def memoize(f):
        def memf(x):
            if x in memo:
                return memo[x]
            else:
                res = memo[x] = f(x)
                return res
        memf.__name__ = f.__name__
        memf.__doc__ = f.__doc__
        return memf
    return memoize

#maximum recursion depth exceeded at n=499
@initialise({0: 1})
def catalan_rec_decorator(n):
    ''' memoize with a decorator closure to hold the cache '''
    res = 0
    for i in range(n):
        res += catalan_rec_decorator(i) * catalan_rec_decorator(n-1-i)
    return res

# ---------------------------------------------------------------------

#Product formula
# C_n+1 = C_n * 2 * (2*n + 1) / (n + 2)
# C_n = C_n-1 * 2 * (2*n - 1) / (n + 1)

#maximum recursion depth exceeded at n=999
def catalan_rec_prod(n):
    ''' recursive, using product formula '''
    if n < 2:
        return 1
    return (4*n - 2) * catalan_rec_prod(n-1) // (n + 1)

#Note that memoizing here gives no benefit when calculating a single value
def catalan_rec_prod_memo(n, memo={0: 1}):
    ''' recursive, using product formula, with a default keyword arg cache '''
    if n in memo:
        return memo[n]
    memo[n] = (4*n - 2) * catalan_rec_prod_memo(n-1) // (n + 1)
    return memo[n]


def catalan_iter_prod0(n):
    ''' iterative, using product formula '''
    p = 1
    for i in range(3, n + 2):
        p *= 4*i - 6 
        p //= i 
    return p


def catalan_iter_prod1(n):
    ''' iterative, using product formula, with incremented m '''
    p = 1
    m = 6
    for i in range(3, n + 2):
        p *= m
        m += 4 
        p //= i 
    return p

#Add memoization to catalan_iter_prod1
@initialise({0: 1})
def catalan_iter_memo(n):
    ''' iterative, using product formula, with incremented m and memoization '''
    p = 1
    m = 6
    for i in range(3, n + 2):
        p *= m
        m += 4 
        p //= i 
    return p

def catalan_iter_prod2(n):
    ''' iterative, using product formula, with zip '''
    p = 1
    for i, m in zip(range(3, n + 2), range(6, 4*n + 2, 4)):
        p *= m
        p //= i 
    return p


def catalan_iter_binom(n):
    ''' iterative, using binomial coefficient '''
    m = 2 * n
    n += 1
    p = 1
    for i in range(1, n):
        p *= m
        p //= i
        m -= 1
    return p // n


#All the functions, in approximate speed order
funcs = (
    catalan_iter_prod1,
    catalan_iter_memo,
    catalan_iter_prod0,
    catalan_iter_binom,
    catalan_iter_prod2,

    catalan_rec_prod,
    catalan_rec_prod_memo,
    catalan_rec_defarg,
    catalan_rec_closure,
    catalan_rec_extern,
    catalan_rec_decorator,
    catalan_rec_funcattr,
    #catalan_rec_plain,
)

# ---------------------------------------------------------------------

def show_bytecode():
    for func in funcs:
        fname = func.__name__
        print('\n%s' % fname)
        dis.dis(func)

#Check that all functions give the same results
def verify(n):
    range_n = range(n)
    #range_n = [n]
    func = funcs[0]
    table = [func(i) for i in range_n]
    #print(table)
    for func in funcs[1:]:
        print(func.__name__, [func(i) for i in range_n] == table)

def time_test(n):
    ''' Print timing stats for all the functions '''
    res = []
    for func in funcs:
        fname = func.__name__
        print('\n%s: %s' % (fname, func.__doc__))
        setup = 'from __main__ import cache, ' + fname
        cmd = '%s(%d)' % (fname, n)
        t = Timer(cmd, setup)
        r = t.timeit(1)
        print(r)
        res.append((r, fname))

    ##Sort results from fast to slow
    #print()
    #res.sort()
    #for t, fname in res:
        #print('%s:\t%s' % (fname, t))
        ##print('%s,' % fname)


#show_bytecode()

#verify(50)
#verify(997)

time_test(450)

#for i in range(20):
    #print('%2d: %d' % (i, catalan_iter_binom(i)))

典型结果

catalan_iter_prod1:  iterative, using product formula, with incremented m 
0.00119090080261

catalan_iter_memo:  iterative, using product formula, with incremented m and memoization 
0.001140832901

catalan_iter_prod0:  iterative, using product formula 
0.00202202796936

catalan_iter_binom:  iterative, using binomial coefficient 
0.00141906738281

catalan_iter_prod2:  iterative, using product formula, with zip 
0.00123286247253

catalan_rec_prod:  recursive, using product formula 
0.00263595581055

catalan_rec_prod_memo:  recursive, using product formula, with a default keyword arg cache 
0.00210690498352

catalan_rec_defarg:  memoize with a default keyword arg cache 
0.46977186203

catalan_rec_closure:  memoize with a simple closure to hold the cache 
0.474807024002

catalan_rec_extern:  memoize with an external cache 
0.47812795639

catalan_rec_decorator:  memoize with a decorator closure to hold the cache 
0.47876906395

catalan_rec_funcattr:  memoize with a function attribute cache 
0.516775131226

上述结果是由 2GHz Pentium 4 在最小系统负载下产生的。然而,每次运行之间存在相当大的差异,尤其是对于更快的算法。

正如您所看到的,对于问题中使用的双重递归算法来说,使用缓存的默认参数实际上是一个很好的方法。因此,递归版本的清理版本是:

def catalan_rec(n, memo={0: 1}):
    ''' recursive Catalan numbers, with memoization '''
    if n in memo:
        return memo[n]

    res = 0
    for i in range(n):
        res += catalan_rec_defarg(i) * catalan_rec_defarg(n-1-i)
    memo[n] = res
    return res

然而,它是much使用迭代算法之一更有效,例如catalan_iter_prod1。如果您打算多次调用该函数并且很可能出现重复的参数,那么请使用记忆版本,catalan_iter_memo.

总之,我应该提到,最好避免递归,除非它适合问题域(例如,当使用树等递归数据结构时)。 Python无法执行尾调用消除 https://en.wikipedia.org/wiki/Tail_call并且它施加了递归限制。因此,如果有迭代算法,它几乎总是比递归算法更好的选择。当然,如果您正在学习递归并且您的老师希望您编写递归代码,那么您就没有太多选择。 :)

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用记忆化与不使用记忆化的递归 的相关文章

随机推荐

  • 关于冒号的简单C++语法问题

    我刚刚看到一个代码片段 其中有一段我以前从未见过的语法 什么是bool start 1 意思是 我在头文件的类定义中找到了它 struct record char name int refcount 4 unsigned dirty 1 这
  • selenium.common.exceptions.WebDriverException:消息:未知错误:无法使用 ChromeDriver Chrome Selenium 创建 Chrome 进程错误

    我正在尝试编写基本的 python Google Chrome 与 webdriver 交互的代码 但在尝试在浏览器上启动链接时不断遇到相同的错误 这是我的代码 from selenium import webdriver import o
  • php:将变量内容下载为文件

    题主可以吗 我有一个正在执行的脚本 有一次 我在变量中有一大段文本 我可以将其作为可下载文件提供 而不实际将变量内容写入磁盘吗 如果您的意思是让用户单击链接并弹出一个对话框以将某些内容保存为文本文件
  • Graphql 创建两个查询之间的关系。错误无法在初始化之前访问

    我有这个代码 const ProductType new GraphQLObjectType name Product fields id type GraphQLID name type GraphQLString category ty
  • 批处理文件中的 URL 解码

    如何在批处理文件中 urldecode 以下字符串 我需要更改以下内容 http x3a x2f x2f www example com x2f some page x2f some x2f link html to this http w
  • 数组运算符 [] 重载 const 和非 const 版本

    我接到一个任务来实现一个模板数组类 要求之一是重载 运算符 我制作了这两个常量和非常量版本 似乎工作正常 const T operator const unsigned int index const and T operator cons
  • 散列到分组数组中

    我对 ruby 的经验不是很丰富 所以我正在努力格式化一段数据 我有这个哈希 其中包含一些具有相同值的键 例如 key gt value1 key2 gt value2 key3 gt value3 key4 gt value1 key5
  • 如何修补 Eclipse 插件?

    我想修复 eclipse 插件 WTP 的官方插件 中的错误 我在本地更改了源代码 对其进行了调试 一切都很好 现在我想将此更改传播到我的 Eclipse 安装 但我遇到了问题 似乎有不止一种方法可以实现这一目标 例如 这个网站 http
  • 从闪亮发送电子邮件

    我是一位新的 Shiny 用户 我有兴趣创建一个 Web 应用程序 访问者可以在其中填写一些问题 取决于随机 R 数据 并可以提交它们 我的问题是找到通过电子邮件向我发送该信息的方法 例如 每次他们提交数据时 我是一名大学讲师 我认为这是评
  • Mac OS X 上的 scp 问题:scp 不喜欢文件名中的空格,“\”修复不起作用

    我正在尝试使用 scp 在两台 Mac 操作系统 10 6 8 之间传输文件 但它失败了 因为我的目录 文件名中有空格 我无法更改目录 文件名 当我使用 Mac 在终端中工作时 我经常使用 符号来表示空格 然而 在这种情况下 它不起作用 我
  • 以编程方式将网页保存到静态 HTML 文件的最佳方法

    我做的研究越多 前景就越黯淡 我正在尝试使用 Python 进行平面保存或静态保存网页 这意味着将所有样式合并到内联属性 并将所有链接更改为绝对 URL 我尝试过几乎所有免费的转换网站 api 甚至 github 上的库 没有一个是那么令人
  • URL 扩展隐藏:重写与重定向

    我已经阅读了很多问题和答案 但我无法决定哪一个更好或如何使用这些扩展隐藏方式的组合 我想要的是就是有一个像这样的url重写堆栈溢出 那么我还应该做什么才能遵守这些规则 url example com file anyEXT show con
  • 从哪里可以获得 Microsoft.DirectX.dll?

    我不知道从哪里得到它 只是微软的 SDK 吗 如果是 如何将其添加到我的项目 Visual Studios 2010 C 中 托管 DirectX 包装器已过时 不再包含在 DirectX SDK 中 最后一个版本是 2010 年 2 月发
  • null对象的hashcode是如何计算的

    由于 HashMap 和 HashSet 允许空对象 那么这些 空 对象的哈希值是多少 java中如何计算 处理它 openJDK 内置的 HashMap 只是将空引用放入数组中用于保存条目的第一个存储桶中 409 private V pu
  • java中引用和对象有什么区别? [复制]

    这个问题在这里已经有答案了 我有类 GUI 所以我可以创建这样的对象 GUI g1 new GUI 和一个像这样的引用变量 GUI g2 现在据我所知 g2 是一个引用变量 它引用 GUI 类 而 g1 是 GUI 类的对象 g1和g2有什
  • 用户名作为路径

    我希望将用户名作为 URL 的一部分 例如mysite com 用户名 这应该重定向到用户配置文件 我用简介2 http drupal org project profile2 and Pathauto http drupal org pr
  • 未选中选项卡下划线的 TabLayout 颜色

    在此图中 在选项卡布局中 选定的选项卡栏下划线颜色为紫色 并且文本 我搜索未选择的标签栏 但找不到未选择的标签栏下划线 我想在选择某个选项卡时更改颜色 更改未选择的选项卡下划线颜色 如果你知道这件事 你会帮助我吗 在drawable文件夹中
  • React Hook“useState”在函数“setResults”中调用,该函数既不是 React 函数组件,也不是自定义 React Hook 函数

    我试图在一个功能组件中进行 API 调用 该组件是一个反应钩子 并根据结果重新渲染组件的内容 这是代码 调用 API 的组件 function IntegrationDownshift render
  • Delphi:MDI应用程序中的最大化子窗体

    如何最大化仅适合客户区而不适合整个父窗口的子窗口 我不希望子窗口在父窗口的主菜单或其他控件下消失 我有这个代码 procedure WMSIZE var Msg TMessage message WM SIZE procedure TFor
  • 使用记忆化与不使用记忆化的递归

    我在学校做的作业是用递归计算加泰罗尼亚数 第一个没有记忆 def catalan rec n res 0 if n 0 return 1 else for i in range n res catalan rec i catalan rec