【问题标题】:Recursion applied to lists vs trees in Python递归应用于 Python 中的列表与树
【发布时间】:2021-03-28 13:46:00
【问题描述】:

我对递归的主要关注是 Python 中的递归限制,我认为是 1000。考虑到这一点,我想讨论两种情况:

场景 1:对平衡树应用递归(二叉树)

例如,在树中搜索最大值:

class Node:
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right

def max(root):
    max_left = max(root.left)
    max_right = max(root.right)
    if max_left > root.value and max_left > max_right:
        return max_left
    if max_right > root.value and max_right > max_left:
        return max_right
    else:
        return root.value

在这里,在任何给定时间堆栈中的最大调用数将是树的高度,即 log_2(n),其中 n 是列表中的元素数。鉴于 Python 中的限制是 1000 次调用,树可以存储多达 2^1000(或 2^999)个元素而不会达到调用限制。对我来说,这不是一个真正的限制,所以我假设我们可以在这里使用递归。

场景 2:对列表应用递归

一个虚拟的例子是计算一个列表的最大值,因此我们返回列表的头部和列表其余部分的相同函数的结果之间的最大值:

def max(l):
    if len(l) == 1:
        return l[0]
    max_tail = max(l[1:])
    if l[0] > max_tail:
        return l[0]
    else:
        return max_tail

输出:

>>> max(list(range(998)))
997
>>> max(list(range(999)))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in max
  File "<stdin>", line 4, in max
  File "<stdin>", line 4, in max
  [Previous line repeated 995 more times]
  File "<stdin>", line 2, in max
RecursionError: maximum recursion depth exceeded while calling a Python object

所以我的理解是,对于列表递归不是一个合理的选择,因为它通常不能处理大于 999(甚至更少,取决于堆栈跟踪)的列表。

现在,我的问题:

  1. 使用递归处理平衡树是否合理?
  2. 对于大多数问题,它确实不是一个选项(列表、非平衡树等)吗?
  3. 还有什么我应该考虑的吗?我只是想更多地了解在使用 Python 时,递归何时是一般的好选择。

【问题讨论】:

  • 如果你计算一个特定大小的平衡树需要多少次递归,并将其与递归限制进行比较,你会得到 1 的答案。
  • @Carcigenicate 在 Python 函数调用限制方面我们没问题。但是使用迭代版本不是更节省空间吗?你们通常对此类任务使用递归吗?
  • 对于树的第一种情况,如果我确定没有办法超过限制,是的,我会在那里使用递归。这比我自己维护一个堆栈来做同样的工作要整洁得多。如果我发现空间是一个问题,那么我会在处理之后将其转换为迭代版本。对于列表,不,我绝对不会使用递归。简单的迭代会好很多。

标签: python recursion big-o space-complexity


【解决方案1】:

首先,请记住,一种语言与一种语言的实现不同。 Python 中的调用堆栈大小因实现而异。 1000 的限制适用于 CPython,但不一定适用于所有实现。


使用递归处理平衡树是否合理?

递归对于平衡树是合理的。正如您已经非常清楚地描述的那样,最大调用堆栈深度是结构大小的对数因子。你需要大量的输入来破坏堆栈。

对于列表或退化不平衡树,递归是不合适的,因为最大调用堆栈深度与结构长度成线性关系。

也就是说,我发现没有必要在 Python 中经常使用自平衡树。大多数工作都是在列表和字典上完成的,偶尔会嵌套和递归定义。递归恰好适合树等结构这一事实并不意味着它在 Python 中普遍适用。


对于大多数问题,它真的不是一个选项(列表、非平衡树等)吗?

是的,但是 CPython 中的设计禁止递归。 Python 是not a functional language。 CPythondoesn't support tail call optimization and "never" will.

Guido 有一个很棒的 blog post 来捍卫 CPython 的反递归设计。无论您是否同意这种设计,工具通常都应按预期使用,而不是按人们希望的那样使用。

在紧要关头,Python(作为一种多范式语言)可以支持以函数式风格编写的代码,并且有 workarounds to make long recursion work,但这并不能改变它们是变通方法的事实。


还有什么我应该在这里考虑的吗?我只是想了解更多关于在使用 Python 时递归通常是什么时候的好选择。

CPython 中的函数调用比迭代具有更多的开销(时间和空间),因此即使结构大小适合调用堆栈或使用支持深度递归的技巧,也需要考虑性能问题。

Using setrecursionlimit is unsafe and almost never necessary。大多数语言都没有这样的选项。增加它意味着您冒着操作系统杀死 CPython 解释器的风险。当然,摆弄限制对于快速脚本和调试会派上用场,但这不是一个通用的解决方案。

标签充斥着来自善意的 CS 学生的问题,他们的任务是使用 Python 和其他非函数式语言解决递归similar to your example that blows the stack 问题。这些帖子的数量和学生的困惑表明,CS教育系统在推动递归作为迭代的默认选项方面发挥了作用。递归的优雅程度取决于为它设计的语言的程度。事实上,递归往往会让学生感到困惑,这更多是因为它误用在命令式语言中,在循环之后,递归是二等公民,而不是递归本身固有的任何东西。

除了学校作业、算法编码挑战和罕见的实际业务应用程序使用之外,您可能永远不需要 Python 中的递归。但是如果你有一把锤子,一切看起来都像钉子,所以这并不是说你不能对你看到的每一个问题都应用递归,而是你可能不应该这样做。

递归思维 非常有价值,这并不是对作为一般工具的递归的攻击,只是认识到 Python 特别不适合使用它(就像大多数其他命令式语言一样)。


与递归无关,我知道您的代码只是一个演示,但max 是一个内置函数,所以我会选择一个不同的名称。单独来看,max(list(range(999))) 看起来应该可以工作。

【讨论】:

    【解决方案2】:

    如果您真的想以递归方式编写代码,您可以在 Python 中进行蹦床。但是以迭代方式编写代码要现实得多。

    您可以创建基于生成器的蹦床函数,如本博文所示:

    def tramp(gen, *args, **kwargs):
        g = gen(*args, **kwargs)
        while isinstance(g, types.GeneratorType):
            g=g.next()
        return g
    

    您还可以在 Python 中更改递归限制,但并不总是推荐,而且很少被称为合适的解决方案;但这是可能的。

    from sys import setrecursionlimit
    setrecursionlimit(10**8)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-11-11
      • 1970-01-01
      • 2020-06-12
      • 1970-01-01
      • 1970-01-01
      • 2013-07-01
      • 2012-08-23
      • 1970-01-01
      相关资源
      最近更新 更多