在递归过程中究竟发生了什么?

时间:2010-08-18 11:17:11

标签: language-agnostic recursion

任何人都可以告诉我在递归程序中到底发生了什么...... ??

6 个答案:

答案 0 :(得分:11)

一个函数一遍又一遍地调用自身,直到满足某些条件。

一个愚蠢的例子:如何递归地走100步:

 function walk(steps) {
     if (steps > 0) { // not there yet
        take 1 step
        walk(steps - 1); // we're 1 step closer, now walk the rest of the way
     } else { // we're there already, don't walk any further than requested
        echo "You're there!"
     }
 }

 walk(100); // go!

这就是:

 walk(100):
   take 1 step
   walk(99):
     take 1 step
     walk(98):
       take 1 step
       walk(97):
         (...)
                   walk(2):
                     take 1 step
                     walk(1):
                       take 1 step
                       walk(0):
                         // "steps > 0" no longer true, use the "else" branch
                         echo "You're there!"

请注意,使用循环(“迭代”)可以完成同样的事情:

function walk(steps) {
  for (c=steps; c > 0; c--) { // note the condition check - "c > 0"
    take 1 step
  }
  echo "You're there!"
}

walk(100); // go!

程序流程会有所不同,但结果是一样的:

walk(100):
  c=100
  take 1 step
  c=99
  take 1 step
  c=98
  take 1 step
  (...)
  c=2
  take 1 step
  c=1
  take 1 step
  c=0
  // "c > 0" no longer true, exit loop
  echo "You're there!"

不能说递归或迭代总是更好。在这个例子中,迭代(循环)比递归更容易编写和更容易理解;在其他情况下(例如遍历树结构),递归可能是更好的解决方案。

答案 1 :(得分:3)

与迭代相反,递归是一种函数调用自身的算法或函数设计。这可以使函数更容易理解,但是因为必须创建新的堆栈会使它变慢。此外,堆栈内存使用量将随着每次调用而线性增加。

另一方面,迭代在一个函数中循环,将时间复杂度保持在O(n),并且空间复杂度固定而不是随着每次迭代而增加。

例如,考虑添加连续数字的函数。请注意,有一个公式可以通过一个简单的计算(O(1)时间复杂度)来执行此操作,但我们只是以此为例。

递归函数可能如下所示:

long consecutive(long a) {
    return a > 1 ? a + consecutive(a - 1) : a + 1;
}

使用此递归调用可能会很快耗尽堆栈内存。但是,迭代模型对此更好:

long consecutive(long a) {
    long result = 0, i;
    for (i = 1; i <= a; i++) result += i;
    return result;
}

答案 2 :(得分:1)

递归的一个例子:

function foo(doRecurse)
{
    alert("Foo was called");

    if (doRecurse)
    {
        foo(false);
    }
}

foo(true);

在这个例子中,当foo被调用为true时,它会再次使用false调用自身。因此,您将获得上述代码的两条警报消息。函数foo调用函数foo的位是递归。

答案 3 :(得分:1)

递归程序可能如下所示:

def f(i):
    if i > 100:
        return 100
    else:
        return f(i+1)

print f(7)

(蟒)

问题是,会发生什么?答案是最好在功能方面考虑这个问题并尝试将其写出来。

所以,我们有声明print f(7)。为了找出这个评估的内容,我们通过函数运行7并找出它的计算结果为f(8)。好的,但是评估的是什么? f(9)。我故意在这里有一个退出条款 - 最终i将在循环中等于100,这将是返回的内容。会发生什么:

  • f(7) = ?
  • f(7) = f(8) = ?
  • f(7) = f(8) = f(9) = ?
  • f(7) = f(8) = f(9) = f(10) = ?
  • ...
  • f(7) = f(8) = ... = 100
  • 所以f(7) is 100

这是一个相当简单的例子,但确实证明了递归。

答案 4 :(得分:1)

正如其他人所提到的,递归只是一种自称的方法。这有几个优点,但也有一些缺点。

例如典型的阶乘计算:

迭代版本:

int factorial(int factor)
{
    int result = 1;
    int current_factor = 1;
    while (current_factor < factor)
    {
      result *= current_factor;
      ++current_factor;
    }
    return result;
}

递归版:

int factorial(int factor)
{
  if (factor == 1) return 1;
  return factor * factorial(factor - 1);
}

正如您所看到的,代码更短,更容易阅读(如果您习惯于递归)。

现在让我们检查一下堆栈:

  • Interative版本:3个整数变量(factor,current_factor,result)
  • 递归版:
    阶乘1:1变量(因子)
    2:2变量的因子(因子和因子的返回值(因子-1))
    阶乘3:3变量(因子,阶乘的返回值(因子-1),其具有另一个阶乘的返回值(因子-1)) ...

看起来像这样粗鲁

factorial(5):
  factorial(4):
    factorial(3):
      factorial(2):
        factorial(1):
        1
      2+1
    3+2+1
  4+3+2+1
5+4+3+2+1

正如您所看到的,递归需要更多的空间在堆栈上(您必须添加返回指针,推送和弹出变量以及函数调用所需的其他东西)并且它通常较慢。使用尾部优化的递归可以减少空间问题,但这对于这篇文章来说太大了,但为了完整性,优化版本看起来像这样:

int factorial(int current, int end, int *result) // result is a pointer, so changes
                                               // on result affect the original variable
{
  if (current > end) return;
  *result = *result * current;
  factorial(current + 1, end, result);
}

递归真正有用的唯一地方是(恕我直言)穿过树木结构。

答案 5 :(得分:0)

一个函数调用自身。这可以用来代替迭代,但通常效率较低,因为必须为每个函数调用分配新的堆栈。