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

Recursion 什么是递归?我应该在什么时候使用它?,recursion,computer-science,Recursion,Computer Science,邮件列表和在线讨论中经常出现的话题之一是攻读计算机科学学位的优点(或不足)。对于消极的一方来说,一个似乎反复出现的论点是,他们已经编码了好几年了,而且从来没有使用过递归 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,

邮件列表和在线讨论中经常出现的话题之一是攻读计算机科学学位的优点(或不足)。对于消极的一方来说,一个似乎反复出现的论点是,他们已经编码了好几年了,而且从来没有使用过递归

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.
因此,问题是:

  • 什么是递归
  • 什么时候使用递归
  • 为什么人们不使用递归
  • 调用自身的函数
  • 当一个函数可以(容易地)分解成一个简单的操作,再加上问题中较小部分的相同函数时。相反,我应该说,这使它成为递归的一个很好的候选者
  • 是的 典型的例子是阶乘,它看起来像:

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

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

    递归函数是一个调用自身的函数。我发现使用它的最常见原因是遍历树结构。例如,如果我有一个带有复选框的树状视图(比如安装一个新程序,“选择要安装的功能”页面),我可能需要一个“全选”按钮,它类似于以下内容(伪代码):

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

    您确实需要对递归小心一点。如果进入无限递归循环,将出现堆栈溢出异常:)

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


    我认为,因为这是一种有趣的技术,一些编码人员可能会在没有真正理由的情况下更多地使用它。这给递归在某些圈子里带来了坏名声。

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

    规范示例是一个生成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;
    }
    
    // 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))));
    
    当基本情况被触发时,将发生最终替换。在这一点上,我们有一个简单的algrebraic公式要求解,它首先等同于阶乘的定义


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

    递归最适合于我喜欢称之为“分形问题”的东西,在这里,你处理的是一个大东西,它由大东西的更小版本组成,每个版本都是大东西的更小版本,依此类推。如果必须遍历或搜索树或嵌套的相同结构之类的对象,则可能会遇到一个很适合递归的问题

    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.
    
    人们避免递归的原因有很多:

  • 大多数人(包括我自己)都热衷于程序或面向对象编程,而不是函数式编程。对这些人来说,迭代方法(通常使用循环)感觉更自然

  • 我们中那些在程序或面向对象编程上刻苦钻研的人经常被告知避免递归,因为它容易出错

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

  • 至少在我使用过的几种编程语言中,我记得听到过一些建议,如果递归超过某个深度,就不要使用递归,因为它的堆栈没有那么深


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

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

  • 如果集合为空,则集合中的项目计数为零(duh!)
  • 如果集合不为空,则计数为1加上移除一个项目后集合中的项目数
  • 假设您有一个这样的集合:[x]。让我们数一数有多少物品

  • 集合是[x],它不是空的,因此我们应用规则2。项目数量为1加上[x x]中的项目数量(即我们删除了一个项目)
  • 集合为[x x],因此我们再次应用规则2:1+[x]中的项数
  • 集合为[x],它仍然符合规则2:1+[]中的项目数
  • 现在集合为[],这与规则1匹配:计数为零
  • 现在我们知道了步骤4中的答案(
    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
    
    numberOfItems(set)
        if set is empty
            return 0
        else
            remove 1 item from set
            return 1 + numberOfItems(set)
    
    struct Node {
        Node* next;
    };
    
    int length(const Node* list) {
        if (!list->next) {
            return 1;
        } else {
            return 1 + length(list->next);
        }
    }
    
    public int Factorial(int n)
    {
        if (n <= 1)
            return 1;
    
        return n * Factorial(n - 1);
    }
    
    public int fact(int n)
    {
        if (n==0) return 1;
        else return n*fact(n-1)
    }
    
    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
    
    factorial(6) = 6*5*4*3*2*1
    
    6 * factorial(5) = 6*(5*4*3*2*1).
    
    factorial(n) = n*factorial(n-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
    
    int FloorByTen(int num)
    {
        if (num % 10 == 0)
            return num;
        else
            return FloorByTen(num-1);
    }
    
    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);
        }
    }
    
      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
    
     integer = 0 | succ(integer)
    
     function computeSomething(x : integer) =
       if x is 0 : base case
       if x is succ(prev) : do something with prev
    
    sum l =
        if empty(l)
            return 0
        else
            return head(l) + sum(tail(l))
    
    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))
    
    sort(l) =
        if empty(l) or length(l) = 1
            return l
        else
            (left,right) = split l
            return merge(sort(left), sort(right))
    
    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.
    
    void f() {
       ... f() ... 
    }
    
    void f() {
        ... g() ...
    }
    
    void g() {
       ... f() ...
    }
    
    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!