递归如何在For循环中工作

时间:2011-01-25 15:44:40

标签: c++ c recursion for-loop

我是递归的新手,并试图理解这段代码。我正在读考试,这是我从斯坦福大学的CIS教育图书馆(来自尼克帕兰特的二元树)找到的“评论家”。

我理解这个概念,但是当我们在内圈中递归时,一切都在吹!请帮我。谢谢。

countTrees()解决方案(C / C ++)

/*
 For the key values 1...numKeys, how many structurally unique
 binary search trees are possible that store those keys.
 Strategy: consider that each value could be the root.
 Recursively find the size of the left and right subtrees.
*/

int countTrees(int numKeys) {
    if (numKeys <=1) {
        return(1);
    }

    // there will be one value at the root, with whatever remains
    // on the left and right each forming their own subtrees.
    // Iterate through all the values that could be the root...

    int sum = 0;
    int left, right, root;

    for (root=1; root<=numKeys; root++) {
        left = countTrees(root - 1);
        right = countTrees(numKeys - root);
        // number of possible trees with this root == left*right
        sum += left*right;
    }

    return(sum);  
}  

7 个答案:

答案 0 :(得分:16)

想象一下,当你进入函数调用时,循环被“暂停”。

仅仅因为函数恰好是递归调用,它的工作方式与您在循环中调用的任何函数相同。

新的递归调用会再次启动for循环,并在再次调用函数时暂停,依此类推。

答案 1 :(得分:1)

以这种方式看待:初次调用有3种可能的情况:

numKeys = 0
numKeys = 1
numKeys > 1

0和1的情况很简单 - 函数只返回1并且你已经完成了。对于numkeys 2,您最终得到:

sum = 0
loop(root = 1 -> 2)
   root = 1:
      left = countTrees(1 - 1) -> countTrees(0) -> 1
      right = countTrees(2 - 1) -> countTrees(1) -> 1
      sum = sum + 1*1 = 0 + 1 = 1
   root = 2:
      left = countTrees(2 - 1) -> countTrees(1) -> 1
      right = countTrees(2 - 2) -> countTrees(0) -> 1
      sum = sum + 1*1 = 1 + 1 = 2

output: 2

表示numKeys = 3:

sum = 0
loop(root = 1 -> 3):
   root = 1:
       left = countTrees(1 - 1) -> countTrees(0) -> 1
       right = countTrees(3 - 1) -> countTrees(2) -> 2
       sum = sum + 1*2 = 0 + 2 = 2
   root = 2:
       left = countTrees(2 - 1) -> countTrees(1) -> 1
       right = countTrees(3 - 2) -> countTrees(1) -> 1
       sum = sum + 1*1 = 2 + 1 = 3
   root = 3:
       left = countTrees(3 - 1) -> countTrees(2) -> 2
       right = countTrees(3 - 3) -> countTrees(0) -> 1
       sum = sum + 2*1 = 3 + 2 = 5

 output 5

等等。这个函数很可能是O(n ^ 2),因为对于每n个键,你运行2 * n-1个递归调用,这意味着它的运行时间会很快增长。

答案 2 :(得分:1)

请记住,所有局部变量(例如numKeyssumleftrightroot都在堆栈内存中。当您转到递归函数的n-th深度时,将会有n个这些局部变量的副本。当它完成执行一个深度时,将从堆栈中弹出这些变量的一个副本。

通过这种方式,您将理解,下一级深度不会影响当前级别的深度局部变量(除非您使用引用,但我们不在这个特定问题中)。

对于这个特殊问题,应该注意时间复杂性。以下是我的解决方案:

/* Q: For the key values 1...n, how many structurally unique binary search
      trees (BST) are possible that store those keys.
      Strategy: consider that each value could be the root.  Recursively
      find the size of the left and right subtrees.
      http://stackoverflow.com/questions/4795527/
             how-recursion-works-inside-a-for-loop */
/* A: It seems that it's the Catalan numbers:
      http://en.wikipedia.org/wiki/Catalan_number */
#include <iostream>
#include <vector>
using namespace std;

// Time Complexity: ~O(2^n)
int CountBST(int n)
{
    if (n <= 1)
        return 1;

    int c = 0;
    for (int i = 0; i < n; ++i)
    {
        int lc = CountBST(i);
        int rc = CountBST(n-1-i);
        c += lc*rc;
    }

    return c;
}

// Time Complexity: O(n^2)
int CountBST_DP(int n)
{
    vector<int> v(n+1, 0);
    v[0] = 1;

    for (int k = 1; k <= n; ++k)
    {
        for (int i = 0; i < k; ++i)
            v[k] += v[i]*v[k-1-i];
    }

    return v[n];
}

/* Catalan numbers:
            C(n, 2n)
     f(n) = --------
             (n+1)
              2*(2n+1)
     f(n+1) = -------- * f(n)
               (n+2)

   Time Complexity: O(n)
   Space Complexity: O(n) - but can be easily reduced to O(1). */
int CountBST_Math(int n)
{
    vector<int> v(n+1, 0);
    v[0] = 1;

    for (int k = 0; k < n; ++k)
        v[k+1] = v[k]*2*(2*k+1)/(k+2);

    return v[n];
}

int main()
{
    for (int n = 1; n <= 10; ++n)
        cout << CountBST(n) << '\t' << CountBST_DP(n) <<
                               '\t' << CountBST_Math(n) << endl;

    return 0;
}
/* Output:
1       1       1
2       2       2
5       5       5
14      14      14
42      42      42
132     132     132
429     429     429
1430    1430    1430
4862    4862    4862
16796   16796   16796
 */

答案 3 :(得分:0)

你可以从基础案例中思考,向上工作。

因此,对于基本情况,您有1个(或更少)节点。 1个节点只有1个结构上唯一的树 - 即节点本身。因此,如果numKeys小于或等于1,则只返回1.

现在假设你有超过1个键。那么,其中一个键是根,一些项目在左侧分支,一些项目在右侧分支。

那些左右分支有多大?那么它取决于什么是根元素。由于您需要考虑可能的树的总量,我们必须考虑所有配置(所有可能的根值) - 因此我们迭代所有可能的值。

对于每次迭代i,我们知道我在根处,i-1个节点在左侧分支上,而numKeys-i节点在右侧分支上。但是,当然,我们已经有一个函数可以计算给定节点数量的树配置总数!这是我们正在写的功能。因此,递归调用函数以获取左右子树的可能树配置的数量。然后,根在i处可能的树的总数是这两个数的乘积(对于左子树的每个配置,可能发生所有可能的右子树。)

总结之后,你就完成了。

所以,如果你有点说明,从循环中递归调用函数没什么特别的 - 它只是我们算法所需的工具。我还建议(正如Grammin所做的那样)通过调试器运行它,看看每一步发生了什么。

答案 4 :(得分:0)

每个调用都有自己的可变空间,正如人们所期望的那样。复杂性来自于函数的执行被“中断”以便执行 - 相同的功能。 这段代码:

for (root=1; root<=numKeys; root++) {
        left = countTrees(root - 1);
        right = countTrees(numKeys - root);
        // number of possible trees with this root == left*right
        sum += left*right;
    }

可以在Plain C中以这种方式重写:

 root = 1;
 Loop:
     if ( !( root <= numkeys ) ) {
         goto EndLoop;
     }

     left = countTrees( root -1 );
     right = countTrees ( numkeys - root );
     sum += left * right

     ++root;
     goto Loop;
 EndLoop:
 // more things...

它实际上是由编译器翻译成类似的东西,但在汇编程序中。正如您所看到的,循环由一对变量 numkeys root 控制,并且由于执行了另一个实例而未修改它们的值相同的程序。当被调用者返回时,调用者恢复执行,并使其在递归调用之前具有相同的值。

答案 5 :(得分:0)

IMO,这里的关键要素是了解函数调用框架,调用堆栈以及它们如何协同工作。

在您的示例中,您有一堆局部变量,它们在第一个调用中已初始化但未最终确定。观察这些局部变量以理解整个想法很重要。在每次调用时,局部变量都会被更新并最终以向后的方式返回(最有可能在每个函数调用帧从堆栈弹出之前,它都存储在寄存器中),直到将其添加到初始函数调用的sum变量中为止。

这里的重要区别是-返回何处。如果您需要像示例中那样的累加总和值,则不能返回 inside (内部)函数,该函数会导致提前返回/退出。但是,如果您依赖某个值处于某个状态,则可以检查该状态是否在for循环内被击中并立即返回而不会一直上升。

答案 6 :(得分:0)

  • 对于递归而言,在您的脑海中描绘调用堆栈结构很有帮助。
  • 如果递归位于循环内,则该结构类似于(几乎)N元树。
  • 在递归确定树的高度时,循环水平控制生成的分支数。
  • 沿着一个特定的分支生成树,直到到达叶子(基本条件),然后水平扩展以获取其他叶子并返回先前的高度并重复。

我发现这种观点通常是一种很好的思维方式。