简体   繁体   English

返回在 Python 中递归创建的不同结果的列表

[英]Returning list of different results that are created recursively in Python

Lately I've been working with some recursive problems in Python where I have to generate a list of possible configurations (ie list of permutations of a given string, list of substrings, etc..) using recursion.最近我一直在处理 Python 中的一些递归问题,我必须使用递归生成可能的配置列表(即给定字符串的排列列表、子字符串列表等)。 I'm having a very hard time in finding the best practice and also in understanding how to manage this sort of variable in recursion.我很难找到最佳实践,也很难理解如何在递归中管理这种变量。

I'll give the example of the generate binary trees problem.我将给出 生成二叉树问题的示例。 I more-or-less know what I have to implement in the recursion:我或多或少知道我必须在递归中实现什么:

  1. If n=1, return just one node.如果 n=1,则只返回一个节点。
  2. If n=3, return the only possible binary tree.如果 n=3,则返回唯一可能的二叉树。
  3. For n>3, crate one node and then explore the possibilities: left node is childless, right node is childless, neither node is childless.对于 n>3,创建一个节点然后探索可能性:左节点是无子节点,右节点是无子节点,没有一个节点是无子节点。 Explore these possibilites recursively.递归地探索这些可能性。

Now the thing I'm having the most trouble visualising is how exactly I am going to arrive to the list of trees.现在,我最难以想象的事情是我将如何准确地到达树列表。 Currently the practice I do is pass along a list in the function call (as an argument) and the function would return this list, but then the problem is in case 3 when calling the recursive function to explore the possibilites for the nodes it would be returning a list and not appending nodes to a tree that I am building. Currently the practice I do is pass along a list in the function call (as an argument) and the function would return this list, but then the problem is in case 3 when calling the recursive function to explore the possibilites for the nodes it would be返回一个列表,而不是将节点附加到我正在构建的树上。 When I picture the recursion tree in my head I imagine a "tree" variable that is unique to each of the tree leaves, and these trees are added to a list which is returned by the "root" (ie first) call.当我在脑海中描绘递归树时,我想象一个“树”变量,它对每个树叶都是唯一的,这些树被添加到由“根”(即第一个)调用返回的列表中。 But I don't know if that is possible.但我不知道这是否可能。 I thought of a global list and the recursive function not returning anything (just appending to it) but the problem I believe is that at each call the function would receive a copy of the variable.我想到了一个全局列表,递归 function 没有返回任何东西(只是附加到它上面),但我认为问题是在每次调用时 function 都会收到变量的副本。

How can I deal with generating combinations and returning lists of configurations in these cases in recursion?在递归的这些情况下,我该如何处理生成组合和返回配置列表? While I gave an example, the more general the answer the better.虽然我举了一个例子,但答案越笼统越好。 I would also like to know if there is a "best practice" when it comes to that.我还想知道在这方面是否有“最佳实践”。

Currently the practice I do is pass along a list in the function call (as an argument) and the function would return this list目前我所做的做法是在 function 调用(作为参数)中传递一个列表,并且 function 将返回此列表

This is not the purest way to attack a recursive problem.这不是解决递归问题的最纯粹的方法。 It would be better if you can make the recursive function such that it solves the sub problem without an extra parameter variable that it must use.如果您可以制作递归 function 以便解决子问题而无需必须使用额外的参数变量,那就更好了。 So the recursive function should just return a result as if it was the only call that was ever made (by the testing framework).因此,递归 function 应该只返回一个结果,就好像它是唯一的调用(由测试框架)。 So in the example, that recursive call should return a list with trees.所以在这个例子中,递归调用应该返回一个带有树的列表。

Alternatively the recursive function could be a sub-function that doesn't return a list, but yields the individual values (in this case: trees).或者,递归 function 可以是不返回列表但产生单个值(在本例中为树)的子函数。 The caller can then decide whether to pack that into a list or not.然后调用者可以决定是否将其打包到列表中。 This is more pythonic.这更像是pythonic。

As to the example problem, it is also important to identify some invariants.对于示例问题,识别一些不变量也很重要。 For instance, it is clear that there are no solutions when n is even.例如,很明显当n为偶数时没有解。 As to recursive aspect: once you have decided to create a root, then both its left and right sided subtree will have an odd number of nodes.至于递归方面:一旦你决定创建一个根,那么它的左右子树都会有奇数个节点。 Of course, this is an observation that is specific to this problem, but it is important to look for such problem properties.当然,这是针对此问题的特定观察,但寻找此类问题的属性很重要。

Finally, it is equally important to see if the same sub problems can reoccur multiple times.最后,同样重要的是查看相同的子问题是否可以重复出现多次。 This surely is the case in the example problem: for instance, the left subtree may sometimes have the same number of nodes as the right subtree.在示例问题中肯定就是这种情况:例如,左子树有时可能与右子树具有相同数量的节点。 In such cases memoization will improve efficiency (dynamic programming).在这种情况下,记忆将提高效率(动态编程)。

When the recursive function returns a list, the caller can then iterate that list to retrieve its elements (trees in the example), and use them to build an extended result that satisfies the caller's task.当递归 function 返回一个列表时,调用者可以迭代该列表以检索其元素(示例中的树),并使用它们构建满足调用者任务的扩展结果。 In the example case that means that the tree taken from the recursively retrieved list, is appended as a child to a new root.在示例情况下,这意味着从递归检索的列表中获取的树作为子节点附加到新的根节点。 Then this new tree is appended to a new list (not related to the one returned from the recursive call).然后这个新树被附加到一个列表中(与递归调用返回的无关)。 This new list will in many cases be longer, although this depends on the type of problem.这个新列表在许多情况下会更长,尽管这取决于问题的类型。

To further illustrate the way to tackle these problems, here is a solution for the example problem: one which uses the main function for the recursive calls, and using memoization:为了进一步说明解决这些问题的方法,这里是示例问题的解决方案:使用主要 function 进行递归调用,并使用记忆:

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]

This code can be made more compact using list comprehension, but it may make the code less readable.使用列表推导可以使这段代码更紧凑,但它可能会降低代码的可读性。

Don't pass information into the recursive calls, unless they need that information to compute their local result.不要将信息传递给递归调用,除非他们需要该信息来计算其本地结果。 It's much easier to reason about recursion when you write without side effects.当你在没有副作用的情况下编写时,推理递归要容易得多。 So instead of having the recursive call put its own results into a list, write the code so that the results from the recursive calls are used to create the return value.因此,不要让递归调用将其自己的结果放入列表中,而是编写代码以便使用递归调用的结果来创建返回值。

Let's take a trivial example, converting a simple loop to recursion, and using it to accumulate a sequence of increasing integers.让我们举一个简单的例子,将一个简单的循环转换为递归,并使用它来累积一个递增的整数序列。

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

We are using functions in the natural way: we put information in with the arguments, and get information out using the return value (rather than mutation of the parameters).我们以自然的方式使用函数:我们使用 arguments 输入信息,并使用返回值(而不是参数的突变)获取信息

In your case:在你的情况下:

Now the thing I'm having the most trouble visualising is how exactly I am going to arrive to the list of trees.现在,我最难以想象的事情是我将如何准确地到达树列表。

So you know that you want to return a list of trees at the end of the process.所以你知道你想在过程结束时返回一个树列表 So the natural way to proceed, is that you expect each recursive call to do that, too.因此,自然的方式是,您希望每个递归调用也这样做。

How can I deal with generating combinations and returning lists of configurations in these cases in recursion?在递归的这些情况下,我该如何处理生成组合和返回配置列表? While I gave an example, the more general the answer the better.虽然我举了一个例子,但答案越笼统越好。

The recursive calls return their lists of results for the sub-problems.递归调用返回子问题的结果列表。 You use those results to create the list of results for the current problem.您使用这些结果来创建当前问题的结果列表。

You don't need to think about how recursion is implemented in order to write recursive algorithms.为了编写递归算法,您无需考虑如何实现递归。 You don't need to think about the call stack.您无需考虑调用堆栈。 You do need to think about two things:确实需要考虑两件事:

  • What are the base cases?基本情况是什么?
  • How does the problem break down recursively?问题如何递归分解? (Alternately: why is recursion a good fit for this problem?) (或者:为什么递归非常适合这个问题?)

The thing is, recursion is not special .问题是,递归并不特殊 Making the recursive call is just like calling any other function that would happen to give you the correct answer for the sub-problem.进行递归调用就像调用任何其他 function碰巧会为您提供子问题的正确答案一样。 So all you need to do is understand how solving the sub-problems helps you to solve the current one.因此,您需要做的就是了解解决子问题如何帮助您解决当前问题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM