递归的想法在现实世界中不是很普遍。因此,对于新手程序员来说似乎有些困惑。虽然,我猜他们逐渐适应了这个概念。那么,对于他们来说,如何轻松地理解这个主意呢?
递归的想法在现实世界中不是很普遍。因此,对于新手程序员来说似乎有些困惑。虽然,我猜他们逐渐适应了这个概念。那么,对于他们来说,如何轻松地理解这个主意呢?
Answers:
为了解释递归,我结合了不同的解释,通常都尝试:
首先,Wolfram | Alpha用比Wikipedia更简单的术语来定义它:
通过重复特定的数学运算来生成每个项的表达式。
如果你的学生(或你解释过,从现在开始,我会说学生本人)至少有一些数学背景,他们显然已经通过一系列的学习和他们的观念遇到递归递归性及其递推关系。
然后,一个很好的开始方法是演示一个系列,并告诉您递归的含义很简单:
通常,您最多会得到一个“呵呵,什么”,因为他们仍然不使用它,或者更有可能只是打a。
对于其余部分,它实际上是我在回答的附录中针对您所指出的有关指针(坏双关)的问题的详细版本。
在这个阶段,我的学生通常知道如何在屏幕上打印内容。假设我们使用C,他们知道如何使用write
或打印单个字符printf
。他们还了解控制回路。
我通常会求助于一些重复的简单编程问题,直到他们明白为止:
阶乘
阶乘是一个非常简单的数学概念,易于理解,其实现非常接近其数学表示形式。但是,他们可能一开始无法理解。
字母
字母版本很有趣,可以教他们思考递归语句的顺序。像指针一样,它们只会向您随机抛出行。关键是使他们意识到可以通过修改条件或仅反转函数中语句的顺序来反转循环。那是打印字母有帮助的地方,因为对他们来说这是视觉效果。只需让他们编写一个函数,该函数将为每个调用打印一个字符,然后递归调用自身以编写下一个(或上一个)字符。
FP爱好者现在可以忽略将内容打印到输出流这一事实……让我们在FP前端不要太烦人。(但是,如果您使用的是具有列表支持的语言,则可以在每次迭代时随意将其连接到列表,并仅打印最终结果。但是通常我以C开头,但是不幸的是,对于此类问题和概念,这并不是最好的选择) 。
求幂
求幂问题稍微困难一些(在学习的这个阶段)。显然,该概念与阶乘完全相同,并且没有额外的复杂性……只是您有多个参数。通常这足以使人们感到困惑,一开始就把他们扔掉。
它的简单形式:
可以这样表示:
更难
一旦在教程中显示并重新实现了这些简单的问题,您就可以进行更困难(但非常经典)的练习:
注意:同样,其中一些确实没有什么困难...它们只是从完全相同的角度或稍有不同的角度解决问题。但是实践是完美的。
参考
一些阅读从来没有伤害。一开始会很好,他们会感到更加失落。这是一种生长在您身上的东西,它坐在您的脑后,直到有一天您意识到自己终于明白了。然后您回想起您阅读的这些内容。该递归, 递归在计算机科学和递推关系页面上维基百科会为现在要做的。
级别/深度
假设您的学生没有太多的编码经验,请提供代码存根。第一次尝试后,请给他们提供可以显示递归级别的打印功能。打印级别的数值会有所帮助。
堆叠即抽屉图
缩进打印结果(或关卡的输出)也有帮助,因为它可以直观地表示您的程序在做什么,可以打开和关闭抽屉式文件系统或文件系统浏览器中的堆栈上下文。
递归首字母缩写词
如果您的学生已经精通计算机文化,那么他们可能已经在使用名称带有递归首字母缩略词的某些项目/软件。一段时间以来一直是一种传统,特别是在GNU项目中。一些示例包括:
递归:
相互递归:
让他们尝试提出自己的想法。
同样,有很多递归幽默的出现,例如Google的递归搜索更正。有关递归的更多信息,请阅读此答案。
人们通常会遇到的一些问题,您需要知道一些答案。
为什么,天哪?
为什么要这么做?一个好的但非显而易见的原因是,用这种方式表达问题通常更简单。一个不太好但是很明显的原因是它通常需要较少的键入(不过,不要仅仅因为使用递归就让他们感到l ...)。
使用递归方法时,某些问题肯定更容易解决。通常,使用分而治之范例可以解决的任何问题都适合多分支递归算法。
又是什么?
为什么我的n
(或(不管您的变量名称是什么))每次都不同?初学者通常在理解变量和参数是什么以及程序中如何命名的东西n
可能具有不同的值时遇到问题。因此,如果此值在控制循环或递归中,那就更糟了!保持友善,不要在各处使用相同的变量名,并且要明确指出参数只是变量。
结束条件
如何确定我的最终状况?这很简单,只要让他们大声说出步骤即可。例如,阶乘从5开始,然后从4开始,然后从...直到0。
细节决定成败
不要和早期邻接说话,例如尾部呼叫优化。我知道,我知道TCO很好,但是起初他们不在乎。给他们一些时间,以对他们有用的方式来围绕这个过程。以后再随意破坏他们的世界,但请稍稍休息一下。
同样,从第一堂课开始就不要直接谈论调用堆栈及其内存消耗,以及... 堆栈溢出。我经常辅导学生私下谁告诉我,他们有50张幻灯片讲讲课一切有了解递归时,他们勉强可以在这个阶段正确地写一个循环。这是一个很好的例子,说明引用以后会有所帮助,但现在只会使您深深困惑。
但是,请在适当的时候明确指出有理由走迭代或递归路线。
相互递归
我们已经看到,函数可以是递归的,甚至它们可以具有多个调用点(8皇后,河内,斐波那契或什至扫雷器的探索算法)。但是相互递归调用呢?也从这里开始数学。f(x) = g(x) + h(x)
其中,g(x) = f(x) + l(x)
与h
和l
刚做的东西。
仅从数学序列开始,因为合同由表达式明确定义,因此更容易编写和实现。例如,霍夫施塔特女性和男性序列:
但是,就代码而言,要指出的是,相互递归解决方案的实现通常会导致代码重复,而应该简化为单个递归形式(请参阅Peter Norvig的解决每个数独难题。
static unsigned int vote = 1;
来自我。如果您愿意,请原谅静态幽默:)这是迄今为止最好的答案。
了解如何使用它,何时使用它以及如何避免不良设计非常重要,这需要您亲自尝试并了解会发生什么。
您需要知道的最重要的事情是要非常小心,以免出现永远不会结束的循环。从pramodc84到您的问题的答案有以下错误:永远不会结束...
递归函数必须始终检查条件以确定是否应再次调用自身。
使用递归的最经典示例是使用没有深度限制的树。这是您必须使用递归的任务。
a
仍会自行调用,只是间接调用(通过调用b
)。
递归编程是逐步减少问题以更容易解决其自身版本的过程。
每个递归函数都倾向于:
如果步骤2在3之前,并且步骤4是微不足道的(并置,求和或不执行任何操作),则启用尾递归。第2步通常必须在第3步之后进行,因为可能需要问题子域的结果才能完成当前步骤。
遍历直截了当的二叉树。遍历可以按需要,按顺序或按顺序进行。
B
A C
预购:BAC
traverse(tree):
visit the node
traverse(left)
traverse(right)
按顺序:ABC
traverse(tree):
traverse(left)
visit the node
traverse(right)
后期订购:ACB
traverse(tree):
traverse(left)
traverse(right)
visit the node
OP表示在现实世界中不存在递归,但我希望有所不同。
让我们以现实世界中切比萨饼的“操作”为例。您已将比萨饼从烤箱中取出,然后将其切成两半,然后将其切成两半,然后再将其切成两半,以供食用。
一遍又一遍地切割比萨饼直到获得所需结果(切片数)的操作。为了争辩,让我们说未切割的比萨饼本身就是一片。
这是Ruby中的示例:
def cut_pizza(existing_slices,desired_slices) 如果exist_slices =需要的_slices #我们没有足够的切片来养活所有人,所以 #我们正在切割披萨片,因此其数量增加了一倍 new_slices = existing_slices * 2 #这是递归调用 cut_pizza(new_slices,desired_slices) 其他 #我们拥有所需的切片数,因此我们返回 #在这里而不是继续递归 返回现有的切片 结束 结束 比萨= 1#整个比萨,“一片” cut_pizza(pizza,8)#=>我们将得到8
因此,现实世界中的操作是切比萨饼,递归一遍又一遍地做同样的事情,直到您拥有所需的东西为止。
您会发现可以使用递归函数实现的操作有:
我建议编写一个程序来根据文件名查找文件,并尝试编写一个调用自身的函数,直到找到为止,签名看起来像这样:
find_file_by_name(file_name_we_are_looking_for, path_to_look_in)
因此,您可以这样称呼它:
find_file_by_name('httpd.conf', '/etc') # damn it i can never find apache's conf
在我看来,这仅仅是编程机制,是一种巧妙地消除重复的方式。您可以使用变量来重写它,但这是一个“更精细”的解决方案。没有什么神秘的或困难的。您将编写几个递归函数,它将单击并在编程工具框中显示另一个机械技巧。
额外信用cut_pizza
如果您要求它提供不是2的幂的切片(即2或4或8或16),上面的示例将给您一个堆栈级别太深的错误。您可以修改它,以便如果有人要10片它不会永远运行吗?
好的,我将尝试保持此简单明了。
递归函数是调用自己的函数。递归函数由三部分组成:
编写递归方法的最佳方法是将尝试编写的方法作为一个简单示例,仅处理要迭代的过程的一个循环,然后将调用添加到方法本身,并在需要时添加终止。最好的学习方法就是像万物一样练习。
由于这是程序员的网站,因此我将避免编写代码,但这是一个很好的链接
如果您开了个玩笑,那么您就有了递归的含义。
递归是程序员可以用来对自身进行函数调用的工具。斐波那契数列是如何使用递归的教科书示例。
大多数递归代码(如果不是全部的话)都可以表示为迭代函数,但是它通常很杂乱。其他递归程序的好例子是数据结构,例如树,二进制搜索树,甚至是快速排序。
递归用于减少代码的草率,请记住,它通常较慢并且需要更多的内存。
我喜欢用这个:
如果您在商店的入口,只需经过它即可。否则,请迈出第一步,然后步行至商店。
包括三个方面至关重要:
实际上,我们在日常生活中经常使用递归;我们只是不这么想。
for
循环转换为无意义的递归函数。
Josh K已经提到过Matroshka娃娃。假设您想学习只有最短的娃娃才知道的东西。问题在于您不能真正直接与她交谈,因为她原本生活在一个较高的玩偶中,第一个图片位于她的左边。这种结构是这样的(一个娃娃生活在较高的娃娃中),直到最后只剩下最高的一个。
因此,您唯一可以做的就是向最高的玩偶提问。最高的娃娃(谁不知道答案)需要将您的问题传递给较短的娃娃(第一张照片在她的右边)。由于她也没有答案,因此她需要问下一个较短的娃娃。这样,直到消息到达最短的玩偶为止。最矮的娃娃(谁是唯一知道秘密答案的人)将答案传递到下一个更高的娃娃(在她的左边找到),然后将答案传递给下一个更高的娃娃……这将一直持续到答案到达它的最终目的地,那是最高的娃娃,最后...你:)
这就是递归的真正作用。函数/方法会自行调用,直到获得期望的答案。这就是为什么在编写递归代码时,决定递归终止时间非常重要的原因。
不是最好的解释,但希望能有所帮助。
递归 n。-一种算法设计模式,其中根据自身定义操作。
典型的例子是找到一个数的阶乘n!。0!= 1,对于任何其他自然数N,N的阶乘是所有小于或等于N的自然数的乘积。因此,6!= 6 * 5 * 4 * 3 * 2 * 1 =720。此基本定义将允许您创建一个简单的迭代解决方案:
int Fact(int degree)
{
int result = 1;
for(int i=degree; i>1; i--)
result *= i;
return result;
}
但是,请再次检查该操作。6!= 6 * 5 * 4 * 3 * 2 * 1。按照相同的定义,5!= 5 * 4 * 3 * 2 * 1,意味着我们可以说6!= 6 *(5!)。依次为5!= 5 *(4!),依此类推。通过这样做,我们将问题减少为对所有先前操作的结果执行的操作。最终,这将减少到一个称为基本案例的点,在该点上,定义是已知的。在我们的例子中,为0!= 1(在大多数情况下,我们也可以说1!= 1)。在计算中,通常允许我们以非常相似的方式定义算法,方法是调用方法本身并传递较小的输入,从而通过多次递归减少基本情况,从而减少了问题:
int Fact(int degree)
{
if(degree==0) return 1; //the base case; 0! = 1 by definition
else return degree * Fact(degree -1); //the recursive case; N! = N*(N-1)!
}
使用三元运算符,可以在许多语言中进一步简化此操作(有时在不提供该运算符的语言中,有时将其视为Iif函数):
int Fact(int degree)
{
//reads equivalently to the above, but is concise and often optimizable
return degree==0 ? 1: degree * Fact(degree -1);
}
好处:
缺点:
我使用的示例是我在现实生活中遇到的一个问题。您有一个容器(例如打算旅行的大背包),想知道总重量。容器中有两个或三个松散的物品,还有一些其他的容器(例如,东西麻袋。)整个容器的重量显然是空容器的重量加上其中所有物品的重量。对于松散的物品,您可以称重它们;对于杂物袋,您可以对其进行称重,或者您可以说:“每个袋子的重量就是空容器的重量加上其中所有物品的重量”。然后,您继续将容器放入容器中,依此类推,直到到达容器中只有零散物品的地步。那是递归。
您可能会认为这在现实生活中从来没有发生过,但可以想象一下,试图计算或计算特定公司或部门中人员的薪金,这些人员或混合人员只为公司工作,部门中的人员,然后是有部门的部门等等。或在有地区(其中有些地区有次地区等)的国家/地区进行的销售。此类问题在企业中始终存在。
递归可用于解决许多计数问题。例如,假设您在一个聚会上有一组n个人(n> 1),并且每个人都一次与其他人握手。进行了几次握手?您可能知道解为C(n,2)= n(n-1)/ 2,但是您可以按以下方式递归求解:
假设只有两个人。那么(通过检查)答案显然是1。
假设您有三个人。挑出一个人,并注意他/她与另外两个人握手。之后,您只需要数一下其他两个人之间的握手。刚才我们已经做过,就是1。所以答案是2 +1 = 3。
假设您有n个人。按照与以前相同的逻辑,它是(n-1)+(n-1个人之间的握手次数)。扩展得到(n-1)+(n-2)+ ... + 1。
表示为递归函数
f(2)= 1
f(n)= n-1 + f(n-1),n> 2
在生活中(相对于计算机程序而言),递归很少在我们的直接控制下发生,因为递归会造成混淆。同样,感知往往是关于副作用的,而不是功能上纯净的,因此,如果发生递归,您可能不会注意到它。
递归确实在世界上发生了。很多。
一个很好的例子是水循环(的简化版本):
这是一个循环,使其自身再次发生。它是递归的。
可以递归的另一个地方是英语(通常是人类语言)。您可能一开始可能不认识它,但是我们生成句子的方式是递归的,因为规则允许我们将一个符号实例嵌入到另一个相同符号实例中。
摘自史蒂文·平克(Steven Pinker)的《语言本能》:
如果女孩吃冰淇淋或女孩吃糖果,则男孩吃热狗
这是一个包含其他完整句子的完整句子:
这个女孩吃冰淇淋
这个女孩吃糖果
这个男孩吃热狗
理解完整句子的行为涉及理解较小的句子,较小的句子使用相同的心理欺骗手段来理解为完整的句子。
要从编程角度理解递归,最简单的方法是看一看可以用递归解决的问题,并了解为什么应该递归,以及这意味着您需要做什么。
对于该示例,我将使用最大的通用除数函数,简称gcd。
您有两个数字a
和b
。要找到它们的gcd(假设两者都不为0),您需要检查能否a
被整除b
。如果是,b
则为gcd,否则,您需要检查的gcd b
和的其余部分a/b
。
您已经可以看到这是一个递归函数,因为您有gcd函数调用gcd函数。只是为了敲打它,它在c#中(同样,假设0永远不会作为参数传递):
int gcd(int a, int b)
{
if (a % b == 0) //this is a stopping condition
{
return b;
}
return (gcd(b, a % b)); //the call to gcd here makes this function recursive
}
在程序中,有一个停止条件很重要,否则您的功能将永远重复出现,最终将导致堆栈溢出!
在这里使用递归而不是使用while循环或其他迭代构造的原因是,当您阅读代码时,它会告诉您它正在做什么以及接下来将要发生什么,因此更容易弄清楚它是否正常工作。
这是递归的真实示例。
让他们想象他们有一个漫画集,您将把它们混合成一大堆。小心-如果他们确实有收藏,当您提起收藏的想法时,他们可能会立即杀死您。
现在,让他们借助本手册对大量未分类的漫画进行分类:
Manual: How to sort a pile of comics
Check the pile if it is already sorted. If it is, then done.
As long as there are comics in the pile, put each one on another pile,
ordered from left to right in ascending order:
If your current pile contains different comics, pile them by comic.
If not and your current pile contains different years, pile them by year.
If not and your current pile contains different tenth digits, pile them
by this digit: Issue 1 to 9, 10 to 19, and so on.
If not then "pile" them by issue number.
Refer to the "Manual: How to sort a pile of comics" to separately sort each
of the new piles.
Collect the piles back to a big pile from left to right.
Done.
这里的好处是:当它们涉及单个问题时,它们具有完整的“堆栈框架”,并且在地面上都可以看到本地桩。给他们多份手册的打印输出,并在每个桩级上放置一个标记,并在该标记上放置您当前所在的位置(即局部变量的状态),以便您可以在每个“完成”上继续。
这就是递归的基本意义:执行相同的过程,只是越细致就越深入。
不是简单的英语,不是真正的现实例子,而是两种通过玩游戏来学习递归的方法:
递归的一个很好的解释实际上是“从自身内部发生的动作”。
考虑一个画家画墙,它是递归的,因为该动作是“从天花板到地板绘制一条带,而不是向右踩小踏板,然后(从天花板到地板绘制一条条纹,而不是向右向一点踩小踏板,(从天花板到地板的条形,而不是从小脚踏板向右移一点,然后(等)))”。
他的paint()函数一遍又一遍地调用自己,以构成更大的paint_wall()函数。
希望这位可怜的画家有某种停止状态:)