【问题标题】:Returning list of different results that are created recursively in Python返回在 Python 中递归创建的不同结果的列表
【发布时间】:2022-01-02 18:33:59
【问题描述】:

最近我一直在处理 Python 中的一些递归问题,我必须使用递归生成可能的配置列表(即给定字符串的排列列表、子字符串列表等)。我很难找到最佳实践,也很难理解如何在递归中管理这种变量。

我将给出generate binary trees 问题的示例。我或多或少知道我必须在递归中实现什么:

  1. 如果 n=1,则只返回一个节点。
  2. 如果 n=3,则返回唯一可能的二叉树。
  3. 对于 n>3,创建一个节点,然后探索可能性:左节点没有子节点,右节点没有子节点,两个节点都没有子节点。递归探索这些可能性。

现在我最难以想象的事情是我将如何准确地到达树木列表。目前我所做的做法是在函数调用中传递一个列表(作为参数)并且该函数将返回这个列表,但是问题是在调用递归函数以探索节点的可能性时的情况 3 它将是返回一个列表,而不是将节点附加到我正在构建的树上。当我在脑海中描绘递归树时,我想象一个“树”变量对每个树叶都是唯一的,并且这些树被添加到由“根”(即第一个)调用返回的列表中。但我不知道这是否可能。我想到了一个全局列表和递归函数不返回任何东西(只是附加到它),但我认为问题是在每次调用时函数都会收到变量的副本。

在这些递归情况下,我该如何处理生成组合和返回配置列表?虽然我举了一个例子,但答案越笼统越好。我也想知道在这方面是否有“最佳实践”。

【问题讨论】:

  • "目前我的做法是在函数调用中传递一个列表(作为参数),然后函数将返回这个列表,"嗯,列表代表什么?各个元素代表什么?实际的策略是什么?
  • 欢迎回到 Stack Overflow。作为复习,请阅读How to Ask。这个问题要是再具体点就更好了。包含实际代码很好,但它也可以解释如何您打算在代码中使用该列表。我不知道您所说的“递归树”是什么意思。我们通常也不处理有关“最佳实践”的问题。
  • @KarlKnechtel 谢谢。你能否详细说明什么会被认为是“更具体的”?我没有添加代码,因为这会使问题过于具体。这里展示了一个递归树:cs.cornell.edu/courses/cs3110/2012sp/lectures/lec20-master/…
  • @KarlKnechtel 在第一条评论中回答您的问题:该列表将代表问题示例中可能的二叉树上的可能配置(例如排列)。例如,如果我要返回每个可能的二叉树的根,我将返回一个根节点列表。
  • 重新树:我猜它们只是意味着绘制出在递归算法期间发生的所有调用;但该页面似乎更倾向于确定算法的大 O 复杂性,而不是弄清楚如何编写它们。

标签: python list recursion return


【解决方案1】:

目前我的做法是在函数调用中传递一个列表(作为参数),函数会返回这个列表

这不是解决递归问题的最纯粹的方法。如果您可以制作递归函数以解决子问题而不需要它必须使用的额外参数变量,那就更好了。所以递归函数应该只返回一个结果好像它是唯一的调用(由测试框架)。所以在这个例子中,递归调用应该返回一个带有树的列表。

或者,递归函数可以是一个不返回列表但产生单个值的子函数(在本例中为树)。然后调用者可以决定是否将其打包到列表中。这更像是pythonic。

对于示例问题,识别一些不变量也很重要。例如,很明显当 n 为偶数时没有解。至于递归方面:一旦你决定创建一个根,那么它的左右子树都会有奇数个节点。当然,这是针对此问题的特定观察,但寻找此类问题的属性很重要。

最后,同样重要的是查看相同的子问题是否会重复出现多次。在示例问题中肯定就是这种情况:例如,左子树有时可能与右子树具有相同数量的节点。在这种情况下,记忆将提高效率(动态编程)。

当递归函数返回一个列表时,调用者可以迭代该列表以检索其元素(示例中的树),并使用它们来构建满足调用者任务的扩展结果。在示例情况下,这意味着从递归检索的列表中获取的树作为子节点附加到新的根节点。然后这个新树被附加到一个 new 列表(与递归调用返回的无关)。这个新列表在很多情况下会更长,尽管这取决于问题的类型。

为了进一步说明解决这些问题的方法,这里有一个示例问题的解决方案:使用 main 函数进行递归调用,并使用记忆:

class Solution:
    memo = { 1: [TreeNode()] }
    
    def allPossibleFBT(self, n: int) -> List[Optional[TreeNode]]:
        # If we didn't solve this problem before...
        if n not in self.memo:
            # Create a list for storing the results (the trees)
            results = []
            # Before creating any root node, 
            #    decide the size of the left subtree.
            #    It must be odd
            for num_left in range(1, n, 2):
                # Make the recursive call to get all shapes of the
                # left subtree
                left_shapes = self.allPossibleFBT(num_left)
                # The remainder of the nodes must be in the right subtree
                num_right = n - 1 - num_left  # The root also counts as 1
                right_shapes = self.allPossibleFBT(num_right)
                # Now iterate the results we got from recursion and 
                #    combine them in all possible ways to create new trees
                for left in left_shapes:
                    for right in right_shapes:
                        # We have a combination. Now create a new tree from it
                        # by putting a root node on top of the two subtrees:
                        tree = TreeNode(0, left, right)
                        # Append this possible shape to our results
                        results.append(tree)
            # All done. Save this for later re-use
            self.memo[n] = results
        return self.memo[n]

使用列表推导可以使这段代码更紧凑,但它可能会降低代码的可读性。

【讨论】:

  • 我知道记忆和这个问题的奇偶属性,但这不是我的问题。我难以理解的是如何编写返回语句。例如考虑 n=5,我必须返回一个节点列表。我为每个子节点调用 n=3 的函数。这将返回 n=3 的解决方案列表,但如果我正在构建树,我需要实际的节点。至于没有任何解释的代码我觉得很难理解。
  • 好的,我将扩展我的答案并专注于您在评论中提到的内容。等等。
  • 已更新。如果这能回答您的疑问,请告诉我。
  • 您高效且乐于助人,我现在更好地理解了,谢谢。迭代结果是我以前没有的见解,但比传递列表并附加到它更有意义。
  • 我真的不明白你卡在哪里了。就像你说你想要一个一般性的答案,但后来我没有根据你对特定问题的描述为你编写代码而感到沮丧。 “这将返回 n=3 的解决方案列表,但如果我正在构建一棵树,我需要实际的节点。”......“迭代结果是我以前没有的洞察力”你是 真的说症结在于你没有想到给定一个子树列表,你可以通过遍历列表来获得子树?
【解决方案2】:

不要将信息传递给递归调用,除非它们需要该信息来计算其本地结果。当你在没有副作用的情况下编写时,推理递归要容易得多。因此,与其让递归调用将其自己的结果放入列表中,不如编写代码以便使用递归调用的结果来创建返回值。

让我们举一个简单的例子,将一个简单的循环转换为递归,并用它来累积一个递增的整数序列。

def recursive_range(n):
    if n == 0:
        return []
    return recursive_range(n - 1) + [n]

我们以自然的方式使用函数:我们将信息与参数一起放入,将信息输出使用返回值(而不是参数的突变) .

在你的情况下:

现在我最难以想象的事情是我将如何准确地到达树列表。

所以你知道你想在过程结束时返回一个树列表。所以自然的方法是,你希望每个递归调用也这样做。

在这些递归情况下,我该如何处理生成组合和返回配置列表?虽然我举了一个例子,但答案越笼统越好。

递归调用返回子问题的结果列表。您可以使用这些结果来创建当前问题的结果列表。

不需要考虑如何实现递归来编写递归算法。您无需考虑调用堆栈。你确实需要考虑两件事:

  • 基本情况是什么?
  • 问题如何递归分解? (或者:为什么递归非常适合这个问题?)

问题是,递归并不特殊。进行递归调用就像调用任何其他函数,它会碰巧为您提供子问题的正确答案。因此,您需要做的就是了解解决子问题如何帮助您解决当前问题。

【讨论】:

  • 我真的不明白这是如何回答我的问题的,我正在阅读更多关于如何从头开始实现递归的指南。当我在我的问题中陈述这两个问题时,您提到知道基本情况是什么以及如何解决问题。也许我的问题不是很清楚,但递归本身“算法上”我可以理解,我遇到的问题是带有 return 语句的 Python 实现。我将在问题中添加代码以使其更清楚。
  • “我遇到的问题是带有 return 语句的 Python 实现”我不明白这是怎么回事。您编写一些逻辑,从递归调用中获取值并计算一个值;然后你return那个值。您说过“答案越笼统越好”。如果不更具体,就不可能变得更模糊。如何将结果组合在一起的细节取决于您要解决的确切问题。
  • 但也许你现在明白我所说的问题的具体性是什么意思了?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-08-31
  • 2021-10-07
  • 2021-08-21
  • 2016-05-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多