【问题标题】:What is a non recursive solution for Fibonacci-like sequence in Java?Java中类斐波那契序列的非递归解决方案是什么?
【发布时间】:2012-02-25 16:40:21
【问题描述】:

给定这个函数的伪代码

f(0) = 1; 
f(1) = 3; 
f(n) = 3 * f(n - 1) - f(n - 2); // for n >= 2.

有没有一种非递归的方式来做到这一点?

【问题讨论】:

  • 是 3*(f(n - 1) - f(n - 2)) 还是 (3*f(n-1))-f(n - 2)
  • @nixit:通常可以安全地假设* 的优先级高于+——我认为APL(和派生语言)和Smalltalk 是唯一不分配@987654324 的语言@ 比 + 更高的优先级。 (虽然有人可能会争辩说,RPN 对* 的优先级也不比+ 高,但这仅仅是因为没有 RPN 没有优先规则...)
  • @sarnold 是的,我知道,但是如果我们将值 0 放入 f(n),那么我得到的值是 -1,所以我会感到困惑
  • @nixit:这里未说明的假设是 f(n) 仅用于非负整数,而公式 f(n) = 3 * f(n-1) - f(n-2) 仅用于大于或等于 2 的整数。 f(0) 根据定义为您提供1 的结果。

标签: java algorithm math recursion


【解决方案1】:

是的,所有递归算法都可以转换为迭代算法。您的问题的递归解决方案类似于(伪代码):

def f(n):
    if n == 0: return 1
    if n == 1: return 3
    return 3 * f(n-1) - f(n-2)

由于您只需要记住前两项即可计算当前一项,因此您可以使用类似于以下伪代码的内容:

def f(n):
    if n == 0:
        return 1
    if n == 1:
        return 3
    grandparent = 1
    parent = 3
    for i = 2 to n:
        me = 3 * parent - grandparent
        grandparent = parent
        parent = me
    return me

这只是首先处理“递归”终止条件,然后在它通常调用自身的地方迭代。在每次迭代中,您计算​​当前术语,然后通过祖父母和父母轮换这些术语。

计算当前迭代后,无需保留祖父母,因为它不再使用。

事实上,可以说迭代解决方案更好(从性能角度来看),因为术语不会像在递归解决方案中那样重新计算。递归解决方案确实有一些优雅的地方(递归解决方案通常这样做)。


当然,就像斐波那契数列一样,您计算的值上升得非常快,因此,如果您想要最快的解决方案(您应该检查所有性能声明,包括我的),可以使用预先计算的查找表去。

使用以下 Java 代码创建一个长值表(while 条件只是捕捉溢出的一个偷偷摸摸的技巧,这是您可以停止构建数组的点):

class GenLookup {
    public static void main(String args[]) {
        long a = 1, b = 3, c;
        System.out.print ("long lookup[] = { " + a + "L, " + b + "L");
        c = 3 * b - a;
        while ((c + a) / 3 == b) {
            System.out.print (", " + c + "L");
            a = b; b = c; c = 3 * b - a;
        }
        System.out.println (" };");
    }
} 

为您提供一个数组定义,您可以将其插入到查找函数中,如下例所示:

public static long fn (int n) {
    long lookup[] = { 1L, 3L, 8L, 21L, 55L, 144L, 377L, 987L, 2584L, 6765L,
        17711L, 46368L, 121393L, 317811L, 832040L, 2178309L, 5702887L,
        14930352L, 39088169L, 102334155L, 267914296L, 701408733L,
        1836311903L, 4807526976L, 12586269025L, 32951280099L, 86267571272L,
        225851433717L, 591286729879L, 1548008755920L, 4052739537881L,
        10610209857723L, 27777890035288L, 72723460248141L, 190392490709135L,
        498454011879264L, 1304969544928657L, 3416454622906707L,
        8944394323791464L, 23416728348467685L, 61305790721611591L,
        160500643816367088L, 420196140727489673L, 1100087778366101931L,
        2880067194370816120L, 7540113804746346429L };

    if ((n < 1) || (n > lookup.length))
        return -1L;

    return lookup[n-1];
}

有趣的是,WolframAlpha 提出了一种甚至不使用迭代的公式化方法。如果你去their site并输入f(0)=1, f(1)=3, f(n)=3f(n-1)-f(n-2),你会得到公式:

不幸的是,它可能没有迭代那么快,因为它使用浮点数,因为输入值的数量有限,这些输入值可以适合 Java long。几乎可以肯定(但同样,您需要检查这一点)比查找表要慢。

而且,它在数学世界中可能是完美的,因为现实世界的限制(如非无限存储)不会发挥作用,但可能由于 IEEE 精度的限制,它在更高的值 n 时会崩溃.

以下函数等效于该表达式和查找解决方案:

class CheckWolf {
    public static long fn2 (int n) {
        return (long)(
            (5.0 - 3.0 * Math.sqrt(5.0)) *
                Math.pow(((3.0 - Math.sqrt(5.0)) / 2.0), n-1) +
            (5.0 + 3.0 * Math.sqrt(5.0)) *
                Math.pow(((3.0 + Math.sqrt(5.0)) / 2.0), n-1)
            ) / 10;
    }

    public static long fn (int n) {
        long lookup[] = { 1L, 3L, 8L, 21L, 55L, 144L, 377L, 987L, 2584L, 6765L,
            17711L, 46368L, 121393L, 317811L, 832040L, 2178309L, 5702887L,
            14930352L, 39088169L, 102334155L, 267914296L, 701408733L,
            1836311903L, 4807526976L, 12586269025L, 32951280099L, 86267571272L,
            225851433717L, 591286729879L, 1548008755920L, 4052739537881L,
            10610209857723L, 27777890035288L, 72723460248141L, 190392490709135L,
            498454011879264L, 1304969544928657L, 3416454622906707L,
            8944394323791464L, 23416728348467685L, 61305790721611591L,
            160500643816367088L, 420196140727489673L, 1100087778366101931L,
            2880067194370816120L, 7540113804746346429L };
        if ((n < 1) || (n > lookup.length)) return -1L;
        return lookup[n-1];
    }

现在我们需要一条主线来比较它们:

    public static void main(String args[]) {
        for (int i = 1; i < 50; i++)
            if (fn(i) != fn2(i))
                System.out.println ("BAD:  " + i + ": " + fn(i) + ", " + fn2(i)
                    + " (" + Math.abs(fn(i) - fn2(i)) + ")");
            else
                System.out.println ("GOOD: " + i + ": " + fn(i) + ", " + fn2(i));
        }
    }

这将输出:

GOOD: 1: 1, 1
GOOD: 2: 3, 3
GOOD: 3: 8, 8
GOOD: 4: 21, 21
GOOD: 5: 55, 55
GOOD: 6: 144, 144
GOOD: 7: 377, 377
GOOD: 8: 987, 987
GOOD: 9: 2584, 2584
GOOD: 10: 6765, 6765
GOOD: 11: 17711, 17711
GOOD: 12: 46368, 46368
GOOD: 13: 121393, 121393
GOOD: 14: 317811, 317811
GOOD: 15: 832040, 832040
GOOD: 16: 2178309, 2178309
GOOD: 17: 5702887, 5702887
GOOD: 18: 14930352, 14930352
GOOD: 19: 39088169, 39088169
GOOD: 20: 102334155, 102334155
GOOD: 21: 267914296, 267914296
GOOD: 22: 701408733, 701408733
GOOD: 23: 1836311903, 1836311903
GOOD: 24: 4807526976, 4807526976
GOOD: 25: 12586269025, 12586269025

这里看起来不错,还有更多:

GOOD: 26: 32951280099, 32951280099
GOOD: 27: 86267571272, 86267571272
GOOD: 28: 225851433717, 225851433717
GOOD: 29: 591286729879, 591286729879
GOOD: 30: 1548008755920, 1548008755920
GOOD: 31: 4052739537881, 4052739537881
GOOD: 32: 10610209857723, 10610209857723
GOOD: 33: 27777890035288, 27777890035288
GOOD: 34: 72723460248141, 72723460248141
GOOD: 35: 190392490709135, 190392490709135
GOOD: 36: 498454011879264, 498454011879264

但后来事情开始出错了:

BAD:  37: 1304969544928657, 1304969544928658 (1)
BAD:  38: 3416454622906707, 3416454622906709 (2)
BAD:  39: 8944394323791464, 8944394323791472 (8)
BAD:  40: 23416728348467685, 23416728348467705 (20)
BAD:  41: 61305790721611591, 61305790721611648 (57)
BAD:  42: 160500643816367088, 160500643816367232 (144)
BAD:  43: 420196140727489673, 420196140727490048 (375)

上述结果非常接近,并且错误中的位数与结果中的位数成正比,这表明它可能是精度损失问题。

此时,公式函数才开始返回最大的 long 值:

BAD:  44: 1100087778366101931, 922337203685477580 (177750574680624351)
BAD:  45: 2880067194370816120, 922337203685477580 (1957729990685338540)
BAD:  46: 7540113804746346429, 922337203685477580 (6617776601060868849)

然后我们的查找函数也崩溃了,因为数字太大了很长时间:

BAD:  47: -1, 922337203685477580 (922337203685477581)
BAD:  48: -1, 922337203685477580 (922337203685477581)
BAD:  49: -1, 922337203685477580 (922337203685477581)

【讨论】:

  • 只有 tail-recursive 算法(以及那些可以变成尾递归的)可以转换为迭代算法。 OP 的算法不能迭代。不过,这并不意味着没有可以做同样事情的迭代算法。
  • 我不确定我是否认同你所做的区分。 OP没有提供算法;他提供了一个递归定义,@paxdiablo 证明可以迭代计算。
  • @ikegami,all 递归算法可以进行迭代,前提是您可以管理自己的数据堆栈而不是使用函数堆栈。我怀疑你的意思是尾递归对于 compiler 来说更容易转换为迭代。
  • @ikegami 在 CS 中“递归”的含义有一个明确的定义。您可以使用自己的,但如果大多数人不明白您在做什么,请不要感到惊讶。 (这不是说你的定义是错误的 - 但我们对这些事情有约定的原因是我们可以谈论事情而不必每次都从头开始)
  • 我喜欢维基百科的名言“如果你已经知道什么是递归,请记住答案,否则,找一个比你更接近 Douglas Hofstadter 的人;然后问他或她什么是递归”。话虽如此,我认为我们应该关闭讨论(或将其移至讨论区)。 (错误)引用 BillG:“15 cmets 对于任何的答案应该足够了”:-)
【解决方案2】:

这里的答案是正确的,但它们在 O(n) 中工作,而您可以在 O(log n) 中完成,速度呈指数级增长。观察一下

[f(n)  ] = [3 -1] [f(n-1)]
[f(n-1)]   [1  0] [f(n-2)]

令 vn 为向量 [f(n), f(n-1)] 和 A 为上述矩阵,所以你得到 vn = A vn-1,因此 vn = An-1 v1。使用binary exponentiation 计算矩阵 A 的 (n-1) 次方,并将其乘以 v1。有关线性重复的更多信息,请参阅here

【讨论】:

  • 还可以找到特征值,并使用特征方程根据较低的矩阵幂来评估矩阵幂...哦等等。
【解决方案3】:

如果您的问题是关于是否可以找到函数的等效非递归定义,您应该搜索Fibonacci sequence 的属性。

您的序列可以通过编写斐波那契(不包括前 2 个数字)并删除每个第 2 个数字来找到:1、3、8、21、55、144、...

sqrt5 = sqrt(5)
phi = ( 1 + sqrt5 ) / 2
fibonacci(n) = round( power( phi, n ) / sqrt5 ) 
f(n) = fibonacci( 2*n + 2 )

【讨论】:

  • 原题是对斐波那契稍作修改;这是原版的 Fib 还是稍作修改?
  • @sarnold: fibonacci(n) 是斐波那契。 f(n) 是被问到的序列。
【解决方案4】:

很简单,Java 中的解决方案如下所示:

public int f(int n) {

      int tmp;
      int a = 3;
      int b = 1;

      for (int i = 0; i < n; i++) {
          tmp = a;
          a = 3 * a - b;
          b = tmp;
      }

      return b;

}

所有递归解决方案都可以转换为迭代解决方案(反之亦然,请参阅post),尽管如果递归解决方案采用尾递归形式,则更容易。

上述算法可以理解为原始递归的动态规划解决方案,它非常高效,因为它只需要保存迭代中每个点的前两个值。

【讨论】:

  • 我们如何知道如何将解决方案转换为迭代解决方案?如果我有超过 2 个起始值怎么办?
  • 您添加了更多变量。每个问题都不一样,如果您有具体示例,请将其作为新问题发布。
【解决方案5】:

[ 糟糕,我认为这是一个 Perl 问题。尽管如此,代码对于 Java 开发人员来说应该足够可读。 ]

这实际上只是将递归移动到用户空间,但您可以使用:

sub f { 
    my ($n) = @_;
    my @f = (1,3);
    $f[$_] = 3 * $f[$_-1]- $f[$_-2] for 2 .. $n;
    return $f[$n];
}

当然,这需要缓存。无需重新计算我们已经知道的值。

my @f = (1,3);
sub f { 
    my ($n) = @_;
    $f[$_] = 3 * $f[$_-1]- $f[$_-2] for @f .. $n;
    return $f[$n];
}

【讨论】:

  • 伙计,几年前我编写 Perl 已有好几年了,发现这段代码很难在第一次开始时理解。 Perl 可能有很多东西,但清晰的并不是其中之一。 (这似乎受到 ElKamina's answer 所需的相同 O(N) 空间的影响,但您的空间足以存储结果以供以后使用。)
  • “对于 Java 开发人员来说应该足够可读”——我怀疑只有一个做过额叶切除术的人。 Perl 代码通常对每个人来说都是不可读的,包括 Perl 开发人员,我之前已经看到它被描述为一种只写语言 :-) 顺便说一句,这还不错,我偶尔会编写 Perl sn-p。
【解决方案6】:

函数是根据自身定义的,因此在某种意义上,任何实现都是递归的,除非某个数学家来告诉我们f(n) 可以在不评估f(n-1)f(n-2) 的情况下进行评估。正如其他人所展示的,有一种方法可以在不调用自身的 Java 函数中实现它。

【讨论】:

  • 你的意思是 (130.5-3)/(2*130.5)*((3-130.5)/2)**n + (0.5 +3/(2*130.5))*((3+13**0.5)/2)**n)
  • 我不知道,但鉴于你是 WolframH,我一定是这个意思
  • 这是 f(n),它不计算 f(n-1) 和 f(n-2),我 am 是一名数学家 - 希望很清楚现在:-)
  • 我不是在讽刺,这正是我要问的。谢谢:)
  • @WolframH,你应该把它放在一个答案中(连同一些证据或支持)。它一个非递归解决方案,假设它是正确的,我没有理由怀疑,尽管没有强烈的意愿去确认:-) 它可能比n的足够大值的迭代解决方案更好(包括我的)所以至少值得一票。
【解决方案7】:
def func(n):
    f= array(n+1)
    f[0]=1
    f[1]=3

    for i in 2:n :
        f[i] = 3*f[i-1]-f[i-2]
    return f[n]

【讨论】:

  • O(N) 空间解决方案可以在 O(1) 中轻松解决?对此不满意。
  • @Voo 这个问题不需要 O(1) 空间解决方案。由于避免了两个(微不足道的)赋值语句,因此该解决方案应该更快:D
  • 嗯.. 不,那个解决方案会慢很多,除非编译器可以证明我们不需要数组(在这种情况下,我们得到了 pax 的解决方案)。如果我们有数组,我们有 N+1 次存储到内存和 2N 次从内存中读取,而不是在寄存器中计算整个事物。就像我说的那样慢得多:)
  • @Voo 当然 paxdiablo 的解决方案比我的好(我赞成)。但是这个解决方案虽然不是最优的,但仍然有效。
  • 此解决方案的优点是您可以记住计算的值(通过将数组放在函数之外并可能跟踪您计算的位置)。这可以大大加快未来的计算速度。
【解决方案8】:

斐波那契数列的开头为:0,1,1,2,3,5,8,13,21,34,55....

这可以通过简单的递归关系 F(n)=F(n-1)+F(n-2) 来定义,对于 n>1 和两个初始条件,F(0)=1和 F(1)=1

斐波那契算法

//计算第n个斐波那契数

//输入:非负整数

//输出:第n个斐波那契数

1. Begin Fibo
2. integer n, i;
3.    if n<=1 then
4.     return n;
5.  else
6.    F(0)<-0; F(1)<-1;
7.    for i<-2 to n do
8.     F(i)<-F(i-1)+F(i-2);
9.     F(i-2)=F(i-2);
10.    F(i-1)=F(i);
11. done
12. end if
13. end Fibo

【讨论】:

    【解决方案9】:

    这里只是一个具有最少代码行和最大灵活性的函数。

    您可以简单地添加任何“初始值”和任何其他递归“函数”。

    def fib(n):
      fibs = [1, 3]       # <--  your initial values here 
      if n == 1:
        return fibs[0]
      if n == 2:
        return fibs[:1]
      for i in range(2, n):
        fibs.append(3*fibs[-1] - fibs[-2])  # <-- your function here
      return fibs
    

    结果是:

    n=10
    print(fib(n))
    
    [1, 3, 8, 21, 55, 144, 377, 987, 2584, 6765]
    

    【讨论】:

      【解决方案10】:

      根据@paxdiablo 的要求,我正在回答这个问题。这是一种递归关系,可以非递归地求解,类似于另一个答案中提到的斐波那契数列。结果是(Python 表示法)。

      def f(n):
          return int((13**0.5-3)/(2*13**0.5)*((3-13**0.5)/2)**n + (0.5+3/(2*13**0.5))*((3+13**0.5)/2)**n)
      

      但是,这个论坛很可能适用于较大的 n,因为浮点精度有限。给定的 python 版本在 n = 30 时失败:

      >>> print ([f(n) == 3 * f(n-1) + f(n-2) for n in range(2, 30)])
      [True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, False]
      >>> print([f(n) for n in range(30)])
      [1, 3, 10, 33, 109, 360, 1189, 3927, 12970, 42837, 141481, 467280, 1543321, 5097243, 16835050, 55602393, 183642229, 606529080, 2003229469, 6616217487, 21851881930, 72171863277, 238367471761, 787274278560, 2600190307441, 8587845200883, 28363725910090, 93679022931153, 309400794703549, 1021881407041801]
      

      警告:我使用了“+”而不是“-”,所以公式是错误的。见 cmets。

      【讨论】:

      • 我认为只要限制是已知(并记录在案),它仍然是一个有效的解决方案。例如,Java(有符号)long 类型无论如何最多只能处理 n=46。 n=23 是有符号和无符号 32 位 int 的限制。我要提到的一件事是您添加第二项而不是减去它。我仍然对你如何从规则到表达式感兴趣,而维基百科页面显示如何为 fib(n) 做到这一点让我头疼:-)
      • @GameZela,那个链接对我不起作用,有些角色被吞了。但是,是的,wolframalpha 是解决问题的一种好方法。我至少部分是手工完成的。 en.wikipedia.org/wiki/Recurrence_relation 解释了如何做到这一点 - 这是一个(简单的)特殊情况,在“一般方法”下进行了描述。
      • @paxdiablo,你说得对,我混淆了 + 和 -... 所以这解决了一个不同的问题。如果您想要正确的解决方案,只需按照 GameZelda 的建议使用 wolframalpha。
      【解决方案11】:

      我不知道为什么没有人提出以下算法,它更简单更快:

      class Fibonacci {
      
          public void compute(int n) {
              long i=0;
              long a=0,b=1,d=0;
      
              while (i<n) {
      
                  System.out.println(a);
                  d=a;
                  a=b;
                  b=b+d;
                  i+=1;
              }
          }
      
          public static void main(String[] args) {
          
              var fib = new Fibonacci();
      
              fib.compute(90);
          }
      
      }
      

      输出:

      0
      1
      1
      2
      3
      5
      8
      13
      21
      34
      55
      89
      144
      233
      377
      610
      987
      1597
      2584
      4181
      6765
      10946
      17711
      28657
      46368
      75025
      121393
      196418
      317811
      514229
      832040
      1346269
      2178309
      3524578
      5702887
      9227465
      14930352
      24157817
      39088169
      63245986
      102334155
      165580141
      267914296
      433494437
      701408733
      1134903170
      1836311903
      2971215073
      4807526976
      7778742049
      12586269025
      20365011074
      32951280099
      53316291173
      86267571272
      139583862445
      225851433717
      365435296162
      591286729879
      956722026041
      1548008755920
      2504730781961
      4052739537881
      6557470319842
      10610209857723
      17167680177565
      27777890035288
      44945570212853
      72723460248141
      117669030460994
      190392490709135
      308061521170129
      498454011879264
      806515533049393
      1304969544928657
      2111485077978050
      3416454622906707
      5527939700884757
      8944394323791464
      14472334024676221
      23416728348467685
      37889062373143906
      61305790721611591
      99194853094755497
      160500643816367088
      259695496911122585
      420196140727489673
      679891637638612258
      1100087778366101931
      1779979416004714189
      
      

      【讨论】:

        猜你喜欢
        • 2012-02-16
        • 2020-07-15
        • 2020-04-13
        • 1970-01-01
        • 1970-01-01
        • 2019-05-10
        • 2021-12-01
        • 2010-12-03
        相关资源
        最近更新 更多