什么是递归,我应该什么时候使用它?

Posted

技术标签:

【中文标题】什么是递归,我应该什么时候使用它?【英文标题】:What is recursion and when should I use it? 【发布时间】:2010-09-05 09:43:35 【问题描述】:

邮件列表和在线讨论中似乎经常出现的主题之一是攻读计算机科学学位的优点(或不足)。反对方似乎一次又一次提出的一个论点是,他们已经编码了几年并且从未使用过递归。

所以问题是:

    什么是递归? 什么时候使用递归? 为什么人们不使用递归?

【问题讨论】:

也许这有帮助:***.com/questions/126756/… 这可能有助于理解这个概念:导航到此页面上问题的第二条评论中提供的链接,然后按照 cmets 所说的去做:***.com/questions/3021/… 【参考方案1】:
    调用自身的函数 当一个函数可以(轻松地)分解为一个简单的操作加上相同的函数处理问题的某个较小部分时。相反,我应该说,这使它成为递归的一个很好的候选者。 确实如此!

典型的例子是阶乘,如下所示:

int fact(int a) 

  if(a==1)
    return 1;

  return a*fact(a-1);

一般来说,递归不一定很快(函数调用开销往往很高,因为递归函数往往很小,见上文)并且可能会遇到一些问题(堆栈溢出任何人?)。有人说他们往往很难在非平凡的情况下得到“正确”,但我并不真正相信这一点。在某些情况下,递归是最有意义的,并且是编写特定函数的最优雅和最清晰的方式。应该注意的是,一些语言偏爱递归解决方案并对其进行了更多优化(想到 LISP)。

【讨论】:

【参考方案2】:

递归函数是调用自身的函数。我发现使用它的最常见原因是遍历树结构。例如,如果我有一个带有复选框的 TreeView(考虑安装一个新程序,“选择要安装的功能”页面),我可能想要一个“检查所有”按钮,它会是这样的(伪代码):

function cmdCheckAllClick 
    checkRecursively(TreeView1.RootNode);


function checkRecursively(Node n) 
    n.Checked = True;
    foreach ( n.Children as child ) 
        checkRecursively(child);
    

所以你可以看到 checkRecursively 首先检查它传递的节点,然后为该节点的每个子节点调用自身。

你确实需要小心递归。如果你进入一个无限递归循环,你会得到一个 Stack Overflow 异常:)

我想不出人们不应该在适当的时候使用它的理由。它在某些情况下很有用,而在其他情况下则没有。

我认为,由于这是一种有趣的技术,一些编码人员最终可能会比他们应该更频繁地使用它,而没有真正的理由。这让递归在某些圈子里名声不好。

【讨论】:

【参考方案3】:

递归是一种基于分而治之思维的解决问题的方法。 基本思想是你把原始问题分成更小的(更容易解决的)实例,解决那些更小的实例(通常再次使用相同的算法),然后将它们重新组合成最终的解决方案。

典型示例是生成 n 的阶乘的例程。 n 的阶乘是通过将 1 和 n 之间的所有数字相乘来计算的。 C# 中的迭代解决方案如下所示:

public int Fact(int n)

  int fact = 1;

  for( int i = 2; i <= n; i++)
  
    fact = fact * i;
  

  return fact;

迭代解决方案并不令人惊讶,任何熟悉 C# 的人都应该理解它。

通过识别第 n 个阶乘是 n * Fact(n-1) 来找到递归解决方案。或者换句话说,如果你知道一个特定的阶乘数是什么,你就可以计算下一个阶乘数。这是 C# 中的递归解决方案:

public int FactRec(int n)

  if( n < 2 )
  
    return 1;
  

  return n * FactRec( n - 1 );

这个函数的第一部分被称为Base Case(或者有时是Guard Clause),它可以防止算法永远运行。每当使用 1 或更小的值调用函数时,它只会返回值 1。第二部分更有趣,被称为递归步骤。在这里,我们调用相同的方法,参数稍作修改(我们将其减 1),然后将结果与我们的 n 副本相乘。

第一次遇到这种情况时可能会让人感到困惑,因此检查它在运行时的工作原理很有指导意义。假设我们调用 FactRec(5)。我们进入了例程,没有被基本情况所接受,所以我们最终是这样的:

// In FactRec(5)
return 5 * FactRec( 5 - 1 );

// which is
return 5 * FactRec(4);

如果我们重新输入带有参数 4 的方法,我们又不会被保护子句停止,所以我们最终会:

// In FactRec(4)
return 4 * FactRec(3);

如果我们把这个返回值代入上面的返回值,我们得到

// In FactRec(5)
return 5 * (4 * FactRec(3));

这应该会为您提供有关如何得出最终解决方案的线索,以便我们快速跟踪并展示下降过程中的每一步:

return 5 * (4 * FactRec(3));
return 5 * (4 * (3 * FactRec(2)));
return 5 * (4 * (3 * (2 * FactRec(1))));
return 5 * (4 * (3 * (2 * (1))));

当基本情况被触发时,会发生最终的替换。在这一点上,我们有一个简单的代数公式来求解,它首先直接等同于阶乘的定义。

请注意,对方法的每次调用都会导致触发基本情况或调用参数更接近基本情况的同一方法(通常称为递归调用)。如果不是这种情况,那么该方法将永远运行。

【讨论】:

很好的解释,但我认为重要的是要注意这只是尾递归,与迭代解决方案相比没有任何优势。它的代码量大致相同,并且由于函数调用开销而运行速度较慢。 @SteveWortham:这不是尾递归。在递归步骤中,FactRec() 的结果必须乘以n 才能返回。【参考方案4】:

我创建了一个递归函数来连接一个字符串列表,它们之间有一个分隔符。我主要使用它来创建 SQL 表达式,通过将字段列表作为 'items' 和 'comma+space' 作为分隔符传递。这是函数(它使用一些 Borland Builder 原生数据类型,但可以适应任何其他环境):

String ArrangeString(TStringList* items, int position, String separator)

  String result;

  result = items->Strings[position];

  if (position <= items->Count)
    result += separator + ArrangeString(items, position + 1, separator);

  return result;

我这样称呼它:

String columnsList;
columnsList = ArrangeString(columns, 0, ", ");

假设您有一个名为 'fields' 的数组,其中包含以下数据:'albumName', 'releaseDate', 'labelId'。然后调用函数:

ArrangeString(fields, 0, ", ");

随着函数开始工作,变量'result'接收到数组位置0的值,即'albumName'。

然后它检查它正在处​​理的位置是否是最后一个。如果不是,那么它将结果与分隔符和函数的结果连接起来,哦,天哪,这就是同一个函数。但是这一次,检查一下,它称自己为位置加1。

ArrangeString(fields, 1, ", ");

它不断重复,创建一个 LIFO 堆,直到它到达正在处理的位置是最后一个位置的点,因此该函数仅返回列表中该位置上的项目,不再连接。然后将堆向后连接。

明白了吗?如果你不这样做,我有另一种方式来解释它。 :o)

【讨论】:

【参考方案5】:

递归最适用于我喜欢称之为“分形问题”的问题,在这种情况下,您正在处理由该大事物的较小版本组成的大事物,每个事物都是大事物的更小版本,并且很快。如果您必须遍历或搜索诸如树或嵌套的相同结构之类的东西,您就会遇到一个可能适合递归的问题。

人们避免递归的原因有很多:

    与函数式编程相比,大多数人(包括我自己)都在过程式或面向对象编程方面磨练了自己的编程能力。对于这些人来说,迭代方法(通常使用循环)感觉更自然。

    经常被告知要避免递归,因为它容易出错。

    我们经常被告知递归很慢。重复调用和从例程返回涉及大量的堆栈推送和弹出,这比循环慢。我认为有些语言比其他语言处理得更好,而且这些语言很可能不是那些主导范式是过程或面向对象的语言。

    对于我使用过的至少几种编程语言,我记得听过一些建议,如果递归超过了一定深度,就不要使用它,因为它的堆栈不是那么深。

【讨论】:

【参考方案6】:

这是一个简单的例子:一个集合中有多少个元素。 (有更好的方法来计算事物,但这是一个很好的简单递归示例。)

首先,我们需要两条规则:

    如果集合为空,则集合中的项目数为零(呵呵!)。 如果集合不为空,则计数为一加移除一项后集合中的项目数。

假设你有一个这样的集合:[x x x]。让我们数一数有多少物品。

    集合是非空的 [x x x],因此我们应用规则 2。项目数是 [x x] 中项目数的 1 加(即我们删除了一个项目)。 集合是 [x x],因此我们再次应用规则 2:1 + [x] 中的项目数。 集合为 [x],仍符合规则 2:一 + [] 中的项目数。 现在集合是 [],它符合规则 1:计数为零! 既然我们知道了步骤 4 (0) 中的答案,我们就可以解决步骤 3 (1 + 0) 同样,既然我们知道了步骤 3 (1) 中的答案,我们就可以解决步骤 2 (1 + 1) 现在我们终于知道了第 2 步 (2) 中的答案,我们可以求解第 1 步 (1 + 2) 并获得 [x x x] 中的项目数,即 3。万岁!

我们可以将其表示为:

count of [x x x] = 1 + count of [x x]
                 = 1 + (1 + count of [x])
                 = 1 + (1 + (1 + count of []))
                 = 1 + (1 + (1 + 0)))
                 = 1 + (1 + (1))
                 = 1 + (2)
                 = 3

在应用递归解决方案时,通常至少有 2 条规则:

基础,简单的案例说明当您“用完”所有数据时会发生什么。这通常是“如果您没有要处理的数据,您的答案是 X”的一些变体 递归规则,它说明如果您仍有数据会发生什么。这通常是一种规则,即“做一些事情来缩小你的数据集,然后将你的规则重新应用于更小的数据集。”

如果我们将上面的内容翻译成伪代码,我们会得到:

numberOfItems(set)
    if set is empty
        return 0
    else
        remove 1 item from set
        return 1 + numberOfItems(set)

我相信其他人会介绍更多有用的示例(例如遍历一棵树)。

【讨论】:

【参考方案7】:

我使用递归。这与拥有 CS 学位有什么关系……(顺便说一句,我没有)

我发现的常见用法:

    站点地图 - 从文档根目录开始递归遍历文件系统 spiders - 爬取网站以查找电子邮件地址、链接等。 ?

【讨论】:

【参考方案8】:

Mario,我不明白您为什么在该示例中使用递归。为什么不简单地遍历每个条目?像这样的:

String ArrangeString(TStringList* items, String separator)

    String result = items->Strings[0];

    for (int position=1; position < items->count; position++) 
        result += separator + items->Strings[position];
    

    return result;

上述方法会更快,更简单。无需使用递归来代替简单的循环。我认为这些例子就是为什么递归会受到不好的批评。即使是典型的阶乘函数示例也可以通过循环更好地实现。

【讨论】:

【参考方案9】:

对已解决的问题进行递归:什么都不做,你就完成了。 递归一个未解决的问题:执行下一步,然后递归其余的。

【讨论】:

【参考方案10】:

在这个线程中有很多关于recursion 的很好的解释,这个答案是关于为什么你不应该在大多数语言中使用它。* 在大多数主要的命令式语言实现中(即 C 的每个主要实现, C++、Basic、Python、Ruby、Java 和 C#)iteration 比递归更可取。

要了解原因,请浏览上述语言用于调用函数的步骤:

    the stack 上为函数的参数和局部变量留出了空间 函数的参数被复制到这个新空间中 控制跳转到函数 函数的代码运行 函数的结果被复制到返回值中 堆栈被倒回到之前的位置 控制跳回到调用函数的地方

完成所有这些步骤需要时间,通常比遍历循环所花费的时间多一点。然而,真正的问题在于步骤#1。当许多程序启动时,它们会为其堆栈分配一块内存,当它们用完该内存时(通常但不总是由于递归),程序会因stack overflow 而崩溃。

因此,在这些语言中,递归速度较慢,并且容易崩溃。尽管如此,仍然有一些使用它的论据。一般来说,一旦您知道如何阅读,递归编写的代码会更短且更优雅。

语言实现者可以使用一种称为tail call optimization 的技术,它可以消除某些类别的堆栈溢出。简而言之:如果一个函数的返回表达式只是一个函数调用的结果,那么您不需要在堆栈上添加新的级别,您可以将当前的级别重用于被调用的函数。遗憾的是,很少有命令式语言实现内置了尾调用优化。

* 我喜欢递归。 My favorite static language 根本不使用循环,递归是重复做某事的唯一方法。我只是不认为在没有针对它进行调整的语言中,递归通常不是一个好主意。

** 顺便说一下,Mario,您的 ArrangeString 函数的典型名称是“join”,如果您选择的语言还没有实现它,我会感到惊讶。

【讨论】:

很高兴看到对递归固有开销的解释。我在回答中也提到了这一点。但对我来说,递归的最大优势在于调用堆栈可以做什么。您可以编写一个简洁的递归算法,该算法重复分支,使您能够轻松地执行诸如爬取层次结构(父/子关系)之类的事情。例如,请参阅我的答案。 非常失望地找到标题为“什么是递归以及何时应该使用它?”的问题的最佳答案。不是actually 回答其中任何一个,不要介意对递归的极端偏见警告,尽管它在您提到的大多数语言中广泛使用(您所说的并没有什么特别错误,但您似乎在夸大问题并低估了有用性)。 你可能是对的@Dukeling。对于上下文,当我写这个答案时,已经写了很多关于递归的很好的解释,我写这篇文章的目的是作为该信息的补充,而不是最佳答案。在实践中,当我需要遍历树或处理任何其他嵌套数据结构时,我通常会求助于递归,而且我还没有遇到我自己在野外制造的堆栈溢出。【参考方案11】:

只要你有一个树形结构,你就想使用它。在读取 XML 时非常有用。

【讨论】:

【参考方案12】:

其实阶乘更好的递归解应该是:

int factorial_accumulate(int n, int accum) 
    return (n < 2 ? accum : factorial_accumulate(n - 1, n * accum));


int factorial(int n) 
    return factorial_accumulate(n, 1);

因为这个版本是Tail Recursive

【讨论】:

【参考方案13】:

在最基本的计算机科学意义上,递归是一个调用自身的函数。假设你有一个链表结构:

struct Node 
    Node* next;
;

你想知道一个链表有多长,你可以用递归来做到这一点:

int length(const Node* list) 
    if (!list->next) 
        return 1;
     else 
        return 1 + length(list->next);
    

(这当然也可以通过 for 循环来完成,但作为概念的说明很有用)

【讨论】:

@Christopher:这是一个不错的简单递归示例。具体来说,这是尾递归的一个例子。然而,正如 Andreas 所说,它可以很容易地用 for 循环重写(更有效)。正如我在回答中解释的那样,递归有更好的用途。 这里真的需要 else 语句吗? 不,只是为了清楚起见。 @SteveWortham:这不是书面的尾递归; length(list-&gt;next) 仍然需要返回到 length(list) 以便后者可以将结果加 1。如果它是为了传递长度而编写的,只有这样我们才能忘记调用者的存在。喜欢int length(const Node* list, int count=0) return (!list) ? count : length(list-&gt;next, count + 1); 【参考方案14】:

应用于编程的递归基本上是从它自己的定义(在它自己内部)中调用一个函数,使用不同的参数来完成一项任务。

【讨论】:

【参考方案15】:

递归是指通过解决问题的较小版本然后使用该结果加上一些其他计算来制定原始问题的答案来解决问题的方法。很多时候,在解决较小版本的过程中,该方法会解决问题的较小版本,依此类推,直到达到一个容易解决的“基本情况”。

例如,要计算数字X 的阶乘,可以将其表示为X times the factorial of X-1。因此,该方法“递归”以找到X-1 的阶乘,然后将得到的任何值乘以X 以给出最终答案。当然,要找到X-1 的阶乘,首先要计算X-2 的阶乘,以此类推。基本情况是X 为 0 或 1,在这种情况下,它知道返回 1,因为0! = 1! = 1

【讨论】:

我认为您所指的不是递归,而是 en.wikipedia.org/wiki/… and Conquer 算法设计原则。例如查看 不,我不是指 D&C。 D&C 意味着存在 2 个或更多子问题,递归本身不存在(例如,这里给出的阶乘示例不是 D&C - 它是完全线性的)。 D&C 本质上是递归的一个子集。 引自您链接的确切文章:“分而治之的算法通过递归将问题分解为两个或更多子问题相同(或相关)类型,” 我不认为这是一个很好的解释,因为严格来说递归根本不需要解决问题。你可以打电话给自己(然后溢出)。 我在我为 php Master 写的一篇文章中使用了你的解释,尽管我不能把它归功于你。希望你不要介意。【参考方案16】:

嗯,这是一个相当不错的定义。***也有很好的定义。所以我会为你添加另一个(可能更糟糕的)定义。

当人们提到“递归”时,他们通常指的是他们编写的一个函数,该函数反复调用自身,直到完成它的工作。遍历数据结构中的层次结构时,递归会很有帮助。

【讨论】:

【参考方案17】:

我喜欢这个定义: 在递归中,例程自己解决问题的一小部分,将问题分成更小的部分,然后调用自身来解决每个更小的部分。

我也喜欢 Steve McConnells 在 Code Complete 中对递归的讨论,他批评了计算机科学书籍中关于递归的示例。

不要对阶乘或斐波那契数使用递归

一个问题 计算机科学教科书是这样的 他们提出了一些愚蠢的例子 递归。典型的例子是 计算阶乘或计算 斐波那契数列。递归是一个 强大的工具,它真的很愚蠢 在任何一种情况下使用它。如果一个 为我工作的程序员使用 递归计算阶乘,我会 雇用其他人。

我认为这是一个非常有趣的观点,并且可能是递归经常被误解的原因。

编辑: 这不是对 Dav 的回答的挖掘 - 我发布此内容时没有看到该回复

【讨论】:

使用阶乘或斐波那契数列作为示例的大部分原因是因为它们是以递归方式定义的常见项目,因此它们自然适用于计算它们的递归示例 - 即使从 CS 的角度来看这实际上并不是最好的方法。 我同意——我只是在阅读这本书时发现,在递归部分的中间提出一个有趣的观点【参考方案18】:

递归是通过调用自身的函数来解决问题。一个很好的例子是阶乘函数。阶乘是一个数学问题,其中 5 的阶乘是 5 * 4 * 3 * 2 * 1。这个函数在 C# 中解决了这个正整数问题(未测试 - 可能存在错误)。

public int Factorial(int n)

    if (n <= 1)
        return 1;

    return n * Factorial(n - 1);

【讨论】:

【参考方案19】:

“如果我有一把锤子,让一切都像钉子。”

递归是一种解决巨大问题的策略,每一步都只是“把两件小事变成一件大事”,每次都用同一个锤子。

示例

假设您的办公桌上堆满了 1024 份杂乱无章的文件。如何使用递归从混乱中制作出整齐、干净的一摞文件?

    分开:将所有纸摊开,这样每个“堆叠”中只有一张纸。 征服:
      四处走动,将每张纸放在另一张纸上。您现在有 2 个堆叠。 四处走动,将每个 2 叠放在另一个 2 叠之上。您现在有 4 个堆叠。 四处走动,将每个 4 叠放在另一个 4 叠之上。您现在有 8 个堆栈。 ... 不断... 您现在有一大叠 1024 张纸!

请注意,这非常直观,除了计算所有内容(这不是绝对必要的)。实际上,您可能不会一直到 1 页堆栈,但您可以并且它仍然可以工作。重要的部分是锤子:用你的手臂,你总是可以将一个堆栈放在另一个之上以形成更大的堆栈,并且(在合理范围内)任何一个堆栈有多大都无关紧要。

【讨论】:

您描述的是分而治之。虽然这是递归的示例,但绝不是唯一的。 没关系。我不想在这里用一句话来捕捉[递归的世界][1]。我想要一个直观的解释。 [1]:facebook.com/pages/Recursion-Fairy/269711978049【参考方案20】:

递归是直接或间接引用自身的表达式。

以递归首字母缩略词为例:

GNU 代表 GNU's Not Unix PHP 代表 PHP:超文本预处理器 YAML 代表 YAML 不是标记语言 WINE 代表 Wine 不是模拟器 VISA代表签证国际服务协会

More examples on Wikipedia

【讨论】:

【参考方案21】:

这是一种无限期地反复做事的方法,以便使用每个选项。

例如,如果您想获取 html 页面上的所有链接,您将需要递归,因为当您获取第 1 页上的所有链接时,您将希望获取第一个找到的每个链接上的所有链接页。然后对于指向新页面的每个链接,您将需要这些链接等等......换句话说,它是一个从自身内部调用自身的函数。

当您这样做时,您需要一种方法来知道何时停止,否则您将处于无限循环中,因此您向函数添加一个整数参数以跟踪循环数。

在c#中你会得到这样的东西:

private void findlinks(string URL, int reccursiveCycleNumb)    
   if (reccursiveCycleNumb == 0)
        
            return;
        

        //recursive action here
        foreach (LinkItem i in LinkFinder.Find(URL))
        
            //see what links are being caught...
            lblResults.Text += i.Href + "<BR>";

            findlinks(i.Href, reccursiveCycleNumb - 1);
        

        reccursiveCycleNumb -= reccursiveCycleNumb;

【讨论】:

【参考方案22】:

递归是方法调用自身以执行特定任务的过程。它减少了代码的冗余。大多数递归函数或方法必须有一个条件来中断递归调用,即在满足条件时阻止它调用自身 - 这可以防止创建无限循环。并非所有函数都适合递归使用。

【讨论】:

【参考方案23】:

在简单的英语中,递归意味着一次又一次地重复某事。

在编程中,一个例子是在自身内部调用函数。

看下面的计算阶乘的例子:

public int fact(int n)

    if (n==0) return 1;
    else return n*fact(n-1)

【讨论】:

用简单的英语来说,一次又一次地重复某件事称为迭代。【参考方案24】:

嘿,对不起,如果我的意见同意某人,我只是想用简单的英语解释递归。

假设您有三位经理 - Jack、John 和 Morgan。 Jack 管理 2 名程序员,John - 3 和 Morgan - 5。 您将给每位经理 300 美元,并想知道要花多少钱。 答案是显而易见的——但如果 Morgan 的 2 名员工也是经理怎么办?

递归来了。 您从层次结构的顶部开始。夏季费用为 0 美元。 你从杰克开始, 然后检查他是否有任何经理作为员工。如果您发现其中任何一个,请检查他们是否有任何经理作为员工等等。每次找到经理时,夏季费用都会增加 300 美元。 当你和杰克谈完后,去找约翰、他的员工,然后去找摩根。

你永远不会知道,在得到答案之前你会经历多少周期,尽管你知道你有多少经理以及你可以花费多少预算。

递归是一棵树,有枝有叶,分别称为父母和孩子。 当您使用递归算法时,您或多或少有意识地根据数据构建一棵树。

【讨论】:

【参考方案25】:

简单的英语: 假设你可以做 3 件事:

    拿一个苹果 记下计数标记 计数标记

你面前的桌子上有很多苹果,你想知道有多少苹果。

start
  Is the table empty?
  yes: Count the tally marks and cheer like it's your birthday!
  no:  Take 1 apple and put it aside
       Write down a tally mark
       goto start

重复同一件事直到完成的过程称为递归。

我希望这是您正在寻找的“简单的英语”答案!

【讨论】:

等等,我面前的桌子上有很多计数标记,现在我想知道有多少计数标记。我可以用苹果做这个吗? 如果你从地上捡起一个苹果(当你在这个过程中把它们放在那里的时候),每次你从列表中划出一个标记,直到没有标记为止,把它放在桌子上左,我很确定你最终得到的苹果数量等于你的计数标记数量。现在只需数一下那些苹果就可以立竿见影! (注意:这个过程不再是递归,而是无限循环)【参考方案26】:

递归语句是一种您将下一步做什么的过程定义为输入和您已经完成的组合的语句。

例如,取阶乘:

factorial(6) = 6*5*4*3*2*1

但很容易看出阶乘(6)也是:

6 * factorial(5) = 6*(5*4*3*2*1).

一般来说:

factorial(n) = n*factorial(n-1)

当然,递归的棘手之处在于,如果您想根据已经完成的内容来定义事物,则需要从某个地方开始。

在这个例子中,我们只是通过定义 factorial(1) = 1 来做一个特殊情况。

现在我们从下往上看:

factorial(6) = 6*factorial(5)
                   = 6*5*factorial(4)
                   = 6*5*4*factorial(3) = 6*5*4*3*factorial(2) = 6*5*4*3*2*factorial(1) = 6*5*4*3*2*1

由于我们定义了 factorial(1) = 1,我们到达了“底部”。

一般来说,递归过程有两个部分:

1) 递归部分,它根据新输入定义一些过程,并结合您通过相同过程“已经完成”的内容。 (即factorial(n) = n*factorial(n-1)

2) 一个基础部分,通过给它一些开始的地方来确保该过程不会永远重复(即factorial(1) = 1

一开始你可能会有点困惑,但只要看看一堆例子,它们就应该融合在一起。如果您想更深入地理解这个概念,请学习数学归纳法。另外,请注意,某些语言针对递归调用进行了优化,而其他语言则没有。如果您不小心,很容易制作出异常缓慢的递归函数,但也有一些技术可以让它们在大多数情况下高效运行。

希望这会有所帮助...

【讨论】:

【参考方案27】:

每当一个函数调用自己,创建一个循环,那就是递归。与任何事物一样,递归也有好的用途和坏的用途。

最简单的例子是尾递归,函数的最后一行是对自身的调用:

int FloorByTen(int num)

    if (num % 10 == 0)
        return num;
    else
        return FloorByTen(num-1);

然而,这是一个蹩脚的、几乎毫无意义的例子,因为它很容易被更有效的迭代所取代。毕竟,递归会受到函数调用开销的影响,在上面的示例中,与函数内部的操作相比,这可能是相当大的。

所以进行递归而不是迭代的全部原因应该是利用call stack 做一些聪明的事情。例如,如果您在同一个循环中多次调用具有不同参数的函数,那么这是完成branching 的一种方式。一个经典的例子是Sierpinski triangle。

您可以使用递归非常简单地绘制其中一个,其中调用堆栈在 3 个方向上分支:

private void BuildVertices(double x, double y, double len)

    if (len > 0.002)
    
        mesh.Positions.Add(new Point3D(x, y + len, -len));
        mesh.Positions.Add(new Point3D(x - len, y - len, -len));
        mesh.Positions.Add(new Point3D(x + len, y - len, -len));
        len *= 0.5;
        BuildVertices(x, y + len, len);
        BuildVertices(x - len, y - len, len);
        BuildVertices(x + len, y - len, len);
    

如果您尝试用迭代做同样的事情,我想您会发现需要更多的代码才能完成。

其他常见用例可能包括遍历层次结构,例如网站爬虫、目录比对等

结论

实际上,当您需要迭代分支时,递归最有意义。

【讨论】:

【参考方案28】:

一个例子:楼梯的递归定义是: 楼梯包括: - 一个步骤和一个楼梯(递归) - 或者只有一个步骤(终止)

【讨论】:

【参考方案29】:

如果基本上由一个 switch 语句组成,并且每个数据类型的 case 都有一个 case,那么任何算法都表现出对数据类型的 结构递归。

例如,当您处理类型时

  tree = null 
       | leaf(value:integer) 
       | node(left: tree, right:tree)

结构递归算法将具有以下形式

 function computeSomething(x : tree) =
   if x is null: base case
   if x is leaf: do something with x.value
   if x is node: do something with x.left,
                 do something with x.right,
                 combine the results

这确实是编写任何适用于数据结构的算法的最明显方式。

现在,当您查看使用 Peano 公理定义的整数(嗯,自然数)时

 integer = 0 | succ(integer)

你看到整数的结构递归算法看起来像这样

 function computeSomething(x : integer) =
   if x is 0 : base case
   if x is succ(prev) : do something with prev

众所周知的阶乘函数是关于 这个表格。

【讨论】:

【参考方案30】:

考虑old, well known problem:

在数学中,两个或多个非零整数的最大公约数 (gcd) ... 是除以无余数的最大正整数。

gcd 的定义出奇的简单:

其中 mod 是modulo operator(即整数除法后的余数)。

在英文中,这个定义是说任意数的最大公约数和零是那个数,两个数mn的最大公约数是最大的n 的公约数和 m 除以 n 后的余数。

如果您想知道为什么会这样,请参阅Euclidean algorithm 上的***文章。

我们以 gcd(10, 8) 为例。每一步都等于它之前的一步:

    gcd(10, 8) gcd(10, 10 mod 8) gcd(8, 2) gcd(8, 8 mod 2) gcd(2, 0) 2

在第一步中,8 不等于 0,因此适用定义的第二部分。 10 mod 8 = 2 因为 8 进入 10 一次,余数为 2。在步骤 3,第二部分再次应用,但这次 8 mod 2 = 0,因为 2 除以 8,没有余数。在第 5 步,第二个参数是 0,所以答案是 2。

您是否注意到 gcd 出现在等号的左侧和右侧?数学家会说这个定义是递归的,因为您在其定义中定义了recurs 的表达式。

递归定义往往很优雅。例如,列表和的递归定义是

sum l =
    if empty(l)
        return 0
    else
        return head(l) + sum(tail(l))

其中head 是列表中的第一个元素,tail 是列表的其余部分。请注意,sum 在最后的定义中重复出现。

也许您更喜欢列表中的最大值:

max l =
    if empty(l)
        error
    elsif length(l) = 1
        return head(l)
    else
        tailmax = max(tail(l))
        if head(l) > tailmax
            return head(l)
        else
            return tailmax

您可以递归地定义非负整数的乘法以将其转换为一系列加法:

a * b =
    if b = 0
        return 0
    else
        return a + (a * (b - 1))

如果关于将乘法转换为一系列加法的那一点没有意义,请尝试扩展几个简单的示例以了解它是如何工作的。

Merge sort 有一个可爱的递归定义:

sort(l) =
    if empty(l) or length(l) = 1
        return l
    else
        (left,right) = split l
        return merge(sort(left), sort(right))

如果您知道要查找什么,递归定义无处不在。注意所有这些定义都有非常简单的基本情况,例如,gcd(m, 0) = m。递归案例减少了问题,得到了简单的答案。

有了这个理解,你现在可以欣赏Wikipedia's article on recursion中的其他算法了!

【讨论】:

以上是关于什么是递归,我应该什么时候使用它?的主要内容,如果未能解决你的问题,请参考以下文章

什么是空间索引,我应该什么时候使用它?

什么是模拟,什么时候应该使用它?

什么是 MvcHtmlString,我应该什么时候使用它?

什么时候应该在 Material-UI 中使用排版?

C++:使用递归反转字符串

什么时候应该使用 TCP_NODELAY,什么时候应该使用 TCP_CORK?