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

时间:2008-08-06 02:29:51

标签: recursion computer-science

在邮件列表和在线讨论中经常出现的一个主题是获得计算机科学学位的优点(或缺乏)。对于否定方似乎一次又一次出现的一个论点是,他们已经编码了几年而且他们从未使用过递归。

所以问题是:

  1. 什么是递归?
  2. 我什么时候使用递归?
  3. 为什么人们不使用递归?

40 个答案:

答案 0 :(得分:86)

在这个帖子中有recursion的一些很好的解释,这个答案是为什么你不应该在大多数语言中使用它。*在大多数主要的命令式语言实现中(即每个主要的实现C,C ++,Basic,Python,Ruby,Java和C#)iteration非常适合递归。

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

  1. 空间在the stack上为函数的参数和局部变量
  2. 分割出来
  3. 将函数的参数复制到此新空间
  4. 控制跳转到功能
  5. 函数的代码运行
  6. 将函数的结果复制到返回值
  7. 堆栈重绕到之前的位置
  8. 控制跳回到函数调用的位置
  9. 执行所有这些步骤需要花费时间,通常比迭代循环所花费的时间多一些。但是,真正的问题在于步骤#1。当许多程序启动时,它们为它们的堆栈分配一块内存,当它们耗尽该内存时(通常但不总是由于递归),程序因stack overflow而崩溃。

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

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

    * 我喜欢递归。 My favorite static language根本不使用循环,递归是重复执行某些操作的唯一方法。我只是不认为递归通常是一种不适合它的语言的好主意。

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

答案 1 :(得分:63)

递归的简单英文示例。

A child couldn't sleep, so her mother told her a story about a little frog,
    who couldn't sleep, so the frog's mother told her a story about a little bear,
         who couldn't sleep, so the bear's mother told her a story about a little weasel... 
            who fell asleep.
         ...and the little bear fell asleep;
    ...and the little frog fell asleep;
...and the child fell asleep.

答案 2 :(得分:49)

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

struct Node {
    Node* next;
};

并且您想知道链接列表可以使用递归执行此操作多长时间:

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

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

答案 3 :(得分:46)

每当一个函数调用自身,创建一个循环,那么就是递归。与任何事物一样,递归有很好的用途和不良用途。

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

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

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

所以进行递归而不是迭代的全部理由应该是利用call stack来做一些聪明的事情。例如,如果在同一循环内多次使用不同参数调用函数,那么这就是实现branching的一种方法。一个典型的例子是Sierpinski triangle

enter image description here

您可以使用递归简单地绘制其中一个,其中调用堆栈在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);
    }
}

如果您尝试使用迭代执行相同的操作,我认为您会发现需要更多代码才能完成。

其他常见用例可能包括遍历层次结构,例如网站抓取工具,目录比较等

<强>结论

实际上,无论何时需要迭代分支,递归都是最有意义的。

答案 4 :(得分:27)

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

规范示例是生成n阶段的例程。通过将1和n之间的所有数相乘来计算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 );
}

此函数的第一部分称为基本案例(或有时称为Guard子句),这是阻止算法永久运行的原因。只要调用函数值为1或更小,它就会返回值1。第二部分更有趣,被称为递归步骤。这里我们使用稍微修改的参数调用相同的方法(我们将其减1)然后将结果与我们的n副本相乘。

第一次遇到这种情况时会有点混乱,因此检查它在运行时的工作方式是有益的。想象一下,我们称之为FactRec(5)。我们输入例程,不会被基本情况拿起来,所以我们最终会这样:

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

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

如果我们使用参数4重新输入方法,我们再次不会被guard子句停止,所以我们最终在:

// 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))));

当基本案例被触发时,最终替换发生。在这一点上,我们有一个简单的算法公式来解决,它首先等同于Factorials的定义。

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

答案 5 :(得分:12)

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

public int Factorial(int n)
{
    if (n <= 1)
        return 1;

    return n * Factorial(n - 1);
}

答案 6 :(得分:9)

递归是指通过求解较小版本的问题然后使用该结果加上一些其他计算来解决原始问题的答案来解决问题的方法。通常情况下,在解决较小版本的过程中,该方法将解决问题的较小版本,依此类推,直到它达到一个无法解决的“基本案例”。

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

答案 7 :(得分:9)

考虑old, well known problem

  

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

gcd的定义非常简单:

gcd definition

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

在英语中,这个定义说任何数字的最大公约数和零是该数字,两个数字 m n 的最大公约数是最大的 n 的公约数和 m 除以 n 之后的余数。

如果您想了解其工作原理,请参阅Euclidean algorithm上的维基百科文章。

我们以gcd(10,8)为例进行计算。每一步都等于前一步:

  1. gcd(10,8)
  2. gcd(10,10 mod 8)
  3. gcd(8,2)
  4. gcd(8,8 mod 2)
  5. gcd(2,0)
  6. 2
  7. 在第一步中,8不等于零,因此定义的第二部分适用。 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中的其他算法!

答案 8 :(得分:8)

  1. 自称为
  2. 的函数
  3. 当一个函数可以(轻松地)分解为一个简单的操作,并在问题的一些较小部分加上相同的函数。我应该说,相反,这使它成为递归的理想选择。
  4. 他们这样做!
  5. 典型的例子是看起来像的阶乘:

    int fact(int a) 
    {
      if(a==1)
        return 1;
    
      return a*fact(a-1);
    }
    

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

答案 9 :(得分:6)

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

function cmdCheckAllClick {
    checkRecursively(TreeView1.RootNode);
}

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

因此,您可以看到checkRecursively首先检查传递的节点,然后为该节点的每个子节点调用自己。

你需要对递归有点小心。如果进入无限递归循环,您将获得Stack Overflow异常:)

我无法想到人们在适当的时候不应该使用它的原因。它在某些情况下很有用,而在其他情况下则不然。

我认为,因为这是一种有趣的技术,一些编码员可能最终会比他们应该更频繁地使用它,没有真正的理由。这在某些圈子中给递归起了一个坏名字。

答案 10 :(得分:5)

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

考虑递归首字母缩略词作为一个简单的例子:

  • GNU 代表 GNU不是Unix
  • PHP 代表 PHP:超文本预处理器
  • YAML 代表 YAML不是标记语言
  • 葡萄酒代表葡萄酒不是模拟器
  • VISA 代表 Visa国际服务协会

More examples on Wikipedia

答案 11 :(得分:4)

1)。 如果方法可以调用自身,则该方法是递归的;直接:

void f() {
   ... f() ... 
}

或间接:

void f() {
    ... g() ...
}

void g() {
   ... f() ...
}

2.。)何时使用递归

Q: Does using recursion usually make your code faster? 
A: No.
Q: Does using recursion usually use less memory? 
A: No.
Q: Then why use recursion? 
A: It sometimes makes your code much simpler!

3。)人们只有在编写迭代代码非常复杂时才使用递归。例如,像预订,后序的树遍历技术可以是迭代的和递归的。但通常我们使用递归是因为它很简单。

答案 12 :(得分:4)

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

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

  1. 如果集合为空,则集合中的项目计数为零(呃!)。
  2. 如果该集合不为空,则在删除一个项目后,计数为1加上集合中的项目数。
  3. 假设您有这样的集合:[x x x]。让我们计算一下有多少项。

    1. 该集合是[x x x],它不是空的,因此我们应用规则2.项目数是一加上[x x]中的项目数(即我们删除了一个项目)。
    2. 该集合为[x x],因此我们再次应用规则2:[x]中的一个+项目数。
    3. 该集合为[x],仍然匹配规则2:[]中的一个+项目数。
    4. 现在该集合是[],它与规则1匹配:计数为零!
    5. 现在我们知道了步骤4(0)的答案,我们可以解决第3步(1 + 0)
    6. 同样,既然我们知道了步骤3(1)中的答案,我们就可以解决第2步(1 + 1)
    7. 最后,现在我们知道了步骤2(2)中的答案,我们可以解决第1步(1 + 2)并获得[x x x]中的项目数,即3个。万岁!
    8. 我们可以将其表示为:

      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
      

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

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

      如果我们将上述内容转换为伪代码,我们得到:

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

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

答案 13 :(得分:4)

递归最适合我喜欢称之为“分形问题”,在那里你处理的是一个由较大版本的小东西组成的大东西,每个东西都是大东西的更小版本,并且等等。如果你必须遍历或搜索像树或嵌套的相同结构之类的东西,你就会遇到一个可能适合递归的问题。

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

  1. 大多数人(包括我自己)在程序或面向对象的编程上削减了编程,而不是函数式编程。对于这样的人来说,迭代方法(通常使用循环)感觉更自然。

  2. 我们这些在程序或面向对象编程上削减编程的人经常被告知要避免递归,因为它容易出错。

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

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

答案 14 :(得分:4)

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

我也喜欢Steve McConnells关于Code Complete中递归的讨论,他在那里批评了计算机科学关于递归的书中使用的例子。

  

不要将阶乘或Fibonacci数字的递归用作

     

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

我认为这是一个非常有趣的提升点,也可能是为什么递归经常被误解的原因。

编辑: 这不是对Dav的答案的挖掘 - 我发布这个时没有看到那个回复

答案 15 :(得分:4)

递归语句是指您将下一步作为输入和已完成内容的组合定义的过程。

例如,采用factorial:

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

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

希望这会有所帮助......

答案 16 :(得分:3)

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

答案 17 :(得分:3)

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

当人们提到“递归”时,他们通常会谈论他们所写的函数,这些函数在完成其工作之前会反复调用。遍历数据结构中的层次结构时,递归会很有用。

答案 18 :(得分:2)

递归函数是一个包含对自身调用的函数。递归结构是一个包含自身实例的结构。您可以将两者组合为递归类。递归项的关键部分是它包含自身的实例/调用。

考虑两个彼此面对的镜子。我们已经看到他们做出的整洁的无限效果。每个反射都是一个镜像的实例,它包含在镜像的另一个实例中,等等。包含自身反射的镜像是递归。

binary search tree是一个很好的递归编程示例。该结构是递归的,每个Node包含2个Node实例。在二叉搜索树上工作的函数也是递归的。

答案 19 :(得分:2)

这是一个老问题,但我想从后勤的角度添加一个答案(即不是从算法的正确性或性能的角度来看)。

我使用Java工作,Java不支持嵌套函数。因此,如果我想进行递归,我可能必须定义一个外部函数(这只是因为我的代码碰到Java的官僚规则而存在),或者我可能必须完全重构代码(我真的很讨厌这样做)。

因此,我经常避免递归,而是使用堆栈操作,因为递归本身实际上是一个堆栈操作。

答案 20 :(得分:2)

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

  1. 拿一个苹果
  2. 记下理货标记
  3. 数量计数标记
  4. 你桌子上有很多苹果,你想知道有多少苹果。

    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
    

    在完成之前重复同样的事情的过程称为递归。

    我希望这是你正在寻找的“普通英语”答案!

答案 21 :(得分:2)

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

答案 22 :(得分:1)

用简单的英语,递归意味着一次又一次地重复一些。

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

查看以下计算数字阶乘的示例:

public int fact(int n)
{
    if (n==0) return 1;
    else return n*fact(n-1)
}

答案 23 :(得分:1)

递归的最简单定义是“自引用”。一个引用自身的函数,i。即调用本身是递归的。要记住的最重要的事情是,递归函数必须具有“基本情况”,即。即如果为true则导致它不调用自身,从而终止递归的条件。否则你将获得无限递归:

recursion http://cart.kolix.de/wp-content/uploads/2009/12/infinite-recursion.jpg

答案 24 :(得分:1)

递归是指您有一个使用自身的操作。它可能会有一个停止点,否则会永远持续下去。

假设您想在字典中查找单词。您可以使用称为“查找”的操作。

你的朋友说:“我现在真的可以把一些布丁舀起来!”你不知道他的意思,所以你在字典中查找“勺子”,它的内容如下:

勺子:名词 - 最后一个带圆勺的器具。 勺子:动词 - 用勺子做某事 勺子:动词 - 从背后紧紧拥抱

现在,由于你真的不懂英语,这会指出你正确的方向,但你需要更多的信息。所以你选择“器具”和“拥抱”来查找更多信息。

拥抱:动词 - 依偎 器具:名词 - 一种工具,通常是一种食用器具

喂!你知道什么是依偎,它与布丁无关。你也知道布丁是你吃的东西,所以现在才有意义。你的朋友一定要用勺子吃布丁。

好吧,好吧,这是一个非常蹩脚的例子,但它说明了(可能很差)递归的两个主要部分。 1)它使用自己。在这个例子中,在你理解它之前,你并没有真正有意义地查找一个单词,这可能意味着要查找更多的单词。这将我们带到第二点, 2)它停在某个地方。它必须有某种基础案例。否则,你最终会查找字典中的每个单词,这可能不太有用。我们的基本情况是,您获得了足够的信息,可以在您之前做过和不理解之间建立联系。

给出的传统例子是阶乘,其中5阶乘是1 * 2 * 3 * 4 * 5(即120)。基本情况为0(或1,取决于)。因此,对于任何整数n,您可以执行以下操作

是n等于0?返回1 否则,返回n *(n-1的阶乘)

让我们以4的例子(我们事先知道的是1 * 2 * 3 * 4 = 24)这样做。

阶乘4 ...是0吗?不,所以它必须是4 *阶乘3 但是3的因子是什么?这是3 * 2的阶乘 阶乘2是2 *阶乘1 阶乘1是1 *阶乘0 我们知道0的阶乘! :-D它是1,这就是定义 阶乘1是1 *阶乘0,即1 ...因此1 * 1 = 1 阶乘2是2 *阶乘1,即1 ...所以2 * 1 = 2 阶乘3是3 *阶乘2,即2 ...所以3 * 2 = 6 因子4(最终!!)是4 *阶乘3,其中6 ... 4 * 6是24

因子是“基本情况,并使用自身”的简单案例。

现在,请注意我们仍在整体下降4的阶乘...如果我们想要100的阶乘,我们必须一直下降到0 ......这可能会有很多开销它。以同样的方式,如果我们在字典中找到一个不起眼的单词,可能需要查找其他单词并扫描上下文线索,直到找到我们熟悉的连接。递归方法可能需要很长时间才能完成。然而,当他们被正确使用和理解时,他们可以使复杂的工作变得非常简单。

答案 25 :(得分:1)

可以在两种类型中考虑很多问题:

  1. 基础案例,只需查看它们即可解决基本问题,
  2. 递归案例,从较小的部分(初级或其他)中构建更大的问题。
  3. 那么什么是递归函数?好吧,那就是你有一个直接或间接定义的功能。好吧,这听起来很荒谬,直到你意识到它对上述类型的问题是明智的:你直接解决基本情况并通过使用递归调用处理递归情况来解决嵌入其中的较小部分问题。

    你需要递归的真正经典例子(或闻起来非常类似的东西)就是你在处理一棵树时。树的叶子是基本情况,分支是递归的情况。 (在伪C中。)

    struct Tree {
        int leaf;
        Tree *leftBranch;
        Tree *rightBranch;
    };
    

    按顺序打印出来的最简单方法是使用递归:

    function printTreeInOrder(Tree *tree) {
        if (tree->leftBranch) {
            printTreeInOrder(tree->leftBranch);
        }
        print(tree->leaf);
        if (tree->rightBranch) {
            printTreeInOrder(tree->rightBranch);
        }
    }
    

    很容易看出它会起作用,因为它非常清晰。 (非递归等价物要复杂得多,需要内部的堆栈结构来管理要处理的事物列表。)好吧,假设当然没有人做过循环连接。

    在数学上,显示递归被驯服的技巧是专注于找到参数大小的度量。对于我们的树示例,最简单的度量标准是当前节点下面树的最大深度。在叶子,它是零。在一个只有叶子下面的分支,它是一个,等等。然后,您可以简单地显示在调用函数的参数大小上有严格有序的序列,以便处理树;递归调用的参数在度量意义上总是“小于”整个调用的参数。通过严格降低的基数指标,您可以进行排序。

    也可以进行无限递归。这种混乱,并且在许多语言中都不会起作用,因为堆栈会爆炸。 (它确实起作用,语言引擎必须确定函数以某种方式不返回并因此能够优化堆栈的保持。一般来说棘手的东西;尾递归只是最简单的方法。)

答案 26 :(得分:1)

递归是根据自身定义函数,集合或算法的技术。

例如

n! = n(n-1)(n-2)(n-3)...........*3*2*1

现在它可以递归地定义为: -

n! = n(n-1)!   for n>=1

在编程术语中,当函数或方法重复调用自身时,直到满足某个特定条件,此过程称为递归。但是必须有一个终止条件,函数或方法不能进入无限循环。

答案 27 :(得分:1)

计算中的递归是一种用于在单个函数(方法,过程或块)调用的正常返回之后计算结果或副作用的技术。

根据定义,递归函数必须能够直接或间接(通过其他函数)调用自身,具体取决于退出条件或未满足的条件。如果满足退出条件,则特定调用将返回其调用者。这将继续,直到返回初始调用,此时可以获得所需的结果或副作用。

例如,这是一个在Scala中执行Quicksort算法的函数(copied from the Wikipedia entry for Scala

def qsort: List[Int] => List[Int] = {
  case Nil => Nil
  case pivot :: tail =>
    val (smaller, rest) = tail.partition(_ < pivot)
    qsort(smaller) ::: pivot :: qsort(rest)
}

在这种情况下,退出条件是一个空列表。

答案 28 :(得分:1)

函数调用自身或使用自己的定义。

答案 29 :(得分:1)

您希望在有树结构时随时使用它。它在阅读XML时非常有用。

答案 30 :(得分:1)

如果基本上包含一个switch语句,并且每种情况都是数据类型的情况,那么任何算法都会对数据类型进行结构递归。

例如,当您处理类型

  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

太知名的因子函数是关于最简单的例子 这个表格。

答案 31 :(得分:1)

递归因为它适用于编程基本上是从它自己的定义(在它自己内部)中调用一个函数,具有不同的参数以完成任务。

答案 32 :(得分:1)

“如果我有一把锤子,那就让所有东西都像钉子一样。”

递归是解决巨大问题的解决问题的策略,每一步都是“将2件小东西变成一件更大的东西”,每次使用相同的锤子。

实施例

假设您的办公桌上布满了无用的1024张纸。如何使用递归从混乱中制作一张整洁,干净的纸叠?

  1. 分割:将所有纸张展开,因此每个“堆叠”中只有一张纸。
  2. 征服:
    1. 四处走动,将每张纸放在另一张纸上。你现在有2个堆栈。
    2. 四处走动,将每个2-stack放在另一个2-stack的顶部。你现在有4个堆栈。
    3. 四处走动,将每个4-stack放在另一个4-stack的顶部。你现在有8个堆栈。
    4. ... on and on ...
    5. 你现在拥有一大堆1024张纸!
  3. 请注意,这非常直观,除了计算所有内容(这不是绝对必要的)。实际上,你可能不会一直到单页堆栈,但你可以,它仍然可以工作。重要的部分是锤子:用你的手臂,你总是可以将一个堆叠放在另一个堆叠的顶部以形成更大的堆叠,并且(在合理范围内)任何一个堆叠都没有关系。

答案 33 :(得分:1)

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

例如,如果您想获取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;
}

答案 34 :(得分:1)

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

假设你有三位经理 - 杰克,约翰和摩根。 杰克管理着2名程序员,John - 3和Morgan - 5。 你将给每位经理300美元,并想知道它会花多少钱。 答案很明显 - 但如果摩根士丹利的两名员工也是经理人呢?

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

你永远都不会知道,在得到答案之前你会花多少时间,但你知道你有多少经理人,你可以花多少预算。

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

答案 35 :(得分:1)

递归是一个方法调用iself能够执行某个任务的过程。它减少了代码的冗余。大多数递归函数或方法必须有一个条件来打破回响调用,即如果条件满足则阻止它调用自身 - 这可以防止创建无限循环。并非所有函数都适合递归使用。

答案 36 :(得分:0)

实际上,对于阶乘的更好的递归解决方案应该是:

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

答案 37 :(得分:0)

马里奥,我不明白为什么你为这个例子使用了递归..为什么不简单地遍历每个条目?像这样:

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

上述方法会更快,更简单。不需要使用递归代替简单的循环。我认为这些种类的例子就是为什么递归得到了糟糕的说法。甚至规范的阶乘函数示例也可以通过循环更好地实现。

答案 38 :(得分:0)

我使用递归。这与获得CS学位有什么关系......(顺便说一下,我不这样做)

我发现的常见用途:

  1. 站点地图 - 从文档根目录开始递归文件系统
  2. 蜘蛛 - 浏览网站以查找电子邮件地址,链接等。

答案 39 :(得分:0)

我创建了一个递归函数来连接字符串列表和它们之间的分隔符。我主要使用它来创建SQL表达式,方法是将一个字段列表作为“ items ”和一个“逗号+空格”作为分隔符。这是函数(它使用一些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)