我是递归的新手,并试图理解这段代码。我正在读考试,这是我从斯坦福大学的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);
}
答案 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)
请记住,所有局部变量(例如numKeys
,sum
,left
,right
,root
都在堆栈内存中。当您转到递归函数的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)
我发现这种观点通常是一种很好的思维方式。