你如何清空一个装有五朵花的花瓶?
答:如果花瓶不是空的,你就拿出一朵花
然后你清空一个装有四朵花的花瓶。
你如何清空一个装有四朵花的花瓶?
答:如果花瓶不是空的,你就拿出一朵花
然后你清空一个装有三朵花的花瓶。
你如何清空一个装有三朵花的花瓶?
答:如果花瓶不是空的,你就拿出一朵花
然后你清空一个装有两朵花的花瓶。
你如何清空一个装有两朵花的花瓶?
答:如果花瓶不是空的,你就拿出一朵花
然后你清空一个装有一朵花的花瓶。
你如何清空一个装有一朵花的花瓶?
答:如果花瓶不是空的,你就拿出一朵花
然后你清空一个没有花的花瓶。
你如何清空一个没有鲜花的花瓶?
答:如果花瓶不是空的,你就拿出一朵花
但是花瓶是空的,所以你完成了。
那是重复的。让我们概括一下:
你如何清空一个包含ñ花卉?
答:如果花瓶不是空的,你就拿出一朵花
然后你清空一个花瓶N-1花卉。
嗯,我们可以在代码中看到吗?
void emptyVase( int flowersInVase ) {
if( flowersInVase > 0 ) {
// take one flower and
emptyVase( flowersInVase - 1 ) ;
} else {
// the vase is empty, nothing to do
}
}
嗯,我们不能在 for 循环中完成吗?
为什么,是的,递归可以用迭代代替,但递归通常更优雅。
让我们谈谈树木。在计算机科学中,一个树是一个由以下组成的结构节点,其中每个节点都有一定数量的子节点,这些子节点也是节点,或者为空。一个二叉树是一棵由具有完全二儿童,通常称为“左”和“右”;再次,孩子可以是节点,也可以是空的。一个根是不是任何其他节点的子节点的节点。
想象一个节点,除了它的孩子之外,还有一个值,一个数字,并想象我们希望对某棵树中的所有值求和。
要对任何一个节点的值求和,我们会将节点本身的值添加到其左孩子的值(如果有)和其右孩子的值(如果有)。现在回想一下,孩子们,如果他们不为空,也是节点。
因此,要对左子节点求和,我们会将子节点本身的值与其左子节点的值(如果有)和右子节点的值(如果有)相加。
因此,要对左孩子的左孩子的值求和,我们会将子节点本身的值加上它的左孩子的值(如果有的话)和它的右孩子的值(如果有的话)。
或许你已经预料到我会用这个去哪里,并且想看一些代码?好的:
struct node {
node* left;
node* right;
int value;
} ;
int sumNode( node* root ) {
// if there is no tree, its sum is zero
if( root == null ) {
return 0 ;
} else { // there is a tree
return root->value + sumNode( root->left ) + sumNode( root->right ) ;
}
}
请注意,我们不是显式测试子节点以查看它们是 null 还是节点,而是让递归函数为 null 节点返回零。
假设我们有一棵看起来像这样的树(数字是值,斜杠指向子节点,@ 表示指针指向 null):
5
/ \
4 3
/\ /\
2 1 @ @
/\ /\
@@ @@
如果我们在根(值为 5 的节点)上调用 sumNode,我们将返回:
return root->value + sumNode( root->left ) + sumNode( root->right ) ;
return 5 + sumNode( node-with-value-4 ) + sumNode( node-with-value-3 ) ;
让我们就地展开它。在我们看到 sumNode 的任何地方,我们都将其替换为 return 语句的扩展:
sumNode( node-with-value-5);
return root->value + sumNode( root->left ) + sumNode( root->right ) ;
return 5 + sumNode( node-with-value-4 ) + sumNode( node-with-value-3 ) ;
return 5 + 4 + sumNode( node-with-value-2 ) + sumNode( node-with-value-1 )
+ sumNode( node-with-value-3 ) ;
return 5 + 4
+ 2 + sumNode(null ) + sumNode( null )
+ sumNode( node-with-value-1 )
+ sumNode( node-with-value-3 ) ;
return 5 + 4
+ 2 + 0 + 0
+ sumNode( node-with-value-1 )
+ sumNode( node-with-value-3 ) ;
return 5 + 4
+ 2 + 0 + 0
+ 1 + sumNode(null ) + sumNode( null )
+ sumNode( node-with-value-3 ) ;
return 5 + 4
+ 2 + 0 + 0
+ 1 + 0 + 0
+ sumNode( node-with-value-3 ) ;
return 5 + 4
+ 2 + 0 + 0
+ 1 + 0 + 0
+ 3 + sumNode(null ) + sumNode( null ) ;
return 5 + 4
+ 2 + 0 + 0
+ 1 + 0 + 0
+ 3 + 0 + 0 ;
return 5 + 4
+ 2 + 0 + 0
+ 1 + 0 + 0
+ 3 ;
return 5 + 4
+ 2 + 0 + 0
+ 1
+ 3 ;
return 5 + 4
+ 2
+ 1
+ 3 ;
return 5 + 4
+ 3
+ 3 ;
return 5 + 7
+ 3 ;
return 5 + 10 ;
return 15 ;
现在看看我们如何通过将其视为复合模板的重复应用来征服任意深度和“分支”的结构?每次通过我们的 sumNode 函数,我们只处理一个节点,使用一个 if/then 分支,以及两个几乎直接从我们的规范中编写出来的简单 return 语句?
How to sum a node:
If a node is null
its sum is zero
otherwise
its sum is its value
plus the sum of its left child node
plus the sum of its right child node
那是递归的力量。
上面的花瓶示例是尾递归.这一切尾递归意思是在递归函数中,如果我们递归(也就是说,如果我们再次调用该函数),那是我们做的最后一件事。
树的例子不是尾递归的,因为即使我们做的最后一件事是递归右孩子,在我们这样做之前我们递归了左孩子。
事实上,我们调用子节点的顺序以及添加当前节点的值根本不重要,因为加法是可交换的。
现在让我们看一下顺序确实很重要的操作。我们将使用节点的二叉树,但这次保存的值将是一个字符,而不是一个数字。
我们的树将有一个特殊的属性,即对于任何节点,它的字符都来自后(按字母顺序)左孩子持有的字符和前(按字母顺序)其右孩子持有的角色。
我们要做的是按字母顺序打印树。考虑到树的特殊属性,这很容易做到。我们只打印左孩子,然后是节点的字符,然后是右孩子。
我们不只是想随意打印,所以我们将传递给我们的函数一些东西来打印。这将是一个带有 print(char) 函数的对象;我们不需要担心它是如何工作的,只要调用 print 时,它会在某个地方打印一些东西。
让我们在代码中看看:
struct node {
node* left;
node* right;
char value;
} ;
// don't worry about this code
class Printer {
private ostream& out;
Printer( ostream& o ) :out(o) {}
void print( char c ) { out << c; }
}
// worry about this code
int printNode( node* root, Printer& printer ) {
// if there is no tree, do nothing
if( root == null ) {
return ;
} else { // there is a tree
printNode( root->left, printer );
printer.print( value );
printNode( root->right, printer );
}
Printer printer( std::cout ) ;
node* root = makeTree() ; // this function returns a tree, somehow
printNode( root, printer );
除了现在很重要的操作顺序之外,这个例子还说明我们可以将事物传递给递归函数。我们唯一要做的就是确保在每次递归调用中,我们继续传递它。我们将节点指针和打印机传递给函数,并且在每次递归调用时,我们将它们“向下”传递。
现在,如果我们的树看起来像这样:
k
/ \
h n
/\ /\
a j @ @
/\ /\
@@ i@
/\
@@
我们将打印什么?
From k, we go left to
h, where we go left to
a, where we go left to
null, where we do nothing and so
we return to a, where we print 'a' and then go right to
null, where we do nothing and so
we return to a and are done, so
we return to h, where we print 'h' and then go right to
j, where we go left to
i, where we go left to
null, where we do nothing and so
we return to i, where we print 'i' and then go right to
null, where we do nothing and so
we return to i and are done, so
we return to j, where we print 'j' and then go right to
null, where we do nothing and so
we return to j and are done, so
we return to h and are done, so
we return to k, where we print 'k' and then go right to
n where we go left to
null, where we do nothing and so
we return to n, where we print 'n' and then go right to
null, where we do nothing and so
we return to n and are done, so
we return to k and are done, so we return to the caller
所以如果我们只看我们打印的行:
we return to a, where we print 'a' and then go right to
we return to h, where we print 'h' and then go right to
we return to i, where we print 'i' and then go right to
we return to j, where we print 'j' and then go right to
we return to k, where we print 'k' and then go right to
we return to n, where we print 'n' and then go right to
我们看到我们打印了“ahijkn”,它确实是按字母顺序排列的。
我们只需知道如何按字母顺序打印单个节点,就可以按字母顺序打印整个树。这只是(因为我们的树具有将值按字母顺序排列在后面的值的左侧的特殊属性)知道在打印节点的值之前打印左孩子,并在打印节点的值之后打印右孩子。
和那是递归的力量:能够通过只知道如何做整体的一部分(以及知道何时停止递归)来完成整个事情。
回想一下,在大多数语言中,运算符 || ("or") 在其第一个操作数为真时短路,一般递归函数为:
void recurse() { doWeStop() || recurse(); }
Luc M cmets:
所以应该为这种答案创建一个徽章。恭喜!
谢谢,卢克!但是,实际上,因为我对这个答案进行了四次以上的编辑(添加最后一个示例,但主要是为了更正拼写错误并润色它——在一个小的上网本键盘上打字很难),我不能再得到任何分数了.这在某种程度上阻止了我在未来的答案中投入尽可能多的精力。
在这里查看我的评论:https://stackoverflow.com/questions/128434/what-are-community-wiki-posts-in-stackoverflow/718699#718699