了解递归函数的工作原理

时间:2014-09-05 00:31:20

标签: function recursion language-agnostic

正如标题所解释的那样,我有一个非常基本的编程问题,我还没有想到。过滤掉所有(非常聪明)"为了理解递归,你必须首先理解递归。"各种在线线程的回复我仍然没有得到它。

了解当面对不知道我们不知道的事情时,我们可能会倾向于提出错误的问题或者错误地提出正确的问题,我会分享我的想法和#34;我的问题是希望有类似前景的人可以分享一些知识,这将有助于为我打开递归灯泡!

这是函数(语法用Swift编写):

func sumInts(a: Int, b: Int) -> Int {
    if (a > b) {
        return 0
    } else {
        return a + sumInts(a: a + 1, b: b)
    }
}

我们将使用2和5作为我们的论点:

println(sumInts(a: 2, b: 5))

显然答案是14.但我还不清楚这个价值是如何实现的。

这是我的两个挂断:

  1. 递归调用该函数,直到满足条件。那个条件是>湾满足此条件时,返回0.乍一看,我希望返回值为0,这显然是不正确的。

  2. 打印出' a'的价值。在每次迭代中产生一个我期望的值:2,3,4,5(此时5 + 1> b满足第一个条件:a> b)但我仍然不知道如何值达到14。

  3. 我首先想到的是,类似于以下内容的东西正在神奇地发生:

    var answer = a;
    answer += a+1 until a > b;
    return answer;   
    

    所以排除魔法,我只是没有得到什么。我很想知道发生的事情不仅仅是隐含的。

    如果有人能够解释在这种功能中技术上发生了什么,以及为什么结果不是0以及最终如何a + sumInts(a: a + 1, b: b) = 14,我将永远负债。

18 个答案:

答案 0 :(得分:127)

  

1.函数被递归调用,直到满足条件。那个条件是a > b。当满足这个条件时,返回0.乍一看,我希望返回值为0,这显然是不正确的。

以下是计算机计算sumInts(2,5)能够思考的内容:

I want to compute sumInts(2, 5)
for this, I need to compute sumInts(3, 5)
and add 2 to the result.
  I want to compute sumInts(3, 5)
  for this, I need to compute sumInts(4, 5)
  and add 3 to the result.
    I want to compute sumInts(4, 5)
    for this, I need to compute sumInts(5, 5)
    and add 4 to the result.
      I want to compute sumInts(5, 5)
      for this, I need to compute sumInts(6, 5)
      and add 5 to the result.
        I want to compute sumInts(6, 5)
        since 6 > 5, this is zero.
      The computation yielded 0, therefore I shall return 5 = 5 + 0.
    The computation yielded 5, therefore I shall return 9 = 4 + 5.
  The computation yielded 9, therefore I shall return 12 = 3 + 9.
The computation yielded 12, therefore I shall return 14 = 2 + 12.

如你所见,对函数sumInts的一些调用实际上返回0但是这不是最终值,因为计算机仍然必须向该0添加5,然后将4添加到结果,然后是3,然后是2,正如我们计算机思想的最后四句话所描述的那样。请注意,在递归中,计算机不仅必须计算递归调用,还必须记住如何处理递归调用返回的值。计算机内存的一个特殊区域叫做堆栈,其中保存了这种信息,这个空间是有限的,过于递归的函数会耗尽堆栈:这是堆栈溢出< / em>将其名称命名为我们最喜​​爱的网站。

你的陈述似乎隐含地假设计算机在进行递归调用时会忘记它的作用,但事实并非如此,这就是为什么你的结论与你的观察不符的原因。

  

2.在每次迭代中打印出'a'的值产生我期望的值:2,3,4,5(此时5 + 1> b满足第一条件:a&gt; b)但我仍然没有看到如何实现14的价值。

这是因为返回值本身不是a,而是a的值与递归调用返回的值之和。

答案 1 :(得分:104)

认为混淆源于将其视为多次调用的“同一功能”。如果你认为它是“被调用的同一个函数的许多副本”,那么它可能更清楚:

只有一个函数副本返回0,而不是第一个(它是最后一个)。所以调用第一个的结果不是0。

对于第二点困惑,我认为用英语拼出递归会更容易。阅读此行:

return a + sumInts(a + 1, b: b)

as“返回'a'加值的值(该函数的另一个副本的返回值,这是'a'加号的副本值(该函数的另一个副本的返回值,这是第二个副本的值'a'加上(...“,函数的每个副本产生一个新的副本,增加1,直到满足a&gt; b条件。

当你到达a&gt; b条件为真,你在运行过程中有一个(可能是任意)长的函数副本,所有副本都在等待下一个副本的结果,找出它们应该添加到'a'的内容。

(编辑:还有一点需要注意的是,我提到的函数的副本堆栈是一个真实的东西,占用了实际的内存,并且如果程序变得太大会使程序崩溃。编译器可以优化它在某些情况下,但是耗尽堆栈空间是许多语言中递归函数的一个重要且不幸的限制。)

答案 2 :(得分:47)

要理解递归,您必须以不同的方式考虑问题。而不是整个有意义的大步骤的逻辑顺序,而是一个大问题,分解成更小的问题并解决这些问题,一旦你有子问题的答案,你结合子问题的结果,使解决更大的问题。想想你和你的朋友需要在一个巨大的桶中计算弹珠的数量。你们每个人都拿一个较小的水桶然后单独统计这些水桶,当你完成后,你们将总数加在一起..好了,如果你们每个人找到一些朋友并进一步拆分水桶,那么你只需要等待这些其他朋友来找出他们的总数,把它带回给你们每个人,你把它加起来。等等。特殊情况是当你只得到1个大理石计数然后你只需要将它返回并说1.让你上面的其他人做你已经完成的添加。

您必须记住每次函数递归调用自身时,它会创建一个带有问题子集的新上下文,一旦解析了该部分,它就会被返回,以便上一次迭代可以完成。

让我告诉你步骤:

sumInts(a: 2, b: 5) will return: 2 + sumInts(a: 3, b: 5)
sumInts(a: 3, b: 5) will return: 3 + sumInts(a: 4, b: 5)
sumInts(a: 4, b: 5) will return: 4 + sumInts(a: 5, b: 5)
sumInts(a: 5, b: 5) will return: 5 + sumInts(a: 6, b: 5)
sumInts(a: 6, b: 5) will return: 0

一旦执行了sumInts(a:6,b:5),就可以计算出结果,从而使用你得到的结果返回到链中:

 sumInts(a: 6, b: 5) = 0
 sumInts(a: 5, b: 5) = 5 + 0 = 5
 sumInts(a: 4, b: 5) = 4 + 5 = 9
 sumInts(a: 3, b: 5) = 3 + 9 = 12
 sumInts(a: 2, b: 5) = 2 + 12 = 14.

表示递归结构的另一种方法:

 sumInts(a: 2, b: 5) = 2 + sumInts(a: 3, b: 5)
 sumInts(a: 2, b: 5) = 2 + 3 + sumInts(a: 4, b: 5)  
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + sumInts(a: 5, b: 5)  
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + 5 + sumInts(a: 6, b: 5)
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + 5 + 0
 sumInts(a: 2, b: 5) = 14 

答案 3 :(得分:40)

递归是一个难以理解的主题,我不认为我可以在这里完全正义。相反,我将尝试专注于您在此处的特定代码片段,并尝试描述解决方案工作原理的直觉以及代码如何计算其结果的机制。

您在此处给出的代码解决了以下问题:您想知道从a到b的所有整数的总和。对于您的示例,您需要2到5之间的数字之和,即

  

2 + 3 + 4 + 5

当尝试递归地解决问题时,最初的步骤之一应该是弄清楚如何将问题分解为具有相同结构的较小问题。因此,假设您想要总结从2到5的数字,包括两者。简化这一点的一种方法是注意上面的总和可以重写为

  

2 +(3 + 4 + 5)

这里,(3 + 4 + 5)碰巧是3和5之间所有整数的总和,包括3和5。换句话说,如果你想知道2和5之间所有整数的总和,首先计算3和5之间所有整数的总和,然后加2。

那么如何计算3到5之间所有整数的总和?好吧,这笔钱是

  

3 + 4 + 5

可以被认为是

  

3 +(4 + 5)

这里,(4 + 5)是4和5之间所有整数的总和,包括4和5。因此,如果你想计算3到5之间所有数字的总和,你可以计算4到5之间所有整数的总和,然后加3。

这里有一个模式!如果要计算a和b之间的整数之和,则可以执行以下操作。首先,计算a + 1和b之间的整数之和。接下来,添加一个总数。您会注意到&#34;计算a + 1和b之间的整数之和,包括&#34;恰好是我们已经尝试解决的问题,但参数略有不同。我们不是从a到b计算包容性,而是从+ 1到b计算,包括在内。这是递归步骤 - 解决更大的问题(&#34;从a到b的总和,包括&#34;),我们将问题减少到自身的较小版本(&#34;来自+的总和) 1到b,包括在内。&#34;)。

如果你看一下上面的代码,你会发现其中有这一步:

return a + sumInts(a + 1, b: b)

这段代码只是上述逻辑的翻译 - 如果你想要从a到b的总和,首先将a + 1加到b,包括(这是对{{1}的递归调用s),然后添加sumInt

当然,这种方法本身并没有实际发挥作用。例如,如何计算5到5之间所有整数的总和?好吧,使用我们当前的逻辑,你可以计算6到5之间所有整数的总和,然后加上5.那么如何计算6到5之间所有整数的总和(包括6和5)?好吧,使用我们当前的逻辑,你计算7和5之间所有整数的总和,然后加6。你会注意到这里有一个问题 - 这只是继续前进和继续!

在递归问题解决中,需要有一些方法来停止简化问题,而只是直接解决它。通常情况下,您会找到一个简单的案例,可以立即确定答案,然后构建您的解决方案,以便在出现时直接解决简单案例。这通常称为基本案例递归基础

那么这个特定问题的基本情况是什么?当你从a到b总结整数时,如果a恰好大于b,那么答案就是0 - 该范围内没有任何数字!因此,我们将按如下方式构建我们的解决方案:

  1. 如果a&gt; b,答案是0。
  2. 否则(a≤b),得到如下答案:
    1. 计算a + 1和b之间的整数之和。
    2. 添加一个以获得答案。
  3. 现在,将此伪代码与您的实际代码进行比较:

    a

    请注意,伪代码中概述的解决方案与此实际代码之间几乎完全是一对一的映射。第一步是基本情况 - 如果您要求空数范围的总和,则得到0.否则,计算a + 1和b之间的总和,然后再添加a。

    到目前为止,我已经在代码背后给出了一个高层次的想法。但是你有两个非常好的问题。首先,为什么不总是返回0,假设函数说如果a>&gt;则返回0。 B'其次,14实际上来自哪里?让我们依次看看这些。

    让我们尝试一个非常非常简单的案例。如果您致电func sumInts(a: Int, b: Int) -> Int { if (a > b) { return 0 } else { return a + sumInts(a + 1, b: b) } } 会怎样?在这种情况下,通过代码进行跟踪,您会看到函数只返回0.这是正确的事情,对于 - 范围内没有任何数字。现在,努力尝试一下。致电sumInts(6, 5)时会发生什么?那么,发生了什么:

    1. 您致电sumInts(5, 5)。我们落入sumInts(5, 5)分支,它返回“a + sumInts(6,5)”的值。
    2. 为了让else确定sumInts(5, 5)是什么,我们需要暂停我们正在执行的操作并拨打sumInts(6, 5)
    3. sumInts(6, 5)被召唤。它进入sumInts(6, 5)分支并返回if。但是,此0实例已由sumInts调用,因此返回值将传回sumInts(5, 5),而不是传递给顶级调用方。
    4. sumInts(5, 5)现在可以计算sumInts(5, 5)以获取5 + sumInts(6, 5)。然后它将它返回给顶级调用者。
    5. 注意这里是如何形成值5的。我们从5打了一个电话。这启动了另一个递归调用,该调用返回的值将信息传回sumInts。然后对sumInts(5, 5)的调用进行了一些计算并将值返回给调用者。

      如果您使用sumInts(5, 5)尝试此操作,那么将会发生什么:

      • sumInts(4, 5)尝试返回sumInts(4, 5)。为此,它会调用4 + sumInts(5, 5)
        • sumInts(5, 5)尝试返回sumInts(5, 5)。为此,它会调用5 + sumInts(6, 5)
        • sumInts(6, 5)将0返回sumInts(6, 5) sumInts(5,5)sumInts(5, 5).</li> <li> sumInts(6,5)now has a value for 5 + 0 = 5`。
      • , namely 0. It then returns现在有sumInts(4, 5)的值,即5.然后返回sumInts(5, 5)

      换句话说,返回的值是通过一次一个地汇总值形成的,每次将特定递归调用返回的一个值取为4 + 5 = 9并添加当前值sumInts。当递归触底时,最深的调用返回0.但是,该值不会立即退出递归调用链;相反,它只是将值交还给它上面一层的递归调用。以这种方式,每个递归调用只增加一个数字并在链中返回更高的数字,最终得到总和。作为练习,尝试将其追溯到a,这是您想要开始的。

      希望这有帮助!

答案 4 :(得分:22)

到目前为止,你已经得到了一些很好的答案,但我会再添加一个不同的方法。

首先,我写了许多关于简单递归算法的文章,你可能会感兴趣;见

http://ericlippert.com/tag/recursion/

http://blogs.msdn.com/b/ericlippert/archive/tags/recursion/

这些是最新的订单,所以从底部开始。

其次,到目前为止,所有答案都通过考虑函数激活来描述递归语义。每个调用都会生成一个新的激活,并且在此激活的上下文中执行递归调用。这是一种很好的思考方式,但还有另一种相同的方式:智能文本搜索和替换

让我将您的功能重写为更紧凑的形式;不要认为这是任何特定的语言。

s = (a, b) => a > b ? 0 : a + s(a + 1, b)

我希望这是有道理的。如果您不熟悉条件运算符,则其格式为condition ? consequence : alternative,其含义将变得清晰。

现在我们希望评估s(2,5)我们这样做是通过用函数体替换调用的文本,然后用a2替换b { {1}}:

5

现在评估条件。我们以s(2, 5) ---> 2 > 5 ? 0 : 2 + s(2 + 1, 5) 为文本替换2 > 5

false

现在以文本方式替换所有错误条件和替代以及所有具有结果的真实条件。我们只有错误的条件,所以我们 textually 用替代方法替换该表达式:

---> false ? 0 : 2 + s(2 + 1, 5)

现在,为了节省我必须键入所有---> 2 + s(2 + 1, 5) 个符号,在文本上用它的值替换常量算术。 (这有点像作弊,但我不想跟踪所有的括号!)

+

现在搜索和替换,这次是通话正文,---> 2 + s(3, 5) 3a b。我们将在括号中替换呼叫:

5

现在我们继续执行相同的文本替换步骤:

---> 2 + (3 > 5 ? 0 : 3 + s(3 + 1, 5))

我们在这里所做的只是直截了当的文字替换。真的,我不应该用“3”代替“2 + 1”等等,直到我不得不这样做,但教学上它本来难以阅读。

函数激活只不过是将函数调用替换为调用体,并将形式参数替换为相应的参数。你必须小心谨慎地引入括号,但除此之外,它只是文本替换。

当然,大多数语言实际上实施激活作为文本替换,但逻辑上就是它的原样。

那么什么是无限递归呢?文本替换不会停止的递归!注意最终我们如何进入一个不再需要---> 2 + (false ? 0 : 3 + s(3 + 1, 5)) ---> 2 + (3 + s(3 + 1, 5)) ---> 2 + (3 + s(4, 5)) ---> 2 + (3 + (4 > 5 ? 0 : 4 + s(4 + 1, 5))) ---> 2 + (3 + (false ? 0 : 4 + s(4 + 1, 5))) ---> 2 + (3 + (4 + s(4 + 1, 5))) ---> 2 + (3 + (4 + s(5, 5))) ---> 2 + (3 + (4 + (5 > 5 ? 0 : 5 + s(5 + 1, 5)))) ---> 2 + (3 + (4 + (false ? 0 : 5 + s(5 + 1, 5)))) ---> 2 + (3 + (4 + (5 + s(5 + 1, 5)))) ---> 2 + (3 + (4 + (5 + s(6, 5)))) ---> 2 + (3 + (4 + (5 + (6 > 5 ? 0 : s(6 + 1, 5))))) ---> 2 + (3 + (4 + (5 + (true ? 0 : s(6 + 1, 5))))) ---> 2 + (3 + (4 + (5 + 0))) ---> 2 + (3 + (4 + 5)) ---> 2 + (3 + 9) ---> 2 + 12 ---> 14 替换的步骤,然后我们可以只应用算术规则。

答案 5 :(得分:11)

我通常会弄清楚递归函数的工作方式是查看基本情况并向后工作。这是适用于此功能的技术。

首先是基本情况:

sumInts(6, 5) = 0

然后就在call stack

之上的电话
sumInts(5, 5) == 5 + sumInts(6, 5)
sumInts(5, 5) == 5 + 0
sumInts(5, 5) == 5

然后调用就在调用堆栈中的调用之上:

sumInts(4, 5) == 4 + sumInts(5, 5)
sumInts(4, 5) == 4 + 5
sumInts(4, 5) == 9

等等:

sumInts(3, 5) == 3 + sumInts(4, 5)
sumInts(3, 5) == 3 + 9
sumInts(3, 5) == 12

等等:

sumInts(2, 5) == 2 + sumInts(3, 5)
sumInts(4, 5) == 2 + 12
sumInts(4, 5) == 14

请注意我们已初次调用该函数 sumInts(2, 5) == 14

执行这些调用的顺序:

sumInts(2, 5)
sumInts(3, 5)
sumInts(4, 5)
sumInts(5, 5)
sumInts(6, 5)

这些调用返回的顺序:

sumInts(6, 5)
sumInts(5, 5)
sumInts(4, 5)
sumInts(3, 5)
sumInts(2, 5)

请注意,我们通过按照返回的顺序跟踪调用来得出关于函数如何运作的结论。

答案 6 :(得分:5)

我会试一试。

执行等式a + sumInts(a + 1,b),我将展示最终答案是14。

//the sumInts function definition
func sumInts(a: Int, b: Int) -> Int {
    if (a > b) {
        return 0
    } else {
        return a + sumInts(a + 1, b)
    }
}

Given: a = 2 and b = 5

1) 2 + sumInts(2+1, 5)

2) sumInts(3, 5) = 12
   i) 3 + sumInts(3+1, 5)
   ii) 4 + sumInts(4+1, 5)
   iii) 5 + sumInts(5+1, 5)
   iv) return 0
   v) return 5 + 0
   vi) return 4 + 5
   vii) return 3 + 9

3) 2 + 12 = 14.

如果您有任何其他问题,请告诉我们。

以下示例中的递归函数的另一个示例。

一名男子刚刚大学毕业。

t是以年为单位的时间量。

退休前的实际工作年数,可按以下方式计算:

public class DoIReallyWantToKnow 
{
    public int howLongDoIHaveToWork(int currentAge)
    {
      const int DESIRED_RETIREMENT_AGE = 65;
      double collectedMoney = 0.00; //remember, you just graduated college
      double neededMoneyToRetire = 1000000.00

      t = 0;
      return work(t+1);
    }

    public int work(int time)
    {
      collectedMoney = getCollectedMoney();

      if(currentAge >= DESIRED_RETIREMENT_AGE 
          && collectedMoney == neededMoneyToRetire
      {
        return time;
      }

      return work(time + 1);
    }
}

这应该足以压抑任何人,哈哈。 ;-P

答案 7 :(得分:5)

递归。在计算机科学中,在有限自动机的主题下深入介绍了递归。

最简单的形式是自我参考。例如,说“我的车是汽车”是递归声明。问题是该语句是无限递归,因为它永远不会结束。 “汽车”声明中的定义是它是“汽车”,因此它可以被替换。然而,没有尽头,因为在替代的情况下,它仍然变成“我的车是汽车”。

如果声明是“我的车是宾利,我的车是蓝色的”,这可能会有所不同。在这种情况下,在第二种情况下,汽车的替代可以是“宾利”,导致“我的宾利是蓝色的”。这些类型的替换在计算机科学中通过Context-Free Grammars在数学上进行了解释。

实际替换是生产规则。鉴于该语句由S表示,并且该car是一个可以是“bentley”的变量,该语句可以递归重建。

S -> "my"S | " "S | CS | "is"S | "blue"S | ε
C -> "bentley"

这可以通过多种方式构建,因为每个|意味着有一个选择。 S可以被这些选项中的任何一个替换,并且S始终为空。 ε表示终止生产。就像S可以被替换一样,其他变量也是如此(只有一个变量,而C代表“bentley”)。

首先,S为空,并将其替换为第一个选择"my"S S

"my"S

S仍可替换,因为它代表变量。我们可以再次选择“我的”,或者ε来结束它,但让我们继续制作我们的原始陈述。我们选择的空格意味着S替换为" "S

"my "S

接下来让我们选择C

"my "CS

C只有一个替代选择

"my bentley"S

又是S

的空间
"my bentley "S

依此类推"my bentley is"S"my bentley is "S"my bentley is blue"S"my bentley is blue"(替换S代表ε结束制作)我们已经递归地建立了我的声明“我的宾利是蓝色的”

将递归视为这些产品和替代品。该过程中的每个步骤都会替换其前一步,以产生最终结果。在从2到5的递归和的确切示例中,您最终得到了生产

S -> 2 + A
A -> 3 + B
B -> 4 + C
C -> 5 + D
D -> 0

这变为

2 + A
2 + 3 + B
2 + 3 + 4 + C
2 + 3 + 4 + 5 + D
2 + 3 + 4 + 5 + 0
14

答案 8 :(得分:4)

你可能对Nisan和Schocken的implementation of functions感兴趣。链接的pdf是免费在线课程的一部分。它描述了虚拟机实现的第二部分,学生应该在其中编写虚拟机语言到机器语言编译器。他们提出的函数实现能够递归,因为它是基于堆栈的。

向您介绍功能实现:请考虑以下虚拟机代码:

enter image description here

如果Swift编译为这种虚拟机语言,那么以下Swift代码块:

mult(a: 2, b: 3) - 4

将编译为

push constant 2  // Line 1
push constant 3  // Line 2
call mult        // Line 3
push constant 4  // Line 4
sub              // Line 5

虚拟机语言围绕全局堆栈设计。 push constant n将整数推送到此全局堆栈。

执行第1行和第2行后,堆栈如下所示:

256:  2  // Argument 0
257:  3  // Argument 1

256257是内存地址。

call mult将返回行号(3)推入堆栈,并为函数的局部变量分配空间。

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  0  // local 0

...它转到标签function mult。执行mult内的代码。执行该代码后,我们计算出2和3的乘积,该乘积存储在函数的第0个局部变量中。

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  6  // local 0

在来自mult return之前,你会注意到这一行:

push local 0  // push result

我们会将产品推到堆栈上。

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  6  // local 0
260:  6  // product

当我们返回时,会发生以下情况:

  • 将堆栈上的最后一个值弹出到第0个参数的内存地址(在本例中为256)。这恰好是最方便的地方。
  • 将堆栈中的所有内容丢弃到第0个参数的地址。
  • 转到返回行号(本例中为3),然后前进。

返回后我们准备执行第4行,我们的堆栈如下所示:

256:  6  // product that we just returned

现在我们将4推入堆栈。

256:  6
257:  4

sub是虚拟机语言的原始功能。它需要两个参数并将其结果返回到通常的地址:第0个参数的结果。

现在我们有了

256:  2  // 6 - 4 = 2

既然你知道函数调用是如何工作的,那么理解递归是如何工作的相对简单。 没有魔法,只是一个堆栈。

我已使用此虚拟机语言实现了sumInts功能:

function sumInts 0     // `0` means it has no local variables.
  label IF
    push argument 0
    push argument 1
    lte              
    if-goto ELSE_CASE
    push constant 0
    return
  label ELSE_CASE
    push constant 2
    push argument 0
    push constant 1
    add
    push argument 1
    call sumInts       // Line 15
    add                // Line 16
    return             // Line 17
// End of function

现在我称之为:

push constant 2
push constant 5
call sumInts           // Line 21

代码执行,我们一直到lte返回false的停止点。这就是堆栈在这一点上的样子:

// First invocation
256:  2   // argument 0
257:  5   // argument 1
258:  21  // return line number
259:  2   // augend
// Second
260:  3   // argument 0
261:  5   // argument 1
262:  15  // return line number
263:  3   // augend
// Third
264:  4   // argument 0
265:  5   // argument 1
266:  15  // return line number
267:  4   // augend
// Fourth
268:  5   // argument 0
269:  5   // argument 1
270:  15  // return line number
271:  5   // augend
// Fifth
272:  6   // argument 0
273:  5   // argument 1
274:  15  // return line number
275:  0   // return value

现在让我们“放松”我们的递归。 return 0并转到第15行并前进。

271:  5
272:  0

第16行:add

271:  5

第17行:return 5并转到第15行并前进。

267:  4
268:  5

第16行:add

267:  9

第17行:return 9并转到第15行并前进。

263:  3
264:  9

第16行:add

263:  12

第17行:return 12并转到第15行并前进。

259:  2
260:  12

第16行:add

259:  14

第17行:return 14并转到第21行并前进。

256:  14

你有它。 递归:荣耀goto

答案 9 :(得分:4)

我在学习和真正理解递归时遇到的一个非常好的提示是花一些时间学习一种语言,除了通过递归之外没有任何形式的循环结构。这样你就可以很好地了解如何通过练习来使用递归。

我关注http://www.htdp.org/,这也是一个方案教程,也是关于如何根据架构和设计设计程序的一个很好的介绍。

但基本上,你需要投入一些时间。如果没有“坚定”地掌握递归,某些算法(例如回溯)对您来说似乎总是“难”甚至“神奇”。所以,坚持下去。 :-D

我希望这会有所帮助,祝你好运!

答案 10 :(得分:4)

我认为理解递归函数的最佳方法是实现它们用于处理递归数据结构。但是在原始函数sumInts(a: Int, b: Int)中递归计算从ab的数字之和,它似乎不是一个递归数据结构......让我们尝试一个稍微修改过的版本{{ 1}}其中sumInts(a: Int, n: Int)是您要添加的数字。

现在,sumInts递归n,这是一个自然数。仍然不是递归数据,对吧?好吧,使用Peano公理可以将自然数视为递归数据结构:

n

所以,0 =零,1 = Succesor(零),2 = Succesor(Succesor(零)),依此类推。

一旦有了递归数据结构,就可以获得该函数的模板。对于每个非递归情况,您可以直接计算该值。对于递归情况,您假设递归函数已经在工作并使用它来计算案例,但是解构参数。对于自然,这意味着我们将使用enum Natural = { case Zero case Successor(Natural) } 而不是Succesor(n)而不是n,而不是n我们将使用n - 1

// sums n numbers beginning from a
func sumInts(a: Int, n: Int) -> Int {
    if (n == 0) {
        // non recursive case
    } else {
        // recursive case. We use sumInts(..., n - 1)
    }
}

现在递归函数更容易编程。首先,基础案例n=0。如果我们不想添加数字,我们应该返回什么?答案当然是0.

递归案例怎么样?如果我们要添加以n开头的a个数字,我们已经有一个适用于sumInts的{​​{1}}函数?好吧,我们需要添加n-1然后使用a调用sumInts,因此我们结束:

a + 1

好消息是,现在你不需要在递归的低级别思考。您只需要验证:

  • 对于递归数据的基本情况,它不使用递归计算答案。
  • 对于递归数据的递归情况,它使用对结构化数据的递归来计算答案。

答案 11 :(得分:3)

已经有很多好的答案。我还是试一试。
调用时,函数会获得分配的内存空间,它被堆叠在调用函数的内存空间上。在这个内存空间中,该函数保留传递给它的参数,变量及其值。此内存空间随着函数的结束返回调用而消失。随着堆栈概念的发生,调用函数的 memory-space 现在变为活动状态。

对于递归调用,相同的函数会将多个内存空间堆叠在一起。这就是全部。关于堆栈如何在计算机内存中工作的简单概念应该让您了解递归如何在实现中发生。

答案 12 :(得分:3)

有点偏离主题,我知道,但是...尝试在Google中查找递归 ...您将通过示例看到它意味着什么: - )


早期版本的Google返回了以下文字(引自内存):

  

递归

     

参见递归

2014年9月10日,关于递归的笑话已经更新:

  

递归

     

您的意思是:递归


有关其他回复,请参阅this answer

答案 13 :(得分:3)

将递归视为多个克隆做同样的事情......

您要求克隆[1]:“2到5之间的总数”

+ clone[1]               knows that: result is 2 + "sum numbers between 3 and 5". so he asks to clone[2] to return: "sum numbers between 3 and 5"
|   + clone[2]           knows that: result is 3 + "sum numbers between 4 and 5". so he asks to clone[3] to return: "sum numbers between 4 and 5"
|   |   + clone[3]       knows that: result is 4 + "sum numbers between 5 and 5". so he asks to clone[4] to return: "sum numbers between 5 and 5"
|   |   |   + clone[4]   knows that: result is 5 + "sum numbers between 6 and 5". so he asks to clone[5] to return: "sum numbers between 6 and 5"
|   |   |   |   clone[5] knows that: he can't sum, because 6 is larger than 5. so he returns 0 as result.
|   |   |   + clone[4]   gets the result from clone[5] (=0)  and sums: 5 + 0,  returning 5
|   |   + clone[3]       gets the result from clone[4] (=5)  and sums: 4 + 5,  returning 9
|   + clone[2]           gets the result from clone[3] (=9)  and sums: 3 + 9,  returning 12
+ clone[1]               gets the result from clone[2] (=12) and sums: 2 + 12, returning 14

和voilá!!

答案 14 :(得分:2)

上面的许多答案非常好。然而,解决递归问题的一种有用技巧是首先说出我们想要做的事情,并按照人类解决它来编写代码。在上面的例子中,我们想要总结一系列连续的整数(使用上面的数字):

2, 3, 4, 5  //adding these numbers would sum to 14

现在,请注意这些行令人困惑(没错,但令人困惑)。

if (a > b) {
    return 0 
}

为什么要测试a>b?以及为什么return 0

让我们更改代码,以更加贴近人类所做的事情

func sumInts(a: Int, b: Int) -> Int {
  if (a == b) {
    return b // When 'a equals b' I'm at the most Right integer, return it
  }
  else {
    return a + sumInts(a: a + 1, b: b)
  }
}

我们能做到更像人类吗?是!通常我们从左到右总结(2 + 3 + ...)。但是上面的递归是从右到左相加(... + 4 + 5)。更改代码以反映它(-可能有点吓人,但不是很多)

func sumInts(a: Int, b: Int) -> Int {
  if (a == b) {
    return b // When I'm at the most Left integer, return it
  }
  else {
    return sumInts(a: a, b: b - 1) + b
  }
}

有些人可能会觉得这个功能更加混乱,因为我们从“远”开始。结束,但练习可以让它感觉自然(这是另一种好的&#39;思考技巧:在解决递归问题时尝试两个方面)。而且,该函数反映了人类(大多数?)的作用:取所有左整数的总和并添加“下一个”&#39;正整数。

答案 15 :(得分:2)

我很难理解递归,然后我找到了this blog,我已经看到了这个问题所以我认为我必须要分享。你必须阅读这个博客,我发现这非常有用,它可以用堆栈解释,甚至可以解释两个递归如何逐步与堆栈一起工作。我建议你先了解一下堆栈是如何工作的,它在这里解释得非常好:journey-to-the-stack

then now you will understand how recursion works now take a look of this postUnderstand recursion step by step

enter image description here

它是一个程序:

def hello(x):
    if x==1:
        return "op"
    else:
        u=1
        e=12
        s=hello(x-1)
        e+=1
        print(s)
        print(x)
        u+=1
    return e

hello(3)

enter image description here enter image description here

答案 16 :(得分:2)

当我停止阅读其他人对此的看法或将其视为我可以避免的东西并且只是编写代码时,递归开始对我有意义。我发现解决方案存在问题,并试图复制解决方案而不看。当我无助地陷入困境时,我只看了解决方案。然后我回去试图复制它。我在多个问题上再次这样做,直到我发展了自己的理解并理解如何识别递归问题并解决它。当我达到这个水平时,我开始解决问题并解决它们。这对我有所帮助。有时,事情只能通过自己尝试和挣扎来学习;直到你“得到它”。

答案 17 :(得分:0)

让我以斐波那契数列为例,斐波那契是

  

t(n)= t(n-1)+ n;

     

如果n = 0,则为1

因此,让我们看看递归的工作原理,我只是将n中的t(n)替换为n-1,依此类推。它看起来:

  

t(n-1)= t(n-2)+ n + 1;

     

t(n-1)= t(n-3)+ n + 1 + n;

     

t(n-1)= t(n-4)+ n + 1 + n + 2 + n;

     

     

     

     

t(n)= t(n-k)+ ... +(n-k-3)+(n-k-2)+(n-k-1)+ n;

我们知道t(0)=(n-k)是否等于1,然后是n-k=0,所以n=k我们将k替换为n

  

t(n)= t(n-n)+ ... +(n-n + 3)+(n-n + 2)+(n-n + 1)+ n;

如果我们省略n-n,那么:

  

t(n)= t(0)+ ... + 3 + 2 + 1 +(n-1)+ n;

所以3+2+1+(n-1)+n是自然数。计算公式为Σ3+2+1+(n-1)+n = n(n+1)/2 => n²+n/2

fib的结果是:O(1 + n²) = O(n²)

这是理解递归关系的最佳方式