【问题标题】:My Fibonacci sequence as a recursive function is an infinite loop我的斐波那契数列作为递归函数是一个无限循环
【发布时间】:2014-10-15 09:20:59
【问题描述】:

以下函数无限递归,我不明白为什么。它进入条件语句,但似乎没有以return 语句终止。

use strict;
use warnings;

print fibonacci(100);

sub fibonacci {

    my $number = shift;

    if ($number == 0) {
        print "return 0\n";
        return 0;
    }
    elsif ($number == 1) {
        print "return 1\n";
        return 1;
    }
    else {
        return fibonacci($number-1) + fibonacci($number-2);
    }
}

【问题讨论】:

  • 尝试一个更小的数字作为输入。
  • 不是无限循环;它只是需要很长时间(并且可能会触发旨在检测无限循环的检查,因为它运行堆栈的深度。)

标签: perl recursion fibonacci


【解决方案1】:

您的循环不会无限递归,只是输入 100 会花费太长时间。尝试记忆化版本:

{ my @fib;
  sub fib {
    my $n = shift;
    return $fib[$n] if defined $fib[$n];
    return $fib[$n] = $n if $n < 2;
    $fib[$n] = fib($n-1) + fib($n-2);
  }
}

【讨论】:

  • 这种手动记忆在我的机器上比 Memoize 的性能高出 2 倍,尽管对于使用该模块的自动化和一致性有一些话要说。
  • @MarkReed:我怀疑你是否会发现很多人关心他们是否会在 15 毫秒或 7 毫秒内得到结果。
  • @MarkReed Memoize 使用哈希来实现缓存,因为它涵盖了一般情况。在这个特定示例中使用数组可能是更高性能的来源。
【解决方案2】:

您的代码可以变得更加简洁,并通过记忆它大大加快速度。

Memoize 模块是迄今为止记忆子程序(缓存结果)的最简单、最清晰的方法。

我推荐这个版本。它仍然会警告深度递归,但它会产生一个我认为是正确的值。

use strict;
use warnings;

use Memoize;
memoize('fibonacci');

print fibonacci(100);

sub fibonacci {
    my ($n) = @_;
    $n < 2 ? $n : fibonacci($n-1) + fibonacci($n-2);
}

输出

Deep recursion on anonymous subroutine at E:\Perl\source\fib.pl line 11.
Deep recursion on anonymous subroutine at E:\Perl\source\fib.pl line 11.
3.54224848179262e+020

【讨论】:

  • 如果你在打代码高尔夫,重写函数体没问题,否则就没有必要了。只需在 OP 的定义中使用 memoize 即可将其加速到它在我的机器上不到 15 毫秒的时间内返回第 100 个斐波那契数。
  • @MarkReed:这远非代码高尔夫。所有必要的空白都已到位,以使其含义清晰。如果您对条件运算符不满意,我很同情,但习惯阅读 Perl 代码的人会发现它比 14 行原始代码更容易阅读。
  • 我读得很好;我只是说改变对于性能提升来说是不必要的。 :)
  • @MarkReed:不,马克,你说改变是“如果你在打代码高尔夫,那很好,但在其他方面是不必要的”。这与 “对于性能提升不是必需的” 不同。
【解决方案3】:

您的递归函数调用不是无限的。只是需要很长时间。

人们应该注意递归函数,因为如果可以这样设计,最好使用循环来编写代码。

在这种情况下,您对fibonacci 的递归调用次数呈指数增长。您可以通过简单地添加较低级别的调用来非常轻松地自己测试这一点

  • fibonacci(0) = 1 次通话
  • fibonacci(1) = 1 次通话
  • fibonacci(2) = 1 次呼叫 + 呼叫 fibonacci(1) + 呼叫 fibonacci(0) → 3
  • fibonacci(3) = 1 次呼叫 + 呼叫 fibonacci(2) + 呼叫 fibonacci(1) → 5
  • fibonacci(4) = 1 次呼叫 + 呼叫 fibonacci(3) + 呼叫 fibonacci(2) → 9
  • fibonacci(5) = 1 次呼叫 + 呼叫 fibonacci(4) + 呼叫 fibonacci(3) → 15
  • fibonacci(N) = 1 次呼叫 + 呼叫 fibonacci(N-1) + 呼叫 fibonacci(N-2) → ?

如您所见,子例程调用的数量呈斐波那契式增长。

要确定fibonacci(100) 的调用次数,我们可以简单地创建一个快速的 perl 单行:

$ perl -e '
    @c = (1, 1); 
    $c[$_] = 1 + $c[$_-1] + $c[$_-2] for (2..100);
    print $c[100]
  ' 
1.14629568802763e+21

以每秒 1 万亿次呼叫的速度,这仍需要 31 年多的时间才能完成。

使用 Memoize 修复

已经提出的一个修复方法是使用核心库Memoize

use Memoize;
memoize('fibonacci');

此模块将环绕您的子例程调用并缓存值的计算。因此,这会将您的函数调用次数从 1.14e21 减少到字面上的 101 次计算。

但是,由于您的递归次数超过 100 次,因此您将根据 perldiag 收到以下警告

  • 匿名子程序的深度递归
  • 子程序“%s”的深度递归

    (W recursion) 这个子程序调用自己(直接或间接)的次数比它返回的次数多 100 倍。这可能表示无限递归,除非您正在编写奇怪的基准程序,在这种情况下它表示其他内容。

    此阈值可以从 100 更改,方法是重新编译 perl 二进制文件,将 C 预处理器宏 PERL_SUB_DEPTH_WARN 设置为所需值。

改为创建循环 - 避免递归

您的算法可以很容易地从自上而下的计算方法重新设计为自下而上的方法。

这完全消除了递归的需要,并将代码简化为以下内容:

use strict;
use warnings;

print fibonacci(100), "\n";

sub fibonacci {
    my $number = shift;

    my @fib = ( 0, 1 );

    for ( $#fib + 1 .. $number ) {
        $fib[$_] = $fib[ $_ - 1 ] + $fib[ $_ - 2 ];
    }

    return $fib[$number];
}

输出:

3.54224848179262e+20

不会给出警告。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-07-27
    • 2023-03-12
    • 2020-03-21
    • 2012-11-19
    • 2021-12-01
    • 2016-11-07
    • 2012-02-16
    相关资源
    最近更新 更多