了解递归函数如何工作


115

正如标题所解释的那样,我有一个非常基本的编程问题,但我现在还无法理解。过滤掉所有(极其聪明的)“为了理解递归,您必须首先理解递归。” 来自各种在线话题的回复我还是不太明白。

了解到,当面对不知道的知识时,我们可能会提出错误的问题或错误地提出正确的问题,我会分享我“认为”的问题,希望有相貌相似的人可以分享一些一点知识,将有助于为我打开递归灯泡!

这是函数(语法用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。但是我不清楚如何实现这一价值。

这些是我的2个挂断电话:

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

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

我首先想到的是,与以下类似的事情正在神奇地发生:

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

所以排除魔术,我只是没有得到什么。我想了解的不仅仅是发生的事情。

如果有人能很好地解释这种功能在技术上会发生什么,以及为什么结果不为0,以及最终如何a + sumInts(a: a + 1, b: b) = 14,我将永远背负着您的债。


7
递归是那些编程概念之一,用数学术语比用代码更容易理解。有一个很好的定义在这里
blgt

5
LearnYouARecursion,来自世界一流教授的完整问题集!
recursion.ninja 2014年

15
我只需要敦促您在Google搜索框中输入“递归”。那些复活节彩蛋之一。我不会为您惊喜。
弗洛里斯2014年


Answers:


107

觉得混淆源于将其视为“相同功能”被多次调用的想法。如果您将其视为“正在调用同一功能的许多副本”,那么可能会更清楚:

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

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

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

为“返回'a'的值加(函数的另一个副本的返回值,即'a'的副本的值,即函数的另一个副本的返回值,即第二个副本的' a'加号(...“),函数的每个副本都会产生一个新的自身副本,并增加1,直到满足a> b条件。

在达到a> b条件为真时,您正在(可能任意)长堆的函数副本都在运行的中间,都在等待下一个副本的结果以查找它们的结果应该加到“ a”。

(编辑:另外,需要注意的是,我提到的函数的副本堆栈是占用真实内存的真实内容,如果程序太大,则会使程序崩溃。编译器可以在某些情况下对其进行优化情况,但是耗尽堆栈空间是许多语言中递归函数的重大和不幸的限制)


7
Catfish_Man:我想你把它钉了!将其视为具有相同功能的多个“副本”是完全有意义的。我仍在缠着它,但我想你已经把我送上了正确的道路!感谢您抽出宝贵的时间来帮助其他程序员!我会将您的答案标记为正确答案。祝你有美好的一天!
杰森·艾伍德

13
这是一个很好的类比-尽管请注意不要从字面上理解它,因为每个“副本”实际上是完全相同的代码。每个副本的不同之处在于它正在处理的所有数据。
Tim B

2
我对将其视为副本不太满意。我发现,更直观的解释是区分函数本身(代码,其作用)和与堆栈框架/执行上下文相关的函数调用(该函数的实例化)。该函数不拥有其局部变量,而是在调用(调用)函数时实例化它们。但是我想这将是递归的入门
Thomas

5
正确的术语是该函数有多个调用。每个调用都有其自己的变量和实例ab
Theodore Norvell 2014年

6
是的,可以为这个答案增加很多精度。我特意省去了“函数实例”和“单个函数调用的激活记录”之间的区别,因为这是额外的概念负担,并不能真正帮助理解问题。它有助于理解其他问题,因此它在其他地方仍然是有用的信息。这些评论似乎是个不错的地方:)
Catfish_Man 2014年

130

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,如的最后四个句子所述我们计算机的想法。请注意,在递归中,计算机不仅必须计算递归调用,而且还必须记住如何处理递归调用返回的值。计算机内存中有一个特殊的区域称为堆栈,用于存储此类信息,此空间有限并且过于递归的功能会使堆栈耗尽:这是堆栈溢出,它的名称赋予了我们最受欢迎的网站。

您的陈述似乎做出了一个隐式假设,即计算机在进行递归调用时会忘记它的状态,但是事实并非如此,这就是为什么您的结论与您的观察结果不符的原因。

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

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


3
感谢您抽出宝贵的时间写出这个很好的答案,迈克尔!+1!
杰森·艾伍德

9
@JasonElwood如果您进行修改sumInts,使其实际上写下“计算机思想”,可能会有所帮助。一旦您编写了此类功能的手册,您可能就会“明白了”!
Michael Le BarbierGrünewald2014年

4
这是一个很好的答案,尽管我注意到并没有要求在称为“堆栈”的数据结构上进行功能激活。递归可以通过连续传递样式来实现,在这种情况下,根本没有堆栈。堆栈只是一个-特别有效,因此在普遍使用中-是对连续性概念的重新定义。
埃里克·利珀特

1
@EricLippert虽然用于实现递归的技术本身是一个有趣的话题,但我不确定它是否对OP(希望了解“工作原理”)是否有用,可以使OP暴露于所使用的各种机制中。虽然延续传递风格或基于扩展语言(如TeX和M4)本质上并没有难度比更常见的编程范式,我不会进攻做上标记,这些“舶来品”和一点任何人善意的谎言,如“它总是发生在堆栈”应帮助OP了解概念。(而且总是涉及一种筹码。)
Michael Le BarbierGrünewald2014年

1
软件必须有某种方式可以记住它在做什么,以递归方式调用该函数,然后在返回时返回到原始状态。该机制的行为类似于堆栈,因此即使使用其他数据结构,也可以方便地将其称为堆栈。
Barmar 2014年

48

要了解递归,您必须以不同的方式考虑问题。您无需解决一个总体上有意义的大步骤,而是将一个大问题分解为较小的问题并加以解决,一旦对子问题有了答案,便可以将子问题的结果组合起来,得出解决更大的问题。想想您和您的朋友需要计算一个大桶中的大理石数量。你们每个人都占用较小的存储桶,并分别进行计数,完成后将总数加在一起。.现在,如果每个人都找到一些朋友并进一步分割存储桶,那么您只需要等待这些其他朋友来计算出他们的总数,将其带回你们每个人,然后加起来。等等。

您必须记住,函数每次以递归方式调用自身时,都会创建带有问题子集的新上下文,一旦解决了该部分,则将其返回,以便可以完成之前的迭代。

让我向您展示步骤:

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 

2
很好,罗布。您以一种非常清晰易懂的方式进行了说明。感谢您抽出宝贵的时间!
杰森·艾尔伍德

3
这是正在发生的事情的最清楚的表示,而无需深入研究它的理论和技术细节,可以清楚地显示执行的每个步骤。
布莱恩2014年

2
我很高兴。:)解释这些事情并不总是那么容易。谢谢你的夸奖。
罗布

1
+1。这就是我的描述方式,特别是最后一个结构示例。从视觉上展开正在发生的事情是有帮助的。
KChaloux 2014年

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之间(包括5和5)的所有整数的总和。因此,如果要计算3到5之间的所有数字的和(包括3和5),则要计算4到5之间的所有整数的和,然后加3。

这里有一个模式!如果要计算a和b之间的整数之和,包括以下各项,可以执行以下操作。首先,计算a +1和b(含)之间的整数之和。接下来,在总计中添加一个。您会注意到,“计算a + 1与b之间的整数之和,包括两端”与我们已经尝试解决的问题几乎相同,但参数略有不同。而不是从a到b(包括端点)进行计算,而是从a +1到b(包括端点)进行计算。这是递归步骤-要解决更大的问题(“从a到b的和,包括”),我们将问题缩小到自身的较小版本(“从a +1到b的和”)。

如果看一下上面的代码,您会发现其中包含以下步骤:

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

这段代码只是上述逻辑的翻译-如果要从a到b求和,包括从a +1到b的求和(包括对sumInts 的递归调用),然后添加a

当然,仅靠这种方法实际上是行不通的。例如,如何计算5到5之间的所有整数之和?好吧,使用我们当前的逻辑,您将计算6到5之间的所有整数之和,然后加5。那么,如何计算6到5之间的所有整数之和?好吧,使用我们当前的逻辑,您将计算7到5之间的所有整数之和,然后加上6。您会在这里注意到一个问题-这种情况一直存在!

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

那么这个特殊问题的基本情况是什么?当您将a到b的整数相加时,如果a恰好大于b,则答案为0-范围内没有任何数字!因此,我们将按照以下方式构建解决方案:

  1. 如果a> b,则答案为0。
  2. 否则(a≤b),得到如下答案:
    1. 计算a + 1和b之间的整数之和。
    2. 添加一个以获得答案。

现在,将此伪代码与您的实际代码进行比较:

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

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

到目前为止,我只给出了代码背后的高级想法。但是您还有另外两个非常好的问题。首先,鉴于函数说如果a> b会返回0,为什么这并不总是返回0?其次,这14个实际上来自何处?让我们依次看看这些。

让我们尝试一个非常非常简单的案例。打电话给我sumInts(6, 5)怎么办?在这种情况下,在代码中进行查找,您会看到该函数仅返回0。这是正确的做法-范围内没有任何数字。现在,尝试更努力。打电话时会sumInts(5, 5)怎样?好吧,这是发生了什么:

  1. 你打电话sumInts(5, 5)。我们陷入else分支,该分支返回`a + sumInts(6,5)的值。
  2. 为了sumInts(5, 5)确定是什么sumInts(6, 5),我们需要暂停正在执行的操作并致电sumInts(6, 5)
  3. sumInts(6, 5)被叫。它进入if分支并返回0。但是,此实例sumInts是由调用的sumInts(5, 5),因此返回值将传递回sumInts(5, 5),而不是传递给顶级调用者。
  4. sumInts(5, 5)现在可以计算5 + sumInts(6, 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)
    • sumInts(6, 5)返回0返回sumInts(5, 5).</li> <li>sumInts(5,5)now has a value forsumInts(6,5), namely 0. It then returns5 + 0 = 5`。
  • sumInts(4, 5)现在具有的值sumInts(5, 5),即5。然后返回4 + 5 = 9

换句话说,返回的值是通过一次将一个值相加而形成的,每次取一个特定的递归调用返回的一个值 sumInts然后加上的当前值a。当递归到达最低点时,最深层调用将返回0。但是,该值不会立即退出递归调用链。取而代之的是,它只是将值交给位于其上一层的递归调用。这样,每个递归调用只需再添加一个数字,然后在链中更高的位置将其返回,最终达到整体求和。作为练习,请尝试针对进行跟踪sumInts(2, 5),这是您要开始的。

希望这可以帮助!


3
感谢您抽出宝贵的时间来分享如此全面的答案!这里有大量有用的信息,这些信息正在帮助我着手处理递归函数,并且肯定会帮助将来偶然发现此帖子的其他人。再次感谢,祝您有美好的一天!
杰森·艾伍德

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,并b5

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

现在评估条件。我们从文字上替换2 > 5false

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

现在,在文本上将所有错误条件替换为替代条件,并将所有真实条件替换为结果。我们只有错误的条件语句,因此我们在文本上用另一种替换该表达式:

---> 2 + s(2 + 1, 5)

现在,为免我不得不键入所有这些+符号,请用文本将其常数替换为常量算术。(这有点作弊,但我不想跟踪所有括号!)

---> 2 + s(3, 5)

现在搜索并替换,这次3是为b a5b 进行调用的主体。我们将替换呼叫放在括号中:

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

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

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

我们在这里所做的只是简单的文本替换。确实,在我不得不之前,我不应该用“ 3”代替“ 2 + 1”,以此类推,但从教学上讲,它会变得很难阅读。

函数激活无非就是将函数调用替换为调用主体,并将形式参数替换为其相应的参数。您必须谨慎地聪明地引入括号,但除此之外,它仅仅是文本替换。

当然,大多数语言实际上并不将激活实现为文本替换,但是在逻辑上就是这样。

那么,无限递归又是什么呢?文本替换不会停止的递归!请注意,最终我们是如何走到无需s再替换的步骤,然后可以将规则应用于算术运算。


很好的例子,但是当您继续进行更复杂的计算时,它会让您心碎。例如。在二叉树中找到共同祖先。
CodeYogi

11

我通常了解递归函数的工作方式是查看基本情况并向后工作。这是应用于此功能的技术。

首先是基本情况:

sumInts(6, 5) = 0

然后在调用堆栈中位于其上方的调用

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)

请注意,通过对调用返回的顺序进行跟踪,我们得出了有关函数如何运行的结论。


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


5

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

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

如果说“我的车是宾利车,我的车是蓝色车”,这可能会有所不同。在这种情况下,第二种情况下的汽车替代可能是“本特利”,导致“我的本特利是蓝色的”。在计算机科学中,通过上下文无关的语法在数学上解释了这些类型的替换

实际替代是生产规则。假定该语句由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

我不确定有限状态自动机或无上下文语法是否可以帮助人们建立关于递归的第一手直觉的最佳示例。它们是很好的例子,但对于以前没有CS背景的程序员来说可能有点陌生。

4

我认为理解递归函数的最好方法是认识到它们是用来处理递归数据结构的。但是在您的原始函数sumInts(a: Int, b: Int)中,它a以递归方式计算从到的数字总和b,这似乎不是一个递归的数据结构...让我们尝试一个稍微修改的版本sumInts(a: Int, n: Int),其中n将添加多少个数字。

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

enum Natural = {
    case Zero
    case Successor(Natural)
}

因此,0 =零,1 =继承者(零),2 =继承者(Succesor(零)),依此类推。

一旦有了递归数据结构,便有了该函数的模板。对于每个非递归情况,您都可以直接计算该值。对于递归案例,您假定递归函数已经在工作,并使用它来计算案例,但要解构参数。在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函数适用于n-1?好了,我们需要添加a,然后调用sumIntsa + 1,所以我们结束于:

// sums n numbers beginning from a
func sumInts(a: Int, n: Int) -> Int {
    if (n == 0) {
        return 0
    } else {
        return a + sumInts(a + 1, n - 1)
    }
}

令人高兴的是,现在您无需考虑低级别的递归。您只需要验证以下内容:

  • 对于递归数据的基本情况,它无需使用递归即可计算答案。
  • 对于递归数据的递归情况,它使用对已分解数据的递归来计算答案。

4

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

向您介绍函数实现:考虑以下虚拟机代码:

在此处输入图片说明

如果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

就在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

现在,让我们“放松”我们的递归。return0,然后转到第15行并前进。

271:  5
272:  0

第16行: add

271:  5

第17行:return5,然后转到第15行并前进。

267:  4
268:  5

第16行: add

267:  9

第17行:return9,然后转到第15行并前进。

263:  3
264:  9

第16行: add

263:  12

第17行:return12,然后转到第15行并前进。

259:  2
260:  12

第16行: add

259:  14

第17行:return14,转到第21行,然后前进。

256:  14

你有它。递归:荣耀的goto


4

我在学习和真正理解递归时遇到的一个非常好的技巧是花一些时间学习一种语言,除了通过递归之外,它没有任何形式的循环构造。这样一来,您将对如何通过练习使用递归有很好的了解。

我紧随http://www.htdp.org/,它不仅是Scheme教程,还是关于如何根据体系结构和设计来设计程序的绝妙介绍。

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

我希望这个帮助能祝你好运!


3

已经有很多好的答案。我仍然尝试一下。
调用时,函数会分配一个内存空间,该内存空间堆叠在调用方函数的内存空间上。在此存储空间中,该函数保留传递给它的参数,变量及其值。该存储空间随着函数的结束return调用而消失。随着堆栈概念的发展,调用者函数的存储空间现在变为活动状态。

对于递归调用,同一函数将多个存储空间相互堆叠。就这样。关于堆栈如何在计算机内存中工作的简单想法应该使您了解实现中递归如何发生的想法。


3

我知道有点偏离主题,但是...尝试在Google中查找递归 ...您会通过示例看到它的含义:-)


Google的早期版本返回以下文本(从内存中引用):

递归

参见递归

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

递归

您的意思是:递归


有关其他答复,请参见此答案


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

和瞧!


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

某些人可能会觉得此功能更令人困惑,因为我们是从“远端”开始的,但是练习会使它感觉自然(这是另一种很好的“思考”技术:在解决递归时尝试“两面”)。再一次,该函数反映了人类(最多?)的行为:取所有左整数的总和,然后加上“下一个”右整数。


2

我当时很难理解递归,然后我找到了这个博客,并且已经看到了这个问题,所以我认为我必须分享一下。您必须阅读此博客,我发现它对堆栈的解释非常有帮助,甚至逐步解释了两个递归如何与堆栈一起工作。我建议您首先了解堆栈的工作原理,这在这里可以很好地解释:堆栈之旅

then now you will understand how recursion works now take a look of this post逐步了解递归

在此处输入图片说明

它是一个程序:

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)

在此处输入图片说明 在此处输入图片说明


2

当我停止阅读别人对它的评论或将其视为我可以避免的事情而只是编写代码时,递归对我开始有意义。我发现解决方案有问题,并试图不看就重复解决方案。我只有无奈地陷入困境时才看解决​​方案。然后我回去尝试复制它。我对多个问题再次进行了此操作,直到我对如何确定递归问题和解决它有了自己的理解和认识。当我达到这个水平时,我开始提出问题并加以解决。那帮助了我更多。有时,只有通过自己尝试并努力奋斗才能学到东西。直到您“得到它”。


0

让我以斐波那契数列的例子告诉你,斐波那契是

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

如果n = 0,则为1

所以让我们看看如何递归的作品,我只是替换nt(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(nk)+ ... +(nk-3)+(nk-2)+(nk-1)+ n;

我们知道,如果t(0)=(n-k)等于1n-k=0使n=k我们替换kn

t(n)= t(nn)+ ... +(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²)

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

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.