it-swarm.cn

用简单的英语来说,递归是什么?

递归的想法在现实世界中不是很普遍。因此,对于新手程序员来说似乎有些困惑。虽然,我猜他们逐渐适应了这个概念。那么,对于他们来说,如何轻松地理解这个想法又是一个很好的解释呢?

76
Gulshan

从同一函数内调用一个函数。

59
ChaosPandion

递归是一个自我调用的函数。

了解如何使用它,何时使用它以及如何避免不良设计非常重要,这需要您亲自尝试并了解会发生什么。

您需要知道的最重要的事情是要非常小心,以免出现永远不会结束的循环。 来自pramodc84的回答 您的问题有此错误:它永远不会结束...
递归函数必须始终检查条件以确定是否应再次调用自身。

使用递归的最经典示例是使用没有深度限制的树。这是您必须使用递归的任务。

27
awe

递归编程是逐步减少问题以更轻松地解决自身问题的过程。

每个递归函数都倾向于:

  1. 列出要处理的列表,其他结构或问题域
  2. 处理当前点/步骤
  3. 在其余/子域上调用自己
  4. 合并或使用子域工作的结果

如果步骤2在3之前,并且步骤4是微不足道的(串联,求和或不执行),则启用 尾递归 。步骤2通常必须在步骤3之后进行,因为可能需要问题子域的结果才能完成当前步骤。

遍历直截了当的二叉树。遍历可以按需要,按顺序或按顺序进行。

   B
A     C

预购:B A C

traverse(tree):
    visit the node
    traverse(left)
    traverse(right)

顺序:A B C

traverse(tree):
    traverse(left)
    visit the node
    traverse(right)

后置订单:A C B

traverse(tree):
    traverse(left)
    traverse(right)
    visit the node

很多递归问题是 map 操作或 fold 的特定情况-仅了解这两个操作可以导致对递归的良好用例的大量了解。

21
Orbling

OP表示在现实世界中不存在递归,但我希望有所不同。

让我们以现实世界中切比萨饼的“操作”为例。您已将比萨饼从烤箱中取出,然后将其切成两半,然后将其切成两半,然后再将其切成两半,以供食用。

一遍又一遍地切割比萨饼直到获得所需结果(切片数)的操作。为了争辩,让我们说未切割的比萨饼本身就是一片。

这是Ruby中的示例:

 def cut_pizza(existing_slices,desired_slices)
如果exist_slices!= desired_slices 
#我们还没有足够的切片供所有人食用,所以
#我们切比萨饼片,从而使其数量加倍.____。]#我们具有所需的切片数量,因此我们在此处返回
#而不是继续递归
返回现存的[slices] 
 end 
 end 
 
 pizza = 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

在我看来,这仅仅是编程机制,是一种巧妙地消除重复的方式。您可以使用变量来重写它,但这是一个“更精细”的解决方案。没有什么神秘的或困难的。您将编写几个递归函数,它将单击并在编程工具框中单击huzzah

额外信用如果您要求它提供不是2的幂(即2或4或8的幂)的切片,则上面的cut_pizza示例将给您一个堆栈级别太深的错误或16)。您可以修改它,以便如果有人要10片它不会永远运行吗?

20
Ryan Allen

好的,我将尝试保持此简单明了。

递归函数是调用自己的函数。递归函数由三部分组成:

  1. 逻辑
  2. 呼唤自己
  3. 何时终止。

编写递归方法的最佳方法是将尝试编写的方法作为一个简单示例,仅处理要迭代的过程的一个循环,然后将调用添加到方法本身,并在需要时添加终止。最好的学习方法就是像万物一样练习。

由于这是程序员的网站,因此我将避免编写代码,但这是一个很好的 link

如果您开了个玩笑,那么您就会了解递归的含义。

16
dustyprogrammer

递归是程序员可以用来对自身进行函数调用的工具。斐波那契数列是如何使用递归的教科书示例。

大多数递归代码(如果不是全部的话)都可以表示为迭代函数,但是它通常很杂乱。其他递归程序的好例子是数据结构,例如树,二进制搜索树,甚至是快速排序。

递归用于减少代码的草率,请记住,它通常较慢并且需要更多的内存。

6
Bryan Harrington

我喜欢用这个:

您如何步行到商店?

如果您在商店的入口,只需经过它即可。否则,请迈出第一步,然后步行至商店。

包括三个方面至关重要:

  • 琐碎的基本案例
  • 解决一小部分问题
  • 递归解决其余问题

实际上,我们在日常生活中经常使用递归;我们只是不这么想。

5
Barry Brown

我要指出的最好的例子是K&R的C编程语言。在那本书(我从内存中引用)中,递归索引页中的条目(单独)列出了他们谈论递归的实际页,索引页。

3
Kanini

Josh K已经提到过 Matroshka 玩偶。假设您想学习只有最短的娃娃才知道的东西。问题是您不能真正与她直接交谈,因为她原本是活着 inside 高个娃娃,第一个照片放在她的左边。这种结构是这样的(一个娃娃生活在更高的娃娃中),直到最后只有最高的娃娃才结束。

因此,您唯一可以做的就是向最高的玩偶提问。最高的娃娃(谁不知道答案)将需要将您的问题传递给较短的娃娃(第一张照片在她的右边)。由于她也没有答案,因此她需要问下一个较短的娃娃。这样,直到消息到达最短的玩偶为止。最矮的娃娃(谁是唯一知道秘密答案的人)将答案传递到下一个更高的娃娃(在她的左边找到),然后将答案传递给下一个更高的娃娃……这将一直持续到答案到达它的最终目的地,那是最高的娃娃,最后...你:)

这就是递归的真正作用。函数/方法会自行调用,直到获得期望的答案。这就是为什么在编写递归代码时,决定递归何时终止非常重要的原因。

不是最好的解释,但希望能有所帮助。

2
sakisk

递归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);
}

优点:

  • 自然表达-对于许多类型的算法,这是表达函数的非常自然的方式。
  • 降低的LOC-递归定义函数通常更为简洁。
  • 速度-在某些情况下,取决于语言和计算机体系结构,算法的递归比等效的迭代解决方案要快,这通常是因为在硬件级别进行函数调用比迭代循环所需的操作和内存访问要快。
  • 可分性-许多递归算法具有“分而治之”的思想;运算的结果是对输入的两半分别执行相同运算的结果的函数。这使您可以在每个级别将工作分成两部分,并且如果可用,可以将另一半分配给另一个“执行单元”进行处理。使用迭代算法通常更难或更不可能。

缺点:

  • 需要理解-您只需“掌握”递归的概念即可了解正在发生的事情,从而编写和维护有效的递归算法。否则,它看起来就像黑魔法。
  • 上下文相关-递归是否是一个好主意,取决于可以就其本身定义算法的优雅程度。虽然可以构建例如递归的SelectionSort,但是迭代算法通常更容易理解。
  • 交易RAM调用堆栈的访问权限-通常,函数调用比缓存访问便宜),这可以使递归比迭代快。但是,调用堆栈的深度通常受到限制,这可能导致递归到迭代算法将起作用的错误。
  • 无限递归-您必须知道何时停止。无限迭代也是可能的,但是所涉及的循环结构通常更易于理解和调试。
2
KeithS

我使用的示例是我在现实生活中遇到的一个问题。您有一个容器(例如打算旅行的大背包),想知道总重量。您在容器中有两个或三个松散的物品,以及一些其他容器(例如,麻袋)。整个容器的重量显然是空容器的重量加上其中所有物品的重量。对于松散的物品,您可以称重它们;对于杂物袋,您可以对其进行称重,或者您可以说:“每个袋子的重量就是空容器的重量加上其中所有物品的重量”。然后,您继续将容器放入容器中,依此类推,直到到达容器中只有零散物品的地步。那是递归。

您可能会认为这在现实生活中从来没有发生过,但可以想象一下,试图计算或计算特定公司或部门中人员的薪金,这些人员或混合人员只为公司工作,部门中的人员,然后是有部门的部门等等。或在拥有地区的国家/地区进行销售,其中一些地区具有次地区等。此类问题在企业中始终存在。

1
Kate Gregory

递归可用于解决许多计数问题。例如,假设您在一个聚会上有一组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

0
Mitch Schwartz

这是递归的真实示例。

让他们想象他们有一个漫画集,您将把它们混合成一大堆。小心-如果他们确实有收藏,当您提起收藏的想法时,他们可能会立即杀死您。

现在,让他们借助本手册对大量未分类的漫画进行分类:

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.

这里的好处是:当涉及单个问题时,它们具有完整的“堆栈框架”,并且在地面上可以看到本地桩。给他们多份手册的打印输出,并在每个桩级上放置一个标记,并在其中标出您当前所在的位置(即局部变量的状态),以便您可以在每个“完成”上继续。

这就是递归的基本意义:执行相同的过程,只是在更精细的细节层次上,您投入的越多。

0
Secure

在生活中(相对于计算机程序而言),在我们的直接控制下很少发生递归,因为递归会令人困惑。同样,感知往往是关于副作用的,而不是功能上纯净的,因此,如果发生递归,您可能不会注意到它。

递归确实在世界上发生了。很多。

一个很好的例子是水循环(的简化版本):

  • 太阳加热湖
  • 水升入天空形成云
  • 乌云飘到山上
  • 在山上,空气变得太冷,无法保持水分
  • 下雨了
  • 一条河形成
  • 河里的水流进湖里

这是一个循环,使其自身再次发生。它是递归的。

可以递归的另一个地方是英语(通常是人类语言)。您可能一开始可能不认识它,但是我们生成句子的方式是递归的,因为规则允许我们将一个符号的一个实例嵌入到另一个相同符号的实例中。

摘自史蒂文·平克(Steven Pinker)的《语言本能》:

如果女孩吃冰淇淋或女孩吃糖果,则男孩吃热狗

这是一个包含其他完整句子的完整句子:

这个女孩吃冰淇淋

这个女孩吃糖果

这个男孩吃热狗

理解完整句子的行为涉及理解较小的句子,较小的句子使用相同的心理欺骗手段来理解为完整的句子。

要从编程角度了解递归,最简单的方法是看一看可以用递归解决的问题,并了解为什么应该递归,以及这意味着您需要做什么。

对于该示例,我将使用最大的通用除数函数,简称gcd。

您有两个数字ab。要找到它们的gcd(假设两者都不为0),您需要检查a是否可被__VARIABLE整除为b。如果是,则b是gcd,否则,您需要检查b的gcd和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循环或其他迭代构造的原因是,当您阅读代码时,它会告诉您它正在做什么以及接下来将要发生什么,因此更容易弄清楚它是否正常工作。

0
Matt Ellen