了解递归[关闭]


225

我在学校了解递归时遇到了很大的麻烦。每当教授在谈论它时,我似乎都明白了,但是只要我自己尝试一下,它就会完全让我震惊。

我整夜试图解决河内塔楼,这完全让我震惊。我的教科书只有大约30页的递归,所以它不太有用。有谁知道可以帮助阐明这一主题的书籍或资源?


200
为了了解递归,您必须首先了解递归。
Paul Tomblin,2009年

40
递归:请参阅递归
Loren Pechtel,2009年

36
@Paul:我开玩笑,但是我一直认为这在技术上是错误的。导致算法结束的基本条件在哪里?这是递归的基本条件。=)
塞尔吉奥·阿科斯塔

70
我会试一下:“要了解递归,您需要了解递归,直到您了解它为止。” =)
塞尔吉奥·阿科斯塔

Answers:


597

您如何清空装有五朵花的花瓶?

答案:如果花瓶不是空的,则取出一朵花,然后倒空包含四朵花的花瓶。

您如何清空装有四朵花的花瓶?

答:如果花瓶不是空的,则取出一朵花,然后倒空包含三朵花的花瓶。

您如何清空装有三朵花的花瓶?

答:如果花瓶不是空的,则取出一朵花,然后倒空包含两朵花的花瓶。

您如何清空装有两朵花的花瓶?

答:如果花瓶不是空的,则取出一朵花,然后倒空装有一朵花的花瓶。

您如何清空装有一朵花的花瓶?

答:如果花瓶不是空的,则取出一朵花,然后倒空一个没有花的花瓶。

如何清空没有花的花瓶?

答:如果花瓶不是空的,您会拿出一朵花,但是花瓶是空的,这样就完成了。

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

您如何清空装有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。一个二叉树是由它们拥有完全节点树2个子女,通常被称为“左”和“右”; 再次,子级可以是节点,也可以为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 ) ;
  }
}

请注意,我们没有明确测试子节点是否为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

这就是递归的力量。


上面的花瓶示例是尾递归的示例。尾递归的全部意思是,在递归函数中,如果我们递归(也就是说,如果再次调用该函数),那就是我们要做的最后一件事。

树的例子不是尾递归的,因为即使我们做的最后一件事是递归右孩子,但在这样做之前,我们递归了左孩子。

实际上,我们调用子级并添加当前节点值的顺序根本没有关系,因为加法是可交换的。

现在,让我们看一下顺序重要的操作。我们将使用节点的二叉树,但是这次保留的值将是字符,而不是数字。

我们的树将具有一个特殊的属性,即对于任何节点,它的字符都(按字母顺序)位于其左子节点所保持的字符之后,并且在其(字符按字母顺序)于其右子节点所保持的字符之后

我们要做的是按字母顺序打印树。考虑到树的特殊属性,这很容易做到。我们只打印左孩子,然后打印节点的字符,然后是右孩子。

我们不只是想打印Willy-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


35

您的大脑炸毁了,因为它无限循环。这是一个常见的初学者错误。

信不信由你,您已经了解了递归,只是被一个常见但有错误的功能隐喻所拖累:一个装满东西的小盒子。

而不是思考任务或过程,例如“更多地了解网络递归”。这是递归的,您没有问题。要完成此任务,您可以:

a)阅读Google的“递归”结果页
b)阅读后,请点击它的第一个链接,然后...
a.1)阅读有关递归的新页面 
b.1)阅读后,请点击它的第一个链接,然后...
a.2)阅读有关递归的新页面 
b.2)阅读后,请点击它的第一个链接,然后...

如您所见,您从事递归操作已经很长时间了,没有任何问题。

您将继续执行该任务多长时间?永远直到你的大脑炸毁?当然不是,只要您相信自己已完成任务,就会停在给定的位置。

当您要求“查找有关网络递归的更多信息”时,无需指定此项,因为您是人,可以自己推断。

计算机无法推断出插孔,因此您必须包括一个明确的结尾:“在网络上查找有关递归的更多信息,除非您了解它,否则您最多阅读10页 ”。

您还推断出应该从Google的“递归”结果页开始,这也是计算机无法做到的。对递归任务的完整描述还必须包括一个明确的起点:

“在网络上找到有关递归的更多信息,直到您了解它为止,或者您最多阅读了10页从www.google.com/search?q=recursion开始

要研究整个问题,建议您尝试以下任何书籍:

  • Common Lisp:符号计算的温和介绍。这是递归的最简单的非数学解释。
  • 小计划者。

6
“功能= I / O小盒子”的隐喻可以递归使用,只要您还想像那里有一个工厂在制造无限克隆,而您的小盒子可以吞噬其他小盒子。
ephemient

2
有趣的是,因此,将来机器人将使用前10个链接在Google上搜索并学习。:) :)
kumar 2014年

2
@kumar谷歌不是已经通过互联网这样做了吗?
TJ 2015年

1
很棒的书,谢谢你的推荐
Max Koretskyi

+1表示“您的大脑炸毁了,因为它陷入了无限递归。这是一个常见的初学者错误。”
堆栈下溢

26

要了解递归,您所要做的就是查看洗发水瓶的标签:

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

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


6
谢谢dar7yl-总是让洗发水瓶子惹恼了我。(我想我总是注定要编程)。虽然我打赌谁决定添加“重复”在指令结尾的家伙使公司数百万美元。
kenj0418

5
希望您rinse()之后lather()
CoderDennis 2013年

@JakeWilson(如果使用了尾部调用优化)-可以。但是,按照目前的状态,这是完全有效的递归。

1
@ dar7yl,所以这就是为什么我的洗发水瓶总是空的……
布兰登·灵

11

如果您想要一本能很好地用简单的术语解释递归的书,请阅读道格拉斯·霍夫斯塔特(Douglas Hofstadter)的《哥德尔,埃舍尔,巴赫:永恒的金辫子》,特别是第5章。计算机科学和数学中的许多复杂概念以一种可以理解的方式进行,其中一种解释基于另一种解释。如果您以前没有对这些概念有太多了解,那么它可能是一本令人难以忘怀的书。


然后漫步在霍夫施塔特的其余书籍中。目前,我最喜欢的是诗歌翻译一本:Le Ton Beau do Marot。不完全是CS主题,但是它提出了有关翻译的真正含义和意义的有趣问题。
RBerteig

9

这更多的是抱怨而不是问题。您是否有关于递归的更具体的问题?像乘法一样,这不是人们写的很多东西。

说到乘法,请考虑一下。

题:

什么是a * b?

回答:

如果b为1,则为a。否则,它是a + a *(b-1)。

什么是a *(b-1)?有关解决方法,请参见上面的问题。


@安德鲁·格林(Andrew Grimm):好问题。此定义适用于自然数,而不是整数。
S.Lott

9

我认为这个非常简单的方法应该可以帮助您理解递归。该方法将自行调用,直到满足特定条件为止,然后返回:

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。对接不会调用writeNumbers(-1);

此代码基本上与以下代码相同:

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

那么为什么要使用递归,您可能会问,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

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


1
大声笑-我花了一秒钟才意识到您正在使用JavaScript!我看到了“函数”,并认为PHP然后意识到变量不是以$开头。然后我以为C#可以使用var这个词-但是方法不称为函数!
ozzy432836 '16

8

实际上,您使用递归来减少手头问题的复杂性。应用递归,直到达到可以轻松解决的简单基本情况。这样,您可以解决最后的递归步骤。并通过所有其他递归步骤解决您的原始问题。


1
我同意这个答案。诀窍是识别并解决基本(最简单)的情况。然后以最简单的情况(已经解决)来表达问题。
瑟吉奥·阿科斯塔

6

我将举一个例子来解释。

你知道什么!手段?如果不是,请访问:http : //en.wikipedia.org/wiki/Factorial

3!= 1 * 2 * 3 = 6

这里有一些伪代码

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

因此,让我们尝试一下:

factorial(3)

n 0?

没有!

因此,我们对递归进行了更深入的研究:

3 * factorial(3-1)

3-1 = 2

是2 == 0吗?

没有!

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

是1 == 0吗?

没有!

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

是0 == 0吗?

是!

我们的案子微不足道

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

希望对你有帮助


这不是考虑递归的有用方法。一个常见的错误初学者提出的是,试着想象会发生什么内部,而不是仅仅相信/证明它会返回正确答案recusive电话, -这答案似乎鼓励。
ShreevatsaR

理解递归的更好方法是什么?我不是说您必须以​​这种方式查看每个递归函数。但这帮助我了解它是如何工作的。
Zoran Zaric

1
[顺带一提,我没有投票-1。]您可能会这样想:信任阶乘(n-1)正确给出(n-1)!=(n-1)* ... * 2 * 1,然后n factorial(n-1)给出n *(n-1) ... * 2 * 1,即n!。管他呢。[如果您想学习自己编写递归函数,不仅要看一些函数的作用。]
ShreevatsaR 2009年

我在解释递归时使用了阶乘,并且我认为递归失败的一个常见原因之一是因为被解释者不喜欢数学,并且陷入了困境。(是否不喜欢数学的人应该编码是另一个问题)。因此,我通常尝试在可能的情况下使用非数学示例。
Tony Meyer

5

递归

方法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...
    }
}

本例中的基本情况在哪里?
Kunal Mukherjee

4

您正在使用哪本书?

实际上不错的标准算法教科书是Cormen&Rivest。我的经验是,它很好地教授了递归。

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

另外,一般来说30页很多,使用一种编程语言编写的30页令人困惑。在从一般书籍中理解递归之前,不要尝试用C或Java学习递归。


4

递归函数只是一个需要多次调用的函数。如果您需要多次处理某个东西,这很有用,但是不确定实际需要多少次。在某种程度上,您可以将递归函数视为一种循环。但是,就像循环一样,您需要为打破流程指定条件,否则流程将变成无限。


4

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中进行更多练习。


javabat.com是一个非常有用的网站,它将帮助人们进行递归思考。我强烈建议您去那里,尝试自己解决递归问题。
天堂

3

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

1:假设您有一个对f(n-1)正确的函数,请构建f使f(n)正确。2:构建f,以使f(1)正确。

这样可以在数学上证明该函数是正确的,称为归纳法。它等效于具有不同的基本情况,或更复杂的功能涉及多个变量)。也等同于想象f(x)对于所有x都是正确的

现在来看一个“简单”的例子。建立一个函数,该函数可以确定是否有可能使5分和7分的硬币组合成x分。例如,按2x5 + 1x7可以有17美分,但不可能有16美分。

现在,假设您有一个函数告诉您是否有可能创建x cents(只要x <n)。调用此函数can_create_coins_small。想象如何为n创建函数应该相当简单。现在构建您的函数:

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;
}

这里的技巧是认识到can_create_coins适用于n的事实,这意味着您可以用can_create_coins替代can_create_coins_small,给出:

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;
}

最后要做的事情是有一个基本案例来停止无限递归。请注意,如果您尝试创建0美分,则可以通过没有硬币来实现。添加此条件将得出:

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;
}

可以证明,使用称为无限下降的方法,该函数将始终返回,但这在此不是必需的。您可以想象f(n)仅调用n的较低值,并且最终总是达到0。

要使用此信息来解决您的河内塔问题,我认为诀窍是假设您具有将n-1个平板电脑从a移至b(对于任何a / b)的功能,试图将n个表格从a移至b 。


3

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)

什么大写?严重的是,大约20年前,它们确实在LISP中过时了。
塞巴斯蒂安·格罗格

好吧,我是在现在已有17年历史的Lisp Machine模型上写的。实际上,我在侦听器中编写了未格式化的函数,进行了一些编辑,然后使用PPRINT对其进行了格式化。那把代码变成了CAPS。
Rainer Joswig

3

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

实际上,这是一个有用的反例,因为您的递归调用应该更简单而不是更难。解释递归到5岁甚至更困难,尽管您可以将递归停止为0,但是没有简单的解决方案来解释递归到0岁。

要使用递归解决问题,请先将其细分为一个或多个可以以相同方式解决的简单问题,然后在问题足够简单而无需进一步递归解决的情况下,可以返回更高级别。

实际上,那是如何解决递归问题的递归定义。


3

子级隐式使用递归,例如:

迪士尼世界之旅

我们到了吗?

我们到了吗?(很快)

我们到了吗(差不多...)

我们到了吗(SHHHH)

我们到了吗?(!!!!!)

这时孩子睡着了...

此倒计时功能是一个简单的示例:

function countdown()
      {
      return (arguments[0] > 0 ?
        (
        console.log(arguments[0]),countdown(arguments[0] - 1)) : 
        "done"
        );
      }
countdown(10);

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

根据乔姆斯基的说法,人类语言的本质是有限的大脑产生他认为是无限语法的能力。这样,他不仅意味着我们可以说的内容没有上限,而且我们的语言所拥有的句子数量没有上限,任何特定句子的大小也没有上限。乔姆斯基声称,人类语言所有这些创造力的基础工具是递归:递归一个短语在另一个相同类型的短语中的能力。如果我说“约翰兄弟的房子”,我有一个名词“房子”,它出现在名词短语“兄弟的房子”中,而该名词短语出现在另一个名词短语“约翰的兄弟房子”中。这很有意义,而且

参考资料


2

使用递归解决方案时,我总是尝试:

  • 首先确定基本情况,即在阶乘解中n = 1时
  • 尝试为所有其他情况提出一个一般规则

另外,递归解决方案的类型也不同,存在分而治之的方法,对分形和许多其他方法很有用。

如果您可以先解决更简单的问题,这也将有所帮助。一些示例正在求解阶乘并生成第n个斐波那契数。

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

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


我想知道首先提出一个通用规则(递归调用)是否比开始时“简单”,这是否更好?然后,基于最简单的情况,基本情况应该变得显而易见。这就是我倾向于考虑递归解决问题的方式。
dlaliberte 2011年

2

哎哟。去年,我试图找出河内的塔楼。关于TOH的棘手问题是,这不是一个简单的递归示例-您具有嵌套的递归,这也改变了每次调用时信号塔的作用。使我有意义的唯一方法是从字面上可视化环在我心中的运动,并用语言表达递归调用的含义。我将从一个戒指开始,然后是两个,然后是三个。我实际上是在互联网上订购游戏的。我花了两三天的时间才能弄明白它。


1

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

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

无论如何,除了经典的例子(因式子效率低下且易于弄平,阶乘是最糟糕的例子,斐波那契,河内...)-这是人为的(我很少,如果有的话,在实际的编程案例中很少使用它们),有趣的地方是真正使用它。

一个很常见的情况是走一棵树(或一棵图,但一般来说树是更常见的)。
例如,文件夹层次结构:要列出文件,请对其进行迭代。如果找到子目录,则列出文件的函数会使用新文件夹作为参数来调用自身。从列出此新文件夹(及其子文件夹!)回来时,它将恢复其上下文,直到下一个文件(或文件夹)。
另一个具体情况是在绘制GUI组件的层次结构时:通常有容器(如窗格)来容纳也可以是窗格的组件或复合组件等。绘制例程递归地调用每个组件的绘制函数,调用其拥有的所有组件的绘画功能,等等。

不知道我是否很清楚,但是我想展示现实世界中对教材的使用,因为这是我过去曾绊脚石。


1

想想工蜂。它试图做蜂蜜。它会尽力而为,并期望其他工蜂能够吸收其余的蜂蜜。当蜂窝装满时,它会停止。

认为它是魔术。您有一个与要尝试实现的函数同名的函数,当您将其赋予子问题时,它会为您解决该问题,而您唯一需要做的就是将零件的解决方案与它的解决方案集成在一起给你。

例如,我们要计算列表的长度。让我们调用函数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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.