您如何清空装有五朵花的花瓶?
答:如果花瓶不是空的,您会拿出一朵花
然后倒空一个装有四朵花的花瓶。
您如何清空装有四朵花的花瓶?
答:如果花瓶不是空的,您会拿出一朵花
然后倒空一个装有三朵花的花瓶。
您如何清空装有三朵花的花瓶?
答:如果花瓶不是空的,您会拿出一朵花
然后倒空一个装有两朵花的花瓶。
您如何清空装有两朵花的花瓶?
答:如果花瓶不是空的,您会拿出一朵花
然后倒空装有一朵花的花瓶。
您如何清空装有一朵花的花瓶?
答:如果花瓶不是空的,您会拿出一朵花
然后清空一个没有花的花瓶。
如何清空没有花的花瓶?
答:如果花瓶不是空的,您会拿出一朵花
但是花瓶是空的,所以您完成了。
那是重复的。让我们概括一下:
您如何清空装有N朵鲜花的花瓶?
答:如果花瓶不是空的,您会拿出一朵花
然后清空一个装有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循环中做到了吗?
为什么可以,递归可以用迭代代替,但是递归通常更优雅。
让我们谈谈树木。在计算机科学中,树是由节点组成的结构,其中每个节点都有一定数量的子节点,这些子节点也是节点,或者为null。二叉树是由恰好具有两个子节点的节点组成的树,通常称为“左”和“右”;再次,子级可以是节点,也可以为null。根是不是任何其他节点的子节点的节点。
想象一个节点,除了其子节点外,还有一个值,一个数字,并想象我们希望对树中的所有值求和。
为了对任何一个节点中的值求和,我们会将节点本身的值添加到其左子节点(如果有)的值以及其右子节点(如果有)的值。现在回想一下,如果子级不为null,则它们也是节点。
因此,要对左子节点求和,我们会将子节点本身的值添加到其左子节点(如果有)的值以及其右子节点(如果有)的值。
因此,要对左子节点的左子节点的值求和,我们将子节点本身的值与其左子节点的值(如果有)和其右子节点的值(如果有)相加。
也许您已经预料到了我要做什么,并希望看到一些代码?好:
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 ) ;
}
}
请注意,我们没有明确测试子项是否为空或节点,而是使递归函数为空节点返回零。
假设我们有一棵像这样的树(数字是值,斜杠指向子级,@表示指针指向空):
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
这就是递归的力量。
上面的花瓶示例是尾递归的示例。尾递归的全部意思是,在递归函数中,如果我们递归(也就是说,如果再次调用该函数),那就是我们要做的最后一件事。
树的示例不是尾递归,因为即使我们做的最后一件事是递归右孩子,但在这样做之前,我们递归了左孩子。
实际上,我们调用子级并添加当前节点值的顺序根本没有关系,因为加法是可交换的。
现在,让我们看一下顺序重要的操作。我们将使用节点的二叉树,但是这次保留的值将是字符,而不是数字。
我们的树将具有特殊的属性,即对于任何节点,其树形都位于其左子级所保持的字符之后(按字母顺序)和其右子级所保持的字符之前(按字母顺序)。
我们要做的是按字母顺序打印树。考虑到树的特殊属性,这很容易做到。我们只打印左孩子,然后打印节点的字符,然后是右孩子。
我们不只是想打印willn-nilly,所以我们将传递一些要打印的函数。这将是一个带有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 );
除了重要的操作顺序外,此示例还说明了我们可以将事物传递给递归函数。我们唯一要做的就是确保在每次递归调用时,我们都继续传递它。我们向该函数传递了一个节点指针和一个打印机,并且在每个递归调用中,我们都将它们传递给“ down”。
现在,如果我们的树看起来像这样:
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”,的确按字母顺序排列。
仅通过了解如何按字母顺序打印单个节点,我们就可以按字母顺序打印整个树。只是(因为我们的树具有将值按字母顺序排列的后面的值的顺序排序的特殊属性)知道在打印节点的值之前先打印左子节点,然后在打印节点的值后再打印右子节点。
这就是递归的力量:通过只知道如何做一部分的整体(并且知道何时停止递归),能够完成全部工作。
回想一下,在大多数语言中,运算符|| (“或”)在其第一个操作数为true时发生短路,一般的递归函数为:
void recurse() { doWeStop() || recurse(); }
吕克M评论:
因此,应该为这种答案创建一个徽章。恭喜你!
谢谢,卢克!但是,实际上,因为我编辑了这个答案四次以上(添加最后一个示例,但主要是为了纠正错别字和修饰它-在小巧的上网本键盘上打字很困难),所以我无法获得更多的分数。这在某种程度上使我不愿为将来的答案投入太多精力。
请参阅我对此的评论:
https://stackoverflow.com/questions/128434/what-are-community-wiki-posts-in-stackoverflow/718699#718699
我是一名优秀的程序员,十分优秀!