JAVA计算斐波那契第100万项的最快算法排名汇总

最近在知乎上看到一个有趣的算法题:斐波那契数列的第一百万项怎么用 C++ 求?

看了几个大神的解答后,注意到很少有用JAVA代码去解决这个问题的,可能一方面java对这种超大数高精度的运算本身就不是特别擅长,另一方面,可能高手都喜欢用各种简短的代码完成复杂的操作.

本人也不是研究算法的,只是对各位大神的算法比较感兴趣,同时也喜欢研究数学,因此结合各路大神的思路和算法,总结了各种高效算法,并且翻译成了java版本,并且对运行时间做了比较,排出了一个榜单.

程序主要研究的是算法和数据结构,我们先来快速了解一下题目:要求我们计算菲波那切数列的第100万项,也可以说是求第n项,注意这边的n非常大.因此我们的算法不光要科学有效(必须要真的能计算出来,不能数值越界),同时要尽可能的快.

一般各类此教程都会介绍矩阵快速幂(因为时间复杂度度最低,但是这算法的时间常数项大,通用矩阵乘法算法的时间复杂度是阶数n的O(n^3).也就是对一个二阶矩阵,分解步骤中有8次乘法,非常耗时,造成矩阵解法时间常数项很大),但是实际上相同时间复杂度的算法,跑出来的效果也是不一样的,这边我不对基本的循环,递归,矩阵快速幂做介绍了,因为基本前面这些算法大家都看过了,而且跑出来的效果其实也没有我接下来介绍的几个方法好.

排名5:通项公式法(没跑出来…)

第一个需要放上的是通项公式法,我们直接利用斐波那契的通向公式:
在这里插入图片描述
,然后带入项数,计算简单粗暴.

import java.math.BigDecimal;
import java.math.BigInteger;

public class F5 {
    private static BigDecimal g5 = BigDecimal.valueOf(Math.sqrt(5));

    //通项公式法
    private static BigInteger f1(int x) {
        BigDecimal res =
                ((((BigDecimal.ONE.add(g5)).divide(BigDecimal.valueOf(2))).pow(x)).subtract(
                        ((BigDecimal.ONE.subtract(g5)).divide(BigDecimal.valueOf(2))).pow(x)
                )).divide(g5);
        return res.toBigInteger();
    }

    public static void main(String[] args) {
        int x = 1000000;
        long t1 = System.nanoTime();
        BigInteger result = f1(x);
        long t2 = System.nanoTime();
        System.out.println("通项公式法:");
        System.out.printf("result:%d%n", result.toString().length());
        System.out.printf("time:%.3fms%n", (t2 - t1) / 1e6);
    }
}

但是计算第100万项时耗费时间太长,等不下去了.列出这个只是为了证明,即使你知道通项公式,但是面对指数级的复杂度,依然难以计算这么大的项.

排名4:带缓存的递推公式法(222.404ms)

这边我们采用一个斐波那契数列的变形递推公式来进行计算,我们使用:
在这里插入图片描述
本质上这是一种二分递归算法,我们将项数分为偶数项和奇数项递归计算,然后将算出的前几项缓存起来,方便后边直接取用

import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;

public class F4 {
    //递推公式法
    //利用:
    // f(2n) = f(n)*f(n-1)+f(n)*f(n+1)
    // f(2n+1) = f(n)*f(n)+f(n+1)*f(n+1)
    private static BigInteger f3(int n) {
        return f3(n, new HashMap<>(100));
    }

    private static BigInteger f3(int n, Map<Integer, BigInteger> cache) {
        if (n == 0) return BigInteger.ZERO;
        if (n == 1 || n == 2) return BigInteger.ONE;
        BigInteger result = cache.get(n);
        if (result != null) {
            return result;
        }
        int m = n >>> 1;
        if ((n & 1) == 0) {//偶数
            //f(x/2+1)+f(x/2-1)
            BigInteger temp = f3(m + 1, cache).add(f3(m - 1, cache));
            //f(x/2)*f(x/2+1)+f(x/2-1)
            result = f3(m, cache).multiply(temp);
        } else {
            BigInteger fm = f3(m, cache);
            BigInteger fm1 = f3(m + 1, cache);
            result = fm.multiply(fm).add(fm1.multiply(fm1));
        }
        cache.put(n, result);
        return result;
    }

    public static void main(String[] args) {
        int x = 1000000;
        long t1 = System.nanoTime();
        BigInteger result = f3(x);
        long t2 = System.nanoTime();
        System.out.println("递推公式法递推公式法:");
        System.out.printf("result:%d%n", result.toString().length());
        System.out.printf("time:%.3fms%n", (t2 - t1) / 1e6);
    }
}

ps:本算法参考斐波拉契数列的第一百万项怎么用Java求

排名3:基于带缓存的Y组合子方法(220.954ms)

这个算法同样来自斐波拉契数列的第一百万项怎么用Java求

按照原作者的博客,上边的计算仅用了28ms!,称之为黑魔法,然而本人的破电脑跑了好多次,也只能跑出这个成绩,成绩之比排名4的快了一点点.我也没搞明白是哪里不对的…

import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

public class F3 {
    interface MetaFunc<T, R> extends Function<MetaFunc<T, R>, R> {
    }

    //基于带缓存的Y组合子方法
    static final class CachedY<T, R> implements Function<Function<Function<T, R>, Function<T, R>>, Function<T, R>> {
        private final Map<T, R> cache = new HashMap<>();

        @Override
        public Function<T, R> apply(Function<Function<T, R>, Function<T, R>> recursiveFunction) {
            MetaFunc<T, Function<T, R>> F =
                    (MetaFunc<T, Function<T, R>> x) ->
                            recursiveFunction.apply(
                                    (T y) -> {
                                        R r = cache.get(y);
                                        if (r != null) return r;
                                        r = x.apply(x).apply(y);
                                        cache.put(y, r);
                                        return r;
                                    });
            return F.apply(F);
        }
    }

    public static void main(String[] args) {
        Function<Function<Integer, BigInteger>, Function<Integer, BigInteger>> f4 =
                (Function<Integer, BigInteger> f) ->
                        (Integer n) -> {
                            if (n == 0) return BigInteger.ZERO;
                            if (n == 1 || n == 2) return BigInteger.ONE;

                            Integer m = n >>> 1;
                            //递推公式
                            if ((n & 1) == 0) {
                                BigInteger temp = f.apply(m + 1).add(f.apply(m - 1));
                                return f.apply(m).multiply(temp);
                            } else {
                                BigInteger fm = f.apply(m);
                                BigInteger fm1 = f.apply(m + 1);
                                return fm.multiply(fm).add(fm1.multiply(fm1));
                            }
                        };
        int x = 1000000;
        long t1 = System.nanoTime();
        BigInteger result = new CachedY<Integer, BigInteger>().apply(f4).apply(x);
        long t2 = System.nanoTime();
        System.out.println("基于带缓存的Y组合子方法:");
        System.out.printf("result:%d%n", result.toString().length());
        System.out.printf("time:%.3fms%n", (t2 - t1) / 1e6);
    }
}

排名2:改良版递推公式计算法(171.573ms)

这个算法出自知乎上原贴上面的答案,我浏览了所有答案后对比此回答的速度最快(当)原答案

这个算法也是利用了和排名4的方法一样的递推公式,但是经过了改良,速度比方法4快了近20%

import java.math.BigInteger;
import java.util.HashMap;

public class F2 {
    private static HashMap<Integer, BigInteger> c = new HashMap<>();

    static {
        c.put(0, BigInteger.ONE);
        c.put(1, BigInteger.ONE);
        c.put(2, BigInteger.ONE);
    }

    //改良版递推公式计算法
    //利用:
    // f(2n) = f(n)*f(n-1)+f(n)*f(n+1)
    // f(2n+1) = f(n)*f(n)+f(n+1)*f(n+1)
    private static BigInteger f2(int x) {
        if (!c.containsKey(x)) {
            if ((x & 1) == 0) {
                //f(n+1)=f(n)+f(n-1)
                c.put((x >> 1) + 1, f2((x >> 1) - 1).add(f2(x >> 1)));
                c.put(x, f2(x >> 1).multiply(f2((x >> 1) + 1).add(f2((x >> 1) - 1))));
            } else {
                c.put(x, f2(x >> 1).multiply(f2(x >> 1)).add(f2((x >> 1) + 1).multiply(f2((x >> 1) + 1))));
            }
        }
        return c.get(x);
    }

    public static void main(String[] args) {
        int x = 1000000;
        long t1 = System.nanoTime();
        BigInteger result = f2(x);
        long t2 = System.nanoTime();
        System.out.println("改良版递推公式计算法:");
        System.out.printf("result:%d%n", result.toString().length());
        System.out.printf("time:%.3fms%n", (t2 - t1) / 1e6);
    }

排名第1:二进制模幂解法(140.195ms!)

好吧,排名第一算法的确非常牛逼!计算100万项的速度仅仅只需要140.195ms!和我递归计算第10项的时间差不多!!!实在牛逼!目前为止全网我还没有找到比这个算法还快的!(仅适用JDK8自带的类)

该算法来自斐波那契数列与Python的尾递归蹦床 连载【6】

也是一种利用递推公式的改良算法,据说GMP和Mathematica内置算斐波那契的算法就是同样的算法,主要是利用2个递推公式:
JAVA计算斐波那契第100万项的最快算法排名汇总_第1张图片
在这里插入图片描述
我这边将算法翻译为java版本

import java.math.BigInteger;

public class F1 {
    //二进制模幂解法
    private static BigInteger f5(int n) {
        BigInteger[] add_on = {BigInteger.valueOf(2), BigInteger.valueOf(-2)};
        BigInteger prev_num = BigInteger.ONE;
        BigInteger current_num = BigInteger.ONE;
        String nb = Integer.toBinaryString(n);
        for (int i = 0; i < nb.length() - 1; i++) {
            if (nb.charAt(i) == '1') {
                //prev_num = F[2k] = F[2k+1] - F[2k-1],current_num就是等于F[2k+1]
                prev_num = current_num.subtract(prev_num);
            } else {
                //prev_num就是等于F[2k-1],current_num = F[2k] = F[2k+1] - F[2k-1]
                current_num = current_num.subtract(prev_num);
            }

            BigInteger sq_prev_num = prev_num.pow(2);
            BigInteger sq_current_num = current_num.pow(2);
            //F[2k-1] = F[k]^2 + F[k-1]^2
            prev_num = sq_prev_num.add(sq_current_num);
            //F[2k+1] = 4*F[k]^2 - F[k-1]^2 + 2*(-1)^k
            current_num = (sq_current_num.multiply(BigInteger.valueOf(4))).subtract(sq_prev_num)
                    .add(add_on[nb.charAt(i) == '1' ? 1 : 0]);
        }
        if ((n & 1) == 0) {
            current_num = current_num.subtract(prev_num);
        }
        return current_num;
    }

    public static void main(String[] args) {
        int x = 1000000;
        long t1 = System.nanoTime();
        BigInteger result = f5(x);
        long t2 = System.nanoTime();
        System.out.println("二进制模幂法:");
        System.out.printf("result:%d%n", result.toString().length());
        System.out.printf("time:%.3fms%n", (t2 - t1) / 1e6);
    }
}

那么,还有没有更快的方法呢?当然有啦!没有最快只有更快!

排名0:查表法(0ms)

最快的方法当然就是查表法拉,直接去别人计算好的数据库中一查,就有了,例如,打开WolframAlpha

在搜索框里输入:fibonacci(1000000) 回车,答案就出来了,理论上花费的时间几乎等于0.

JAVA计算斐波那契第100万项的最快算法排名汇总_第2张图片

若有大神有更加先进更加快的方法请留言告知哦,我会把它加入这个排名中去,让所有人膜拜大神的智慧!

你可能感兴趣的:(算法)