我在学校里很难理解递归。每当教授谈到它的时候,我似乎都懂了,但当我自己尝试的时候,它完全震撼了我的大脑。

我整晚都在尝试着解决《Towers of Hanoi》这款游戏,结果完全出乎我的意料。我的课本只有大约30页关于递归的内容,所以它不是很有用。有人知道哪些书籍或资源可以帮助澄清这个话题吗?


你在用哪本书?

关于算法的标准教科书是Cormen & Rivest。我的经验是,它很好地教授了递归。

递归是编程中较难掌握的部分之一,虽然它确实需要本能,但它是可以学习的。但它确实需要一个好的描述,好的例子和好的插图。

此外,30页通常是很多的,30页是用一种编程语言编写的。在你从一本普通的书中理解递归之前,不要尝试用C或Java学习递归。


这与其说是一个问题,不如说是一个抱怨。关于递归你有更具体的问题吗?就像乘法一样,人们不会写很多关于它的东西。

说到乘法,想想这个。

问题:

* b是什么?

答:

如果b = 1,就是a。 否则就是a+a*(b-1)

* (b - 1)是什么?请参考上面的问题来解决这个问题。


递归

方法A调用方法A调用方法A,最终这些方法A中的一个不会调用并退出,但这是递归,因为有东西调用了它自己。

递归的例子,我想打印出硬盘驱动器上的每个文件夹名称:(在c#中)

public void PrintFolderNames(DirectoryInfo directory)
{
    Console.WriteLine(directory.Name);

    DirectoryInfo[] children = directory.GetDirectories();

    foreach(var child in children)
    {
        PrintFolderNames(child); // See we call ourself here...
    }
}

当使用递归解时,我总是尝试:

首先建立基本情况,即。 当n = 1的阶乘 试着想出一个一般的规则 对于其他情况

还有不同类型的递归解,分而治之的方法对分形和其他方法很有用。

如果你能先解决一些简单的问题,只是为了掌握窍门,这也会有帮助。一些例子是求解阶乘和生成第n个斐波那契数。

作为参考,我强烈推荐Robert Sedgewick的算法。

希望这能有所帮助。祝你好运。


如果你想要一本很好地用简单的术语解释递归的书,可以看看Gödel,埃舍尔·巴赫:道格拉斯·霍夫施塔特的《永恒的金辫子》,特别是第五章。除了递归,它还能很好地以一种可理解的方式解释计算机科学和数学中的许多复杂概念,一个解释建立在另一个解释的基础上。如果你以前没有接触过这类概念,这可能是一本非常令人兴奋的书。


我会用一个例子来解释。

你知道n!意味着什么?如果不是:http://en.wikipedia.org/wiki/Factorial

3! Is 1 times 2 times 3, which is 6

下面是一些伪代码

function factorial(n) {
  if (n==0) return 1
  else return (n * factorial(n-1))
}

让我们试试吧:

factorial(3)

n是0吗?

no!

所以我们在递归中深入挖掘:

3 * factorial(3-1)

3 minus 1 is 2

2 == 0?

no!

所以我们要深入! 3 * 2 *阶乘(2-1) 2-1 = 1

1 == 0吗?

no!

所以我们要深入! 3 * 2 * 1 *阶乘(1-1) 1-1 = 0

0 == 0?

yes!

我们有一个小问题

所以我们有 3 * 2 * 1 * 1 = 6

我希望这对你有所帮助


递归函数只是一个函数,它可以根据需要多次调用自己。如果您需要多次处理某件事,但不确定实际需要多少次,那么它就很有用。在某种程度上,你可以把递归函数看作是一种循环。然而,就像循环一样,您需要指定中断流程的条件,否则它将变得无限。


http://javabat.com是一个有趣而令人兴奋的练习递归的地方。他们的例子开始时相当简单,然后逐步扩展(如果你想这么做的话)。注意:他们的方法是在实践中学习。这是我写的一个递归函数,用来替换for循环。

for循环:

public printBar(length)
{
  String holder = "";
  for (int index = 0; i < length; i++)
  {
    holder += "*"
  }
  return holder;
}

这是做同样事情的递归。(请注意,我们重载了第一个方法,以确保它像上面那样使用)。我们还有另一种方法来维护索引(类似于上面的for语句)。递归函数必须维护自己的索引。

public String printBar(int Length) // Method, to call the recursive function
{
  printBar(length, 0);
}

public String printBar(int length, int index) //Overloaded recursive method
{
  // To get a better idea of how this works without a for loop
  // you can also replace this if/else with the for loop and
  // operationally, it should do the same thing.
  if (index >= length)
    return "";
  else
    return "*" + printBar(length, index + 1); // Make recursive call
}

简而言之,递归是一种编写更少代码的好方法。在后面的printBar中,请注意我们有一个if语句。如果我们的条件已经达到,我们将退出递归并返回到前一个方法,该方法返回到前一个方法,等等。如果我发送一个printBar(8),我得到********。我希望通过一个简单函数的例子,它做的事情与for循环相同,这可能会有所帮助。不过,您可以在Java Bat中进行更多的练习。


递归函数就像弹簧,每次调用都要压缩一点。在每一步中,您将一些信息(当前上下文)放在堆栈上。当到达最后一步时,释放弹簧,立即收集所有值(上下文)!

不确定这个比喻是否有效…: -)

无论如何,除了经典的例子(阶乘是最糟糕的例子,因为它效率低,很容易被平化,Fibonacci, Hanoi…),这些都有点人为(我很少,如果有的话,在实际编程案例中使用它们),看看它真正被使用的地方是有趣的。

A very common case is to walk a tree (or a graph, but trees are more common, in general). For example, a folder hierarchy: to list the files, you iterate on them. If you find a sub-directory, the function listing the files call itself with the new folder as argument. When coming back from listing this new folder (and its sub-folders!), it resumes its context, to the next file (or folder). Another concrete case is when drawing a hierarchy of GUI components: it is common to have containers, like panes, to hold components which can be panes too, or compound components, etc. The painting routine calls recursively the paint function of each component, which calls the paint function of all the components it holds, etc.

不确定我是否很清楚,但我喜欢展示现实世界中教材的使用,因为这是我过去偶然发现的东西。


我认为这个非常简单的方法可以帮助你理解递归。该方法将调用自身,直到某个条件为真,然后返回:

function writeNumbers( aNumber ){
 write(aNumber);
 if( aNumber > 0 ){
  writeNumbers( aNumber - 1 );
 }
 else{
  return;
 }
}

这个函数将输出从你输入的第一个数字到0的所有数字。因此:

writeNumbers( 10 );
//This wil write: 10 9 8 7 6 5 4 3 2 1 0
//and then stop because aNumber is no longer larger then 0

基本上发生的情况是writeNumbers(10)将写入10,然后调用writeNumbers(9),后者将写入9,然后调用writeNumber(8)等。直到writeNumbers(1)写入1,然后调用writeNumbers(0),这将写入0 butt将不会调用writeNumbers(-1);

这段代码本质上与以下代码相同:

for(i=10; i>0; i--){
 write(i);
}

你可能会问为什么要用递归,如果for循环本质上是一样的。当你需要嵌套for循环但不知道它们嵌套的深度时,你通常会使用递归。例如,当从嵌套数组中打印项目时:

var nestedArray = Array('Im a string', 
                        Array('Im a string nested in an array', 'me too!'),
                        'Im a string again',
                        Array('More nesting!',
                              Array('nested even more!')
                              ),
                        'Im the last string');
function printArrayItems( stringOrArray ){
 if(typeof stringOrArray === 'Array'){
   for(i=0; i<stringOrArray.length; i++){ 
     printArrayItems( stringOrArray[i] );
   }
 }
 else{
   write( stringOrArray );
 }
}

printArrayItems( stringOrArray );
//this will write:
//'Im a string' 'Im a string nested in an array' 'me too' 'Im a string again'
//'More nesting' 'Nested even more' 'Im the last string'

这个函数可以接受一个可以嵌套到100层的数组,而你写一个for循环就需要你嵌套它100次:

for(i=0; i<nestedArray.length; i++){
 if(typeof nestedArray[i] == 'Array'){
  for(a=0; i<nestedArray[i].length; a++){
   if(typeof nestedArray[i][a] == 'Array'){
    for(b=0; b<nestedArray[i][a].length; b++){
     //This would be enough for the nestedAaray we have now, but you would have
     //to nest the for loops even more if you would nest the array another level
     write( nestedArray[i][a][b] );
    }//end for b
   }//endif typeod nestedArray[i][a] == 'Array'
   else{ write( nestedArray[i][a] ); }
  }//end for a
 }//endif typeod nestedArray[i] == 'Array'
 else{ write( nestedArray[i] ); }
}//end for i

如你所见,递归方法要好得多。


实际上,使用递归是为了降低手头问题的复杂性。你应用递归,直到你达到一个简单的基本情况,可以很容易地解决。这样就可以解决最后一个递归步骤。用这些递归步骤就可以解决最初的问题。


你的大脑爆炸是因为它进入了无限递归。这是初学者常犯的错误。

信不信由你,你已经理解了递归,你只是被一个常见的,但错误的函数比喻拖了下来:一个小盒子,里面有东西进进出出。

而不是考虑一个任务或过程,比如“在网上找到更多关于递归的知识”。这是递归的,没有问题。要完成这个任务,你可以:

a) Read a Google's result page for "recursion"
b) Once you've read it, follow the first link on it and...
a.1)Read that new page about recursion 
b.1)Once you've read it, follow the first link on it and...
a.2)Read that new page about recursion 
b.2)Once you've read it, follow the first link on it and...

如您所见,您已经做了很长一段时间的递归工作,没有出现任何问题。

你会坚持做这个任务多久?永远,直到你的大脑爆炸?当然不是,只要你相信你已经完成了任务,你就会停在一个给定的点上。

当要求你“在网上找到更多关于递归的知识”时,没有必要指定这一点,因为你是一个人,你可以自己推断。

计算机无法推断任何东西,所以你必须包含一个明确的结尾:“在网上找到更多关于递归的知识,直到你理解它或你阅读了最多10页”。

您还推断应该从谷歌的结果页面开始进行“递归”,这也是计算机无法做到的。递归任务的完整描述还必须包括一个显式的起点:

“在网上找到更多关于递归的知识,直到你理解它,或者你已经阅读了最多10页,并从www.google.com/search?q=recursion开始”

要想全面了解,我建议你试试下面这些书:

普通Lisp:符号计算的简单介绍。这是对递归最可爱的非数学解释。 小阴谋家。


要理解递归,你只需要看看洗发水瓶上的标签:

function repeat()
{
   rinse();
   lather();
   repeat();
}

这样做的问题是没有终止条件,递归将无限重复,或者直到洗发水或热水用完为止(外部终止条件,类似于吹你的堆栈)。


你如何倒空一个装有五朵花的花瓶?

答:如果花瓶不是空的,你就拿出一朵花 然后你倒空了一个装有四朵花的花瓶。

你如何倒空一个装有四朵花的花瓶?

答:如果花瓶不是空的,你就拿出一朵花 然后你倒空了一个装有三朵花的花瓶。

你如何倒空一个装有三朵花的花瓶?

答:如果花瓶不是空的,你就拿出一朵花 然后你把装着两朵花的花瓶倒空。

你如何倒空一个装有两朵花的花瓶?

答:如果花瓶不是空的,你就拿出一朵花 然后你把只装了一朵花的花瓶倒空。

你如何清空只装了一朵花的花瓶?

答:如果花瓶不是空的,你就拿出一朵花 然后你倒空了一个没有花的花瓶。

你怎么倒空一个没有花的花瓶?

答:如果花瓶不是空的,你就拿出一朵花 但是花瓶是空的,所以你完成了。

这是重复的。让我们概括一下:

你如何倒空一个装有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。二叉树是由恰好有两个子结点组成的树,通常称为“左”和“右”;同样,子节点可以是节点,也可以为空。根节点不是任何其他节点的子节点。

假设一个节点,除了它的子节点外,还有一个值,一个数字,然后假设我们想要在某个树中对所有的值求和。

为了对任意一个节点的值求和,我们将节点本身的值与它左子节点的值(如果有的话)和右子节点的值(如果有的话)相加。现在回想一下,如果子节点不为空,它们也是节点。

因此,要对左子节点求和,我们需要将子节点自身的值与左子节点的值(如果有的话)以及右子节点的值(如果有的话)相加。

因此,要对左子节点的左子节点的值求和,我们需要将子节点自身的值与它的左子节点的值(如果有的话)以及它的右子节点的值(如果有的话)相加。

也许您已经预料到我要做什么,并希望看到一些代码?好:

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):

     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分支,和两个简单的返回语句,几乎自己写,直接从我们的规范?

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评论道:

SO应该为这种回答创建一个徽章。恭喜你!

谢谢,吕克·!但实际上,因为我编辑了不止四次这个答案(为了添加最后一个例子,但主要是为了纠正错字和润色——在一个小小的上网本键盘上打字很困难),我无法从这个答案中获得更多的分数。这在一定程度上阻碍了我在未来的答案上花那么多精力。

请看我的评论:https://stackoverflow.com/questions/128434/what-are-community-wiki-posts-in-stackoverflow/718699#718699


Common Lisp中的简单递归示例:

MYMAP对列表中的每个元素应用一个函数。

1)空列表没有元素,所以我们返回空列表-()和NIL都是空列表。

2)将函数应用到第一个列表,对列表的其余部分调用MYMAP(递归调用),并将两个结果合并到一个新列表中。

(DEFUN MYMAP (FUNCTION LIST)
  (IF (NULL LIST)
      ()
      (CONS (FUNCALL FUNCTION (FIRST LIST))
            (MYMAP FUNCTION (REST LIST)))))

让我们观察跟踪执行。在输入函数时,输出参数。在退出函数时,输出结果。对于每个递归调用,输出将按级别缩进。

这个例子对列表(1 2 3 4)中的每个数字调用SIN函数。

Command: (mymap 'sin '(1 2 3 4))

1 Enter MYMAP SIN (1 2 3 4)
| 2 Enter MYMAP SIN (2 3 4)
|   3 Enter MYMAP SIN (3 4)
|   | 4 Enter MYMAP SIN (4)
|   |   5 Enter MYMAP SIN NIL
|   |   5 Exit MYMAP NIL
|   | 4 Exit MYMAP (-0.75680256)
|   3 Exit MYMAP (0.14112002 -0.75680256)
| 2 Exit MYMAP (0.9092975 0.14112002 -0.75680256)
1 Exit MYMAP (0.841471 0.9092975 0.14112002 -0.75680256)

这是我们的结果:

(0.841471 0.9092975 0.14112002 -0.75680256)

Ouch. I tried to figure out the Towers of Hanoi last year. The tricky thing about TOH is it's not a simple example of recursion - you have nested recursions which also change the roles of towers on each call. The only way I could get it to make sense was to literally visualize the movement of the rings in my mind's eye, and verbalize what the recursive call would be. I would start with a single ring, then two, then three. I actually ordered the game on the internet. It took me maybe two or three days of cracking my brains to get it.


构建递归函数的真正数学方法如下:

1:假设你有一个函数对f(n-1)是正确的,构造f使f(n)是正确的。 2:构造f,使得f(1)是正确的。

This is how you can prove that the function is correct, mathematically, and it's called Induction. It is equivalent to have different base cases, or more complicated functions on multiple variables). It is also equivalent to imagine that f(x) is correct for all x Now for a "simple" example. Build a function that can determine if it is possible to have a coin combination of 5 cents and 7 cents to make x cents. For example, it's possible to have 17 cents by 2x5 + 1x7, but impossible to have 16 cents. Now imagine you have a function that tells you if it's possible to create x cents, as long as x < n. Call this function can_create_coins_small. It should be fairly simple to imagine how to make the function for n. Now build your function: bool can_create_coins(int n) { if (n >= 7 && can_create_coins_small(n-7)) return true; else if (n >= 5 && can_create_coins_small(n-5)) return true; else return false; } The trick here is to realize that the fact that can_create_coins works for n, means that you can substitute can_create_coins for can_create_coins_small, giving: bool can_create_coins(int n) { if (n >= 7 && can_create_coins(n-7)) return true; else if (n >= 5 && can_create_coins(n-5)) return true; else return false; } One last thing to do is to have a base case to stop infinite recursion. Note that if you are trying to create 0 cents, then that is possible by having no coins. Adding this condition gives: bool can_create_coins(int n) { if (n == 0) return true; else if (n >= 7 && can_create_coins(n-7)) return true; else if (n >= 5 && can_create_coins(n-5)) return true; else return false; } It can be proven that this function will always return, using a method called infinite descent, but that isn't necessary here. You can imagine that f(n) only calls lower values of n, and will always eventually reach 0. To use this information to solve your Tower of Hanoi problem, I think the trick is to assume you have a function to move n-1 tablets from a to b (for any a/b), trying to move n tables from a to b.


想想工蜂。它试着酿蜂蜜。它完成了自己的工作,并期待其他工蜂来酿造剩下的蜂蜜。蜂房满了,蜂房就停了。

把它想象成魔法。你有一个与你要实现的函数同名的函数,当你给它子问题时,它就会帮你解决它,你唯一需要做的就是把你的部分的解与它给你的解集成起来。

例如,我们想计算一个列表的长度。让我们用magical_length来调用我们的函数,用magical_length来调用神奇的助手 我们知道,如果我们给出没有第一个元素的子列表,它会神奇地给我们子列表的长度。那么我们唯一需要考虑的就是如何将这些信息与我们的工作结合起来。第一个元素的长度是1,而magic_counter给出了子列表的长度n-1,因此总长度是(n-1) + 1 -> n

int magical_length( list )
  sublist = rest_of_the_list( list )
  sublist_length = magical_length( sublist ) // you can think this function as magical and given to you
  return 1 + sublist_length

然而,这个答案是不完整的,因为我们没有考虑如果我们给出一个空列表会发生什么。我们认为我们的列表总是至少有一个元素。因此,我们需要思考,如果给我们一个空列表,答案显然是0,那么答案应该是什么。所以把这些信息加到我们的函数中,这被称为基础/边缘条件。

int magical_length( list )
  if ( list is empty) then
    return 0
  else
    sublist_length = magical_length( sublist ) // you can think this function as magical and given to you
    return 1 + sublist_length

要向一个六岁的孩子解释递归,首先要向一个五岁的孩子解释,然后再等一年。

实际上,这是一个有用的反例,因为递归调用应该更简单,而不是更难。向一个5岁的孩子解释递归就更难了,尽管你可以在0点停止递归,但你没有简单的解决方案来向一个0岁的孩子解释递归。

要使用递归解决一个问题,首先将其细分为一个或多个可以用相同方法解决的更简单的问题,然后当问题简单到无需进一步递归就可以解决时,您可以返回到更高的级别。

事实上,这是用递归方法来解决问题的递归定义。


子函数隐式地使用递归,例如:

去迪士尼乐园自驾游

我们到了吗?(没有) 我们到了吗?(很快) 我们到了吗?(快了……) 我们到了吗? 我们到了吗?(!!!!!)

这时孩子就睡着了……

这个倒数函数是一个简单的例子:

倒计时()函数 { 返回(参数[0]> 0 ? ( Console.log(参数[0]),倒计时(参数[0]- 1)): “完成” ); } 倒计时(10);

霍夫施塔特定律也适用于软件项目。

The essence of human language is, according to Chomsky, the ability of finite brains to produce what he considers to be infinite grammars. By this he means not only that there is no upper limit on what we can say, but that there is no upper limit on the number of sentences our language has, there's no upper limit on the size of any particular sentence. Chomsky has claimed that the fundamental tool that underlies all of this creativity of human language is recursion: the ability for one phrase to reoccur inside another phrase of the same type. If I say "John's brother's house", I have a noun, "house", which occurs in a noun phrase, "brother's house", and that noun phrase occurs in another noun phrase, "John's brother's house". This makes a lot of sense, and it's an interesting property of human language.

参考文献

递归与人类思想