使斐波那契更快

2022-09-01 07:12:21

我被要求编写斐波那契算法的简单实现,然后使其更快

这是我的初始实现

public class Fibonacci {

    public static long getFibonacciOf(long n) {
        if (n== 0) {
            return 0;
        } else if (n == 1) {
            return 1;
        } else {
            return getFibonacciOf(n-2) + getFibonacciOf(n-1);
        }
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner (System.in);
        while (true) {
            System.out.println("Enter n :");
            long n = scanner.nextLong();
            if (n >= 0) {
                long beginTime = System.currentTimeMillis();
                long fibo = getFibonacciOf(n);
                long endTime = System.currentTimeMillis();

                long delta = endTime - beginTime;

                System.out.println("F(" + n + ") = " + fibo + " ... computed     in " + delta + " milliseconds");
            } else {
                break;

            }
        }

    }

}

正如你所看到的,我正在使用System.currentTimeMillis()来简单测量计算斐波那契时经过的时间。

这种实现速度很快呈指数级增长,如下图所示

simple version of fibonacci's algorithm

所以我有一个简单的优化想法。将以前的值放在 HashMap 中,而不是每次都重新计算它们,只需将它们从 HashMap 中取回(如果它们存在)。如果它们不存在,我们就把它们放在HashMap中

这是新版本的代码

public class FasterFibonacci {

    private static Map<Long, Long> previousValuesHolder;
    static {
        previousValuesHolder = new HashMap<Long, Long>();
        previousValuesHolder.put(Long.valueOf(0), Long.valueOf(0));
        previousValuesHolder.put(Long.valueOf(1), Long.valueOf(1));
    }
    public static long getFibonacciOf(long n) {
        if (n== 0) {

            return 0;
        } else if (n == 1) {
            return 1;
        } else {
            if (previousValuesHolder.containsKey(Long.valueOf(n))) {
                return previousValuesHolder.get(n);
            } {

                long newValue = getFibonacciOf(n-2) + getFibonacciOf(n-1);
                previousValuesHolder.put(Long.valueOf(n),     Long.valueOf(newValue));
                return newValue;
            }

        }
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner (System.in);
        while (true) {
            System.out.println("Enter n :");
            long n = scanner.nextLong();
            if (n >= 0) {
                long beginTime = System.currentTimeMillis();
                long fibo = getFibonacciOf(n);
                long endTime = System.currentTimeMillis();

                long delta = endTime - beginTime;

                System.out.println("F(" + n + ") = " + fibo + " ... computed     in " + delta + " milliseconds");
            } else {
                break;

            }
        }

    }

这种变化使计算速度非常快。我立即计算了从2到103的所有值,并且在F(104)处得到了一个很长的溢出(给我F(104) = -7076989329685730859,这是错误的)。我发现它是如此之快,以至于**我想知道我的代码中是否有任何错误(感谢您的检查,请让我知道)**。请看第二张图片:

Faster Fibonacci

我更快的斐波那契算法的实现是否正确(在我看来,因为它得到的值与第一个版本相同,但由于第一个版本太慢,我无法用它计算更大的值,如F(75)))?我还可以使用哪些其他方法来加快速度?还是有更好的方法来使它更快?另外,我如何计算更大值(如150,200)的斐波那契而不得到**长溢出**?虽然它看起来很快,但我想把它推向极限。我记得Abrash先生说过“最好的优化器在你的两只耳朵之间”,所以我相信它仍然可以改进。感谢您的帮助

[版本说明:]虽然这个问题解决了我问题中的一个要点,但你可以从上面看到我有额外的问题。


答案 1

动态规划

想法:无需多次重新计算相同的值,只需存储计算的值并在过程中使用它们即可。

f(n)=f(n-1)+f(n-2)其中 f(0)=0,f(1)=1。因此,在计算 f(n-1) 时,如果存储 f(n) 和 f(n-1) 的值,则可以轻松计算 f(n)。

让我们先来看看一个 Bignums 数组。A[1..200]。将它们初始化为 -1。

伪代码

fact(n)
{
if(A[n]!=-1) return A[n];
A[0]=0;
A[1]=1;
for i=2 to n
  A[i]= addition of A[i],A[i-1];
return A[n]
}

这以 O(n) 时间运行。亲自检查一下。

这种技术也称为记忆。

理念

动态规划(通常称为DP)是一种非常强大的技术,用于解决特定类别的问题。它需要非常优雅的方法和简单的思维公式,编码部分非常容易。这个想法很简单,如果你已经用给定的输入解决了一个问题,那么保存结果以供将来参考,以避免再次解决同样的问题。很快“记住你的过去”。

如果给定的问题可以分解为较小的子问题,而这些较小的子问题又被划分为更小的问题,并且在此过程中,如果您观察到一些,那么这对DP来说是一个很大的提示。此外,子问题的最优解有助于给定问题的最优解(称为 )。over-lappping subproblemsOptimal Substructure Property

有两种方法可以做到这一点。

1.)自上而下:通过分解来开始解决给定的问题。如果您发现问题已经解决,则只需返回保存的答案即可。如果尚未解决,请解决它并保存答案。这通常很容易想到,而且非常直观。这称为“备忘录”。(我用过这个想法)。

2.)自下而上:分析问题并查看子问题的解决顺序,并从琐碎的子问题开始解决,直到给定的问题。在此过程中,可以保证在解决问题之前先解决子问题。这称为动态编程。(用这个主意)MinecraftShamrock


还有更多!

(其他方法)

看看我们对获得更好解决方案的追求并没有就此结束。你会看到一个不同的方法——

如果你知道如何解决,那么你会找到这种关系的解决方案recurrence relation

f(n)=f(n-1)+f(n-2) given f(0)=0,f(1)=1

求解后,您将得出公式 -

f(n)= (1/sqrt(5))((1+sqrt(5))/2)^n - (1/sqrt(5))((1-sqrt(5))/2)^n

可以以更紧凑的形式编写

f(n)=floor((((1+sqrt(5))/2)^n) /sqrt(5) + 1/2)

复杂性

您可以在 O(logn) 操作中获取数字的幂。你必须通过平方来学习幂。

编辑:很高兴指出,这并不一定意味着斐波那契数列可以在O(logn)中找到。实际上,我们需要线性计算乌鸦的位数。可能是因为我说过,它似乎声称一个错误的想法,即一个数字的阶乘可以在O(logn)时间内计算。[Bakurui,MinecraftShamrock对此发表评论]


答案 2

如果你需要非常频繁地计算第n个斐波那契数列,我建议使用amalsom的答案。

但是如果你想计算一个非常大的斐波那契数列,你会耗尽内存,因为你存储了所有较小的斐波那契数列。以下伪代码仅将最后两个斐波那契数列保存在内存中,即它需要的内存要少得多:

fibonacci(n) {
    if n = 0: return 0;
    if n = 1: return 1;
    a = 0;
    b = 1;
    for i from 2 to n: {
        sum = a + b;
        a = b;
        b = sum;
    }
    return b;
}

分析
这可以以相当低的内存消耗计算非常高的斐波那契数列:我们有O(n)时间,因为循环重复n-1次。空间复杂性也很有趣:第n个斐波那契数列的长度为O(n),可以很容易地显示:
Fn <= 2 * Fn-1
这意味着第n个斐波那契数列最多是其前身的两倍。将二进制数加倍等效于单个左移,这会将必要的位数增加一。因此,表示第n个斐波那契数列最多需要O(n)空间。我们在内存中最多有三个连续的斐波那契数列,这使得O(n)+ O(n-1)+ O(n-2) = O(n)总空间消耗。与此相反,记忆算法始终将前n个斐波那契数列保存在内存中,这使得O(n)+ O(n-1)+ O(n-2) + ... + O(1) = O(n^2)空间消耗。

那么应该使用哪种方式呢?
将所有较低的斐波那契数列保留在内存中的唯一原因是,如果您非常频繁地需要斐波那契数列。这是一个平衡时间与内存消耗的问题。