【问题标题】:danger of recursive functions递归函数的危险
【发布时间】:2018-02-15 00:54:09
【问题描述】:

经常有人说python中不建议使用递归函数(递归深度限制、内存消耗等)

我从this question 中获取了一个排列示例。

def all_perms(str):
  if len(str) <=1:
    yield str
  else:
    for perm in all_perms(str[1:]):
        for i in range(len(perm)+1):
            yield perm[:i] + str[0:1] + perm[i:]

后来我把它改成了非递归的版本(我是python新手)

def not_recursive(string):
  perm = [string[0]]
  for e in string[1:]:
    perm_next = []
    for p in perm:
        perm_next.extend(p[:i] + e + p[i:] for i in range(len(p) + 1))
    perm = perm_next

  for p in perm:
    yield p

我比较了它们

before=time()
print len([p for p in all_perms("1234567890")])
print "time of all_perms %i " % (time()-before)

before=time()
print len([p for p in not_recursive("1234567890")])
print "time of not_recursive %i " % (time()-before)

before=time()
print len([p for p in itertools.permutations("1234567890")])
print "time of itertools.permutations %i " % (time()-before)

结果很有趣。递归函数最快5秒,非递归8秒,内建35秒。

那么递归函数 在 Python 中不好用吗?内置 itertools.permutations 有什么问题?

【问题讨论】:

  • 我们可以假设您检查了三个函数的输出是否相同?
  • 至少大小是一样的。我检查了 3 个元素的完整列表,它们是相同的

标签: python recursion


【解决方案1】:

递归在 Python 中“不好”,因为它通常比迭代解决方案慢,而且 Python 的堆栈深度不是无限的(并且没有尾调用优化)。对于 sum 函数,是的,您可能想要无限的深度,因为想要对一百万个数字的列表求和是完全合理的,并且性能增量将成为如此大量项目的问题。在这种情况下,您不应该使用递归。

另一方面,如果您正在遍历从 XML 文件读取的 DOM 树,则不太可能超过 Python 的递归深度(默认为 1000)。它当然可以, 但实际上,它可能不会。当您提前知道您将使用哪些类型的数据时,您可以确信您不会溢出堆栈。

在我看来,递归树遍历比迭代遍历更自然,而且递归开销通常只占运行时间的一小部分。如果您认为需要 16 秒而不是 14 秒对您来说真的很重要,那么将 PyPy 扔给它可能会更好地利用您的时间。

递归似乎很适合您发布的问题,如果您认为这样的代码更易于阅读和维护,并且性能足够,那就去吧。

我是在计算机上编写代码长大的,实际上,如果提供的话,递归深度限制在 16 左右,所以 1000 对我来说似乎很奢侈。 :-)

【讨论】:

  • 有没有无限堆栈的语言?
  • 有些语言只支持受内存限制的递归,也有可以优化尾递归的语言。大多数函数式语言两者兼而有之。 (我只说“大多数”,因为我不是 100% 确定没有任何不这样做的。)
【解决方案2】:

递归很好可以解决那些需要干净、清晰、递归实现的问题。但是像所有编程一样,您必须执行一些算法分析以了解性能特征。在递归的情况下,除了操作数之外,您还必须估计最大堆栈深度。

大多数问题发生在新程序员认为递归很神奇而没有意识到下面有一个堆栈使之成为可能时。新程序员也被称为分配内存并且从不释放它,以及其他错误,因此递归在这种危险中并不是唯一的。

【讨论】:

  • +1 始终检查算法的要求,如果它使您的堆栈切换到使用您自己的堆栈的迭代解决方案。
  • 在 Python 中不太可能出现“分配内存并且从不释放它”的情况。 ;-)
【解决方案3】:

你的时间完全错了:

def perms1(str):
  if len(str) <=1:
    yield str
  else:
    for perm in perms1(str[1:]):
        for i in range(len(perm)+1):
            yield perm[:i] + str[0:1] + perm[i:]

def perms2(string):
  perm = [string[0]]
  for e in string[1:]:
    perm_next = []
    for p in perm:
        perm_next.extend(p[:i] + e + p[i:] for i in range(len(p) + 1))
    perm = perm_next

  for p in perm:
    yield p

s = "01235678"
import itertools
perms3 = itertools.permutations

现在用 timeit 测试一下:

thc:~$ for i in 1 2 3; do echo "perms$i:"; python -m timeit -s "import permtest as p" "list(p.perms$i(p.s))"; done 
perms1:
10 loops, best of 3: 23.9 msec per loop
perms2:
10 loops, best of 3: 39.1 msec per loop
perms3:
100 loops, best of 3: 5.64 msec per loop

如您所见,itertools.permutations 是迄今为止最快的,其次是递归版本。

但这两个纯 Python 函数都没有机会很快,因为它们执行昂贵的操作,例如添加列表ala perm[:i] + str[0:1] + perm[i:]

【讨论】:

  • 谢谢。我也做了类似的测试,但使用 s = "012356789" 并启用 gc。在这种情况下,内置排列比递归排列慢。你知道为什么吗?
【解决方案4】:

我无法重现您的计时结果(在 Mac OS X 上的 Python 2.6.1 中):

>>> import itertools, timeit
>>> timeit.timeit('list(all_perms("0123456789"))', 
...               setup='from __main__ import all_perms'),
...               number=1)
2.603626012802124
>>> timeit.timeit('list(itertools.permutations("0123456789"))', number=1)
1.6111600399017334

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-12-01
    • 2011-07-16
    • 1970-01-01
    • 2021-12-11
    • 1970-01-01
    • 2013-07-24
    • 2023-03-08
    相关资源
    最近更新 更多