查找大量阶乘的快速方法

时间:2018-07-20 14:56:39

标签: java factorial

这是我的程序,但是对于10万这样的大数字,它的运行速度非常慢,是否有优化的选择?

import java.math.BigInteger;
import java.util.Scanner;

public class Main {

    public static void main(String[] args) {

        Scanner in = new Scanner(System.in);

        int n = in.nextInt();

        BigInteger sum = BigInteger.valueOf(1);

        for (BigInteger i = BigInteger.valueOf(n);
             i.compareTo(BigInteger.ZERO) > 0;
             i = i.subtract(BigInteger.ONE)) {

            sum = sum.multiply(i);
        }

        System.out.println(sum);    
    }

}

2 个答案:

答案 0 :(得分:2)

只是为了说明有时它需要操纵表达式,我修改了计算1 * 2 * 3 * ... * n的标准乘法循环,将其分为两部分:一部分将奇整数相乘(1 * 3 * 5 * ...),另一个将偶数相乘(2 * 4 * 6 * ...)。通过将0 mod 2但 not 0 mod 4(例如2 * 6 * 10 * ...)的偶数相乘,再将0 mod 4的偶数乘以 not 0 mod 8(例如4 * 12 * 20 * 28 * ...),依此类推,但是2的幂首先移出该数字。将两个的幂加起来,然后将乘积最后一次向左移。这利用了Java 8 BigInteger的实现方式,可以使较大的左移相当有效。

private static BigInteger fac4(int n) {

    BigInteger evens = multiplyEvens(n);
    BigInteger odds = multiplyOdds(n);
    BigInteger product = evens.multiply(odds);
    return product;
}

private static BigInteger multiplyOdds(int n) {
    BigInteger odds = BigInteger.ONE;
    for (long i=1; i<=n; i+=2) {
        odds = odds.multiply(BigInteger.valueOf(i));
    }
    return odds;
}

private static BigInteger multiplyEvens(int n) {
    BigInteger evens = BigInteger.ONE;
    long pow2 = 1;
    int shiftAmount = 0;
    while ((1 << pow2) <= n) {
        for (long i = (1<<pow2); i <= n; i += (1 << (pow2 + 1))) {
            shiftAmount += pow2;
            evens = evens.multiply(BigInteger.valueOf(i >> pow2));
        }
        ++pow2;
    }
    return evens.shiftLeft(shiftAmount);
}


public static void main(String[] args) {
    // Print out some small factorials to verify things are working
    for (int i = 0; i < 10; i++) {
        System.out.printf("%d! = %d%n", i, fac4(i));
    }

    Scanner in = new Scanner(System.in);

    int n = in.nextInt();
    long start = System.currentTimeMillis();
    BigInteger fac = fac4(n);
    long end = System.currentTimeMillis();
    float total = end - start;

    System.out.printf("%d! is %d bits long, took %f seconds to compute", n, fac.bitLength(), total / 1000);
}

这是一次n = 100000的输入/输出日志:

0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880
100000
100000! is 1516705 bits long, took 1.758000 seconds to compute

为了进行比较,我实现简单的多重循环大约需要3秒钟。

编辑:

这是我尝试过的另一个实现,它甚至更快。这个想法是利用Java 8+ BigInteger的渐近速度比O(n 2 )算法快的事实,而乘法的操作数变得足够大以提供优势。但是,幼稚的方法总是将单个“肢体”整数乘以快速增长的累积乘积。这种方法不适用于更快的算法。但是,如果我们乘以近似相等的操作数,则可以使用更快的算法。

private static final int SIMPLE_THRESHOLD = 10;
private static BigInteger fac6(int n) {
    return subfac(1, n);
}

/**
 * compute a * (a+1) * ... *(b-1) * b
 * The interval [a,b] includes the endpoints a and b.
 *
 * @param a the interval start.
 * @param b the interval end, inclusive.
 * @return the product.
 */
private static BigInteger subfac(int a, int b) {
    if ((b-a) < SIMPLE_THRESHOLD) {
        BigInteger result = BigInteger.ONE;
        for (int i=a; i<=b; i++) {
            result = result.multiply(BigInteger.valueOf(i));
        }
        return result;
    } else {
        int mid = a + (b-a) / 2;
        return subfac(a, mid).multiply(subfac(mid+1, b));
    }

}

使用与上述相同的main()方法的输出为:

0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880
100000
100000! is 1516705 bits long, took 0.243000 seconds to compute

因此fac6()fac4()快10倍。一些实验表明,SIMPLE_THRESHOLD的值对速度几乎没有影响,大概是因为函数调用的开销与BigInteger乘法的开销相形见

所有这些实验都是使用JDK 1.8.0_181在Mac OS X High Sierra笔记本电脑上运行的。

答案 1 :(得分:0)

这是我的第一个明显的实现:

public static void main(String[] args) {
        long start = System.currentTimeMillis();
        int n = 100000;
        BigInteger bigInteger = BigInteger.ONE;
        for (int i = 1; i < n; i++) {
            bigInteger = bigInteger.multiply(BigInteger.valueOf(i));
        }
        System.out.println(bigInteger);
        long end = System.currentTimeMillis();
        float total = end - start;
        System.out.println(total);
    }

100000的基准是一个456569位的数字(因此我在这里不能打印出来),我的解决方案大约需要3.5秒。

如果这对您来说不是不可能的,那么您必须设计一个基于多线程的解决方案。例如,一个线程将n的前一半相乘,而另一个线程则将后一半相乘。然后,将这两个数字相乘。