了解递归函数的工作原理
Posted
技术标签:
【中文标题】了解递归函数的工作原理【英文标题】:Understanding how recursive functions work 【发布时间】:2014-10-29 21:08:29 【问题描述】:正如标题所解释的那样,我有一个非常基本的编程问题,我还没有解决。过滤掉所有的(非常聪明的)“为了理解递归,你必须先理解递归”。来自各种在线主题的回复我仍然不太明白。
了解当面对不知道我们不知道的事情时,我们可能倾向于提出错误的问题或错误地提出正确的问题我将分享我“认为”我的问题是希望有相似观点的人可以分享一些有助于为我打开递归灯泡的知识!
这是函数(语法是用 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 个挂断:
递归调用该函数,直到满足条件。该条件是 a > b。当满足这个条件时,返回0。乍一看,我认为返回值是0,这显然是不正确的。
在每次迭代中打印出 '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
,我将永远欠你的债。
【问题讨论】:
递归是那些用数学术语比用代码更容易理解的编程概念之一;有一个很好的定义hereLearnYouARecursion
,来自世界级教授的完整问题集!
我只是有敦促您在 Google 搜索框中输入“递归”。其中一个复活节彩蛋。我不会破坏你的惊喜。
***.com/questions/25676961/…的可能重复
What is recursion and when should I use it?的可能重复
【参考方案1】:
我认为这种混淆源于将其视为“同一个函数”被多次调用。如果您将其视为“调用同一函数的许多副本”,那么它可能会更清楚:
只有一个函数的副本会返回 0,而且它不是第一个(它是最后一个)。所以调用第一个的结果不是0。
对于第二点混淆,我认为用英语拼出递归会更容易。阅读这一行:
return a + sumInts(a + 1, b: b)
as "返回'a'的值加上(函数的另一个副本的返回值,也就是'a'的副本的值加上(函数的另一个副本的返回值,也就是第二个副本的值) 'a' 的值加上 (...",函数的每个副本都会产生一个自身的新副本,其 a 增加 1,直到满足 a > b 条件。
当你达到 a > b 条件为真时,你有一个(可能是任意的)长堆栈的函数副本都在运行中,都在等待下一个副本的结果找到找出他们应该添加到“a”的内容。
(编辑:另外,需要注意的是,我提到的函数的副本堆栈是一个真实的东西,它会占用实际内存,如果它变得太大会使你的程序崩溃。编译器可以优化它在某些情况下,耗尽堆栈空间是许多语言中递归函数的一个重要且不幸的限制)
【讨论】:
Catfish_Man:我认为你成功了!将其视为同一功能的多个“副本”是完全有道理的。我还在纠结它,但我认为你让我走上了正确的道路!感谢您在忙碌的一天中抽出时间来帮助一位程序员!我会将您的答案标记为正确答案。祝你有美好的一天! 这是一个很好的类比——尽管小心不要把它看得太字面意思,因为每个“副本”实际上都是完全相同的代码。每个副本的不同之处在于它正在处理的所有数据。 我不太乐意将其视为副本。我发现更直观的解释是区分函数本身(代码,它的作用)和与堆栈帧/执行上下文相关联的函数调用(该函数的实例化)。该函数不拥有它的局部变量,它们在函数被调用(调用)时被实例化。但我想这将作为递归的介绍 正确的术语是函数有多个调用。每个调用都有自己的实例变量a
和b
。
是的,可以在此答案中添加大量精确度。我故意省略了“函数实例”和“单个函数调用的激活记录”之间的区别,因为它是额外的概念负载,并不能真正帮助理解问题。它有助于理解其他问题,所以它仍然是有用的信息,只是在别处。这些 cmets 似乎是一个适合它的好地方 :)【参考方案2】:
递归是一个难以理解的话题,我认为我不能在这里完全做到这一点。相反,我将尝试专注于您在此处拥有的特定代码,并尝试描述解决方案为何有效的直觉以及代码如何计算其结果的机制。
您在此处给出的代码解决了以下问题:您想知道从 a 到 b 的所有整数的总和,包括在内。对于您的示例,您需要从 2 到 5 的数字的总和,包括 2 到 5,即
2 + 3 + 4 + 5
当尝试递归解决问题时,第一步应该是弄清楚如何将问题分解为具有相同结构的较小问题。因此,假设您想将 2 到 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(含)之间的整数之和。接下来,将 a 添加到该总数中。你会注意到“计算 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,包括(这是对 sumInt
s 的递归调用),然后添加a
.
当然,就其本身而言,这种方法实际上是行不通的。例如,您将如何计算 5 到 5 之间的所有整数的总和?好吧,使用我们当前的逻辑,您将计算 6 到 5(含)之间的所有整数的总和,然后加 5。那么,您如何计算 6 到 5(含)之间的所有整数的总和?好吧,使用我们当前的逻辑,您将计算 7 和 5 之间的所有整数的总和,包括 7 和 5,然后加上 6。您会注意到这里有一个问题 - 这一直在继续!
在递归问题解决中,需要有一些方法来停止简化问题,而直接去解决它。通常,您会找到一个可以立即确定答案的简单案例,然后构建您的解决方案以在出现简单案例时直接解决它们。这通常称为基本情况或递归基础。
那么,这个特定问题的基本情况是什么?当你对从 a 到 b 的整数求和时,如果 a 恰好大于 b,那么答案是 0 - 范围内没有任何数字!因此,我们的解决方案结构如下:
-
如果 a > b,则答案为 0。
否则(a≤b),得到答案如下:
-
计算 a + 1 和 b 之间的整数之和。
添加一个以获得答案。
现在,将此伪代码与您的实际代码进行比较:
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)
时会发生什么?好吧,这就是发生的事情:
-
您致电
sumInts(5, 5)
。我们进入 else
分支,它返回 `a + sumInts(6, 5) 的值。
为了让sumInts(5, 5)
确定sumInts(6, 5)
是什么,我们需要暂停正在执行的操作并调用sumInts(6, 5)
。
sumInts(6, 5)
被调用。它进入if
分支并返回0
。但是,sumInts
的这个实例是由sumInts(5, 5)
调用的,因此返回值会传回给sumInts(5, 5)
,而不是***调用者。
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 for
sumInts(6, 5), namely 0. It then returns
5 + 0 = 5`。
sumInts(4, 5)
现在有一个 sumInts(5, 5)
的值,即 5。然后它返回 4 + 5 = 9
。
换句话说,返回的值是通过一次对一个值求和形成的,每次取一个特定递归调用返回的值 sumInts
并加上 a
的当前值。当递归触底时,最深的调用返回 0。但是,该值不会立即退出递归调用链;相反,它只是将值交还给其上一层的递归调用。这样,每个递归调用只是增加了一个数字并将其返回到链中更高的位置,最终得到总和。作为练习,请尝试查找 sumInts(2, 5)
,这是您想要开始的。
希望这会有所帮助!
【讨论】:
感谢您在百忙之中抽出时间来分享如此全面的答案!这里有很多很棒的信息可以帮助我了解递归函数,并且肯定会帮助其他人在未来偶然发现这篇文章。再次感谢,祝您有美好的一天!【参考方案3】:要理解递归,您必须以不同的方式思考问题。不是一个整体上有意义的大逻辑步骤序列,而是把一个大问题分解成更小的问题并解决这些问题,一旦你对子问题有了答案,你就可以结合子问题的结果来制作解决更大的问题。想想你和你的朋友需要数一个大桶里弹珠的数量。你们每个人都拿一个较小的桶,然后单独计算,当你完成后,你把总数加在一起。现在如果你们每个人都找到一些朋友并进一步拆分桶,那么你只需要等待这些其他朋友算出他们的总数,把它带回给你们每个人,你把它加起来。等等。特殊情况是,当您只计算 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
【讨论】:
说得好,罗伯。您以一种非常清晰易懂的方式表达了它。感谢您抽出宝贵时间! 这是对正在发生的事情的最清晰的表示,没有深入探讨它的理论和技术细节,而是清楚地显示了执行的每个步骤。 我很高兴。 :) 解释这些事情并不总是那么容易。谢谢你的夸奖。 +1。这就是我将如何描述它,特别是您的最后一个结构示例。直观地展开正在发生的事情很有帮助。【参考方案4】:我会试一试的。
执行方程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】: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 但这不是最终值,因为计算机仍必须将 5 加到该 0 上,然后将 4 加到结果中,然后是 3,然后是 2,如所述通过我们计算机思想的最后四句话。请注意,在递归中,计算机不仅要计算递归调用,还必须记住如何处理递归调用返回的值。计算机内存中有一个称为堆栈的特殊区域,用于保存此类信息,该空间是有限的,并且过于递归的函数可能会耗尽堆栈:这就是 堆栈溢出 为我们最喜爱的网站命名。
您的陈述似乎隐含假设计算机在执行递归调用时忘记了它的状态,但事实并非如此,这就是您的结论与您的观察不符的原因。
2. 在每次迭代中打印出 'a' 的值会产生一个我期望的值:2、3、4、5(此时 5+1 > b 满足第一个条件:a > b)但是我还是没看出来14这个值是怎么实现的。
这是因为返回值不是a
本身,而是a
的值与递归调用返回的值之和。
【讨论】:
感谢您抽出宝贵的时间来撰写这个出色的答案,迈克尔! +1! @JasonElwood 如果您修改sumInts
以便它真正记下“计算机思想”,这可能会有所帮助。一旦你编写了这些函数的手,你可能已经“明白了”!
这是一个很好的答案,尽管我注意到没有要求函数激活发生在称为“堆栈”的数据结构上。递归可以通过延续传递风格来实现,在这种情况下根本没有堆栈。堆栈只是一个——特别有效,因此被普遍使用——对延续概念的具体化。
@EricLippert 虽然用于实现递归的技术是一个有趣的话题本身,但我不确定它是否对 OP 有用——他们想了解“它是如何实现的”作品”——接触各种使用的机制。虽然延续传递风格或基于扩展的语言(例如 TeX 和 m4)本质上并不比更常见的编程范式更难,但我不会通过标记这些“异国情调”和像“它”这样的小 善意 来冒犯任何人总是发生在 the 堆栈上”应该有助于 OP 理解这个概念。 (而且总是涉及一种堆栈。)
必须有某种方式让软件记住它在做什么,递归调用函数,然后在它返回时返回到原来的状态。这种机制的作用类似于栈,因此称它为栈很方便,即使使用了其他一些数据结构。【参考方案6】:
我通常通过查看基本情况并向后工作来弄清楚递归函数是如何工作的。这是应用于此功能的技术。
首先是基本情况:
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)
请注意,我们通过按照调用返回的顺序跟踪调用来得出关于函数如何运行的结论。
【讨论】:
【参考方案7】:到目前为止,您在这里已经得到了一些很好的答案,但我会再添加一个采取不同策略的答案。
首先,我写了很多关于简单递归算法的文章,你可能会觉得有趣;见
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)
,我们将调用文本替换为函数体,然后将a
替换为2
,将b
替换为5
:
s(2, 5)
---> 2 > 5 ? 0 : 2 + s(2 + 1, 5)
现在评估条件。我们将2 > 5
文本替换为false
。
---> false ? 0 : 2 + s(2 + 1, 5)
现在将所有假条件句替换为备选句,并将所有真条件句替换为结果。我们只有错误的条件,所以我们以文本方式用替代表达式替换该表达式:
---> 2 + s(2 + 1, 5)
现在,为了省去我输入所有+
符号的麻烦,用它的值文本替换常量算术。 (这有点作弊,但我不想跟踪所有的括号!)
---> 2 + s(3, 5)
现在进行搜索和替换,这次是调用正文,3
用于 a
,5
用于 b。我们会将调用的替换项放在括号中:
---> 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
可替换的步骤,然后我们可以只应用算术规则。
【讨论】:
很好的例子,但是当你继续进行更复杂的计算时,它会让你心碎。例如。在二叉树中找到共同的祖先。【参考方案8】:递归。在计算机科学中,递归在有限自动机主题下进行了深入介绍。
最简单的形式是自引用。例如,说“我的车是车”是一个递归语句。问题是该语句是一个无限递归,因为它永远不会结束。 “汽车”的声明中的定义是它是“汽车”,因此它可以被替换。但是,没有尽头,因为在替换的情况下,仍然是“我的车是车”。
如果声明是“我的车是宾利。我的车是蓝色的”,情况可能会有所不同。在这种情况下,在第二种情况下替换汽车可能是“宾利”,从而导致“我的宾利是蓝色的”。这些类型的替换在计算机科学中通过Context-Free Grammars 进行了数学解释。
实际的替换是一个产生式规则。假设语句由 S 表示,并且 car 是一个可以是“宾利”的变量,这个语句可以递归重构。
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 背景的程序员来说可能有点陌生。【参考方案9】:我认为理解递归函数的最佳方式是意识到它们是用来处理递归数据结构的。但是在您的原始函数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 = 继任者(继任者(零)),依此类推。
一旦你有了一个递归数据结构,你就有了函数的模板。对于每个非递归情况,您可以直接计算该值。对于递归案例您假设递归函数已经在工作并使用它来计算案例,但解构参数。在 Natural 的情况下,这意味着我们将使用n
代替Succesor(n)
,或者等效地,我们将使用n - 1
代替n
。
// 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。
递归的情况呢?如果我们想添加以a
开头的n
数字并且我们已经有一个适用于n-1
的有效sumInts
函数?好吧,我们需要添加a
,然后用a + 1
调用sumInts
,所以我们以:
// 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)
好消息是现在您不需要考虑低级别的递归了。您只需要验证:
对于递归数据的基本情况,它在不使用递归的情况下计算答案。 对于递归数据的递归情况,它使用对解构数据的递归计算答案。【讨论】:
【参考方案10】:您可能对 Nisan 和 Schocken 的 implementation of functions 感兴趣。链接的 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
256
和 257
是内存地址。
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
ing 之前,您会注意到以下行:
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
。
【讨论】:
【参考方案11】:已经有很多好的答案了。我还在尝试。 调用时,函数会获得分配的 memory-space,它堆叠在调用函数的 memory-space 上。在这个内存空间中,函数保存传递给它的参数、变量及其值。这个内存空间随着函数的结束返回调用而消失。随着堆栈的发展,调用函数的内存空间现在变得活跃了。
对于递归调用,同一个函数将多个内存空间堆叠在一起。就这样。 stack 如何在计算机的内存中工作的简单概念应该让您了解递归在实现中是如何发生的。
【讨论】:
【参考方案12】:有点离题,我知道,但是...尝试在 Google 中查找 recursion...您将通过示例了解它的含义 :-)
早期版本的 Google 返回以下文本(凭记忆引用):
递归
见递归
2014 年 9 月 10 日,更新了关于递归的笑话:
递归
你的意思是:递归
如需其他回复,请参阅this answer。
【讨论】:
【参考方案13】:我在学习和真正理解递归时遇到的一个非常好的技巧是花一些时间学习一种除了递归之外没有任何形式的循环构造的语言。这样你就可以通过练习对如何使用递归有一个很好的感觉。
我关注了http://www.htdp.org/,它不仅是一个 Scheme 教程,还很好地介绍了如何从架构和设计方面设计程序。
但基本上,您需要投入一些时间。如果没有对递归的“牢固”掌握,某些算法(例如回溯)对您来说总是显得“困难”甚至“神奇”。所以,坚持。 :-D
我希望这会有所帮助,祝你好运!
【讨论】:
【参考方案14】:将递归视为多个克隆做同样的事情......
你要求克隆[1]:“sum numbers between 2 and 5”
+ clone[1] it knows that: result is 2 + "sum numbers between 3 and 5". so it asks to clone[2] to return: "sum numbers between 3 and 5"
| + clone[2] it knows that: result is 3 + "sum numbers between 4 and 5". so it asks to clone[3] to return: "sum numbers between 4 and 5"
| | + clone[3] it knows that: result is 4 + "sum numbers between 5 and 5". so it asks to clone[4] to return: "sum numbers between 5 and 5"
| | | + clone[4] it knows that: result is 5 + "sum numbers between 6 and 5". so it asks to clone[5] to return: "sum numbers between 6 and 5"
| | | | clone[5] it knows that: it can't sum, because 6 is larger than 5. so he returns 0 as result.
| | | + clone[4] it gets the result from clone[5] (=0) and sums: 5 + 0, returning 5
| | + clone[3] it gets the result from clone[4] (=5) and sums: 4 + 5, returning 9
| + clone[2] it gets the result from clone[3] (=9) and sums: 3 + 9, returning 12
+ clone[1] it gets the result from clone[2] (=12) and sums: 2 + 12, returning 14
然后瞧!!
【讨论】:
yep。 :) 复制通常也是understanding function calls 的关键(每次调用都使用相同的函数配方以及新的新调用框架/沙盒)。【参考方案15】:上面的很多答案都很好。不过,解决递归的一个有用技术是首先说明我们想要做什么,然后像人类一样编写代码来解决它。在上面的例子中,我们想要对一个连续的整数序列求和(使用上面的数字):
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
有些人可能会觉得这个函数更令人困惑,因为我们从“远端”开始,但练习可以让它感觉自然(这是另一种很好的“思考”技巧:解决递归时尝试“双方”)。再一次,该函数反映了人类(大多数?)所做的事情:获取所有左侧整数的总和并添加“下一个”右侧整数。
【讨论】:
【参考方案16】:我很难理解递归,然后我找到了this blog,我已经看到了这个问题,所以我想我必须分享一下。你必须阅读这个博客,我发现这非常有用,它用堆栈解释,甚至它解释了两个递归如何一步一步地与堆栈一起工作。我建议您首先了解堆栈的工作原理,它在这里解释得很好:journey-to-the-stack
then now you will understand how recursion works now take a look of this post
: Understand recursion step by step
这是一个程序:
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)
【讨论】:
【参考方案17】:当我不再阅读其他人对它的看法或不再将其视为我可以避免的事情而只是编写代码时,递归开始对我有意义。我发现了一个解决方案的问题,并试图在不查看的情况下复制该解决方案。当我无助地卡住时,我才查看解决方案。然后我回去尝试复制它。我在多个问题上再次这样做,直到我对如何识别递归问题并解决它有了自己的理解和认识。当我达到这个水平时,我开始编造问题并解决它们。这对我帮助更大。有时候,只有自己尝试和努力才能学到东西;直到你“明白”为止。
【讨论】:
【参考方案18】:我用斐波那契数列的例子告诉你,斐波那契是
t(n) = t(n - 1) + n;
如果 n = 0 那么 1
让我们看看递归是如何工作的,我只是将t(n)
中的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²)
这是理解递归关系的最佳方式
【讨论】:
以上是关于了解递归函数的工作原理的主要内容,如果未能解决你的问题,请参考以下文章