喜欢与改变

导航

大数阶乘

求阶乘第一版

由于数组长度采用递归,数据太大内存不够。

package com.example.common.factorial;

/**
 * 求阶乘.
 * 基于jvm运行内存有限。大概12000以上的数字由于递归占用会有溢出错误。
 * 且本方法不考虑非法情况,默认用户正确输入正整数。
 * main()方法提供调用示例,并打印运行时间,运行结果与结果的位数。
 **/
public class Factorial0 {
    /**
     * 求{@code num}的阶乘
     *
     * @param num 要求阶乘的数字
     * @return 结算结果组成的字符串
     */
    public static String factorial(int num) {

        int[] data = new int[(int) StrictMath.ceil(capacity(num))];
        //目前数组有效数字的长度
        int size = 0;
        //进位
        int carryBit = 0;
        //如果阶乘数是1,直接插入
        data[size++] = 1;
        //1已经插入,从2开始运算,直到相乘到阶乘数
        for (int factor = 2; factor <= num; factor++) {
            //计算的结果倒序存储在数组中,每次从数组的第一位就是上次结果的末尾开始遍历与factor做乘法。
            //结果对进制(默认10)取余就是当前数组位数对应的结果,对进制取商就是进位的数。
            //在数组的下一位与factor做乘法时加上上次的进位在对进制取余,就是数组中下一位的对应的结果,对进制取商进位。
            for (int i = 0; i < size; i++) {
                int box = data[i] * factor + carryBit;
                data[i] = box % 10;
                carryBit = box / 10;
            }
            //对最后一个factor结果取余就是当前数组的最后一位的值,取商如果不为0,size增加,数组长度变长。
            while (carryBit != 0) {
                data[size++] = carryBit % 10;
                carryBit /= 10;
            }
        }
        //数字的有效位数为size,倒序组成String打印就是阶乘的结果.
        StringBuilder sb = new StringBuilder(size);
        for (int i = size - 1; i >= 0; i--) {
            sb.append(data[i]);
        }
        return sb.toString();
    }

    /**
     * 获取阶乘数的长度。
     * 原理:数字 x 的长度=以10为底x的对数
     * n*(n-1)*...*2*1的长度=log(n*(n-1)*...*2*1)=log(n)+log(n-1)+...+log(2)+log(1)
     * 方法用于创建数组,所以加长了一位。
     *
     * @param num 阶乘
     * @return 阶乘的实际长度+1
     */
    private static double capacity(int num) {
        return num == 1 ? 1 : StrictMath.log10(num) + capacity(num - 1);
    }

    public static void main(String[] args) {
        long l = System.currentTimeMillis();
        String factorial = factorial(10000);
        System.out.println(System.currentTimeMillis() - l);
        System.out.println(factorial);
        System.out.println(factorial.length());
    }
}

求阶乘优化版。

思想是通过采用较大的进制,减少数组的长度,降低二层循环中循环的次数,加快运行速度。

数组长度递归换for循环,递归太消耗内存,支持更大的数(也只是基本类型表达的数)。

进制10进制换成动态进制。数组存储10进制的数浪费空间,进制要满足数组中的任意数字与最大阶乘数相乘结果在数组的类型范围。

package com.example.common.factorial;

/**
 * 求阶乘.
 * main()方法提供调用示例,并打印运行时间,运行结果与结果的位数。
 **/
public class Factorial2 {
    private static final String ZERO = "0";

    /**
     * 求{@code num}的阶乘
     *
     * @param num 要求阶乘的数字
     * @return 结算结果组成的字符串
     */
    public static String factorial(int num) {

        long scale = getScale(num);
        int size = 0;
        long carryBit = 0L;
        double capacity = 0D;
        for (int i = 1; i <= num; i++) {
            capacity += StrictMath.log10(i);
        }
        //因为采用大进制,数组的长度=log以scale为底n*(n-1)...*2*1
        // 换底公式=log(n*(n-1)*...*2*1)/log(scale)
        capacity = capacity / StrictMath.log10(scale);
        long[] data = new long[(int) StrictMath.ceil(capacity) + 1];


        data[size++] = 1;
        for (int factor = 2; factor <= num; factor++) {
            for (int i = 0; i < size; i++) {
                long box = data[i] * factor + carryBit;
                data[i] = box % scale;
                carryBit = box / scale;
            }
            while (carryBit != 0) {
                data[size++] = carryBit % scale;
                carryBit /= scale;
            }
        }

        StringBuilder sb = new StringBuilder();
        for (int i = size - 1; i >= 0; i--) {
            for (int i1 = 0; i1 < leftZeroCount(data[i], scale); i1++) {
                sb.append(ZERO);
            }
            sb.append(data[i]);
        }
        return sb.toString();
    }

    /**
     * 获取最大的十进制的幂的进制。最小为10进制。
     * 因为最终表示为10进制的字符串,采用10的幂作为进制,可以直接按位组合。
     * 参考
     * 二进制  111111111111
     * 八进制  8888
     * 十六进制FFF
     *
     * @param num 阶乘数
     * @return 进制
     */
    private static long getScale(int num) {
        long pow = (long) StrictMath.pow(10, StrictMath.floor(StrictMath.log10(Long.MAX_VALUE) - StrictMath.log10(num)));
        return pow >= 10 ? pow : 10;
    }

    /**
     * 举例:
     * 10000进制(3999)(0)(9)
     * 10进制   9*(10000)+0*(10000e1)+3999*(10000e2)
     * =3999 0000 0009
     * 因为进制为10的幂次方,转为10进制的时候,只要在对应的位左补零拼接在一起就是10进制。
     *
     * @param num   阶乘数
     * @param scale 进制
     * @return 左边需要补零的个数
     */
    private static int leftZeroCount(long num, long scale) {
        //如果num=0,因为自己也占一位少补一个0.
        return num == 0 ? (int) StrictMath.log10(scale) - 1 : (int) (StrictMath.log10(scale) - StrictMath.ceil(StrictMath.log10(num)));
    }

    public static void main(String[] args) {
        int time = 1;
        long l = System.currentTimeMillis();
        String factorial = null;
        for (int i = 0; i < time; i++) {
            factorial = factorial(10000);
        }
        System.out.println(time + "次平均运行时间" + (System.currentTimeMillis() - l) / time + "ms");
        System.out.println(factorial);
    }

}

 

posted on 2020-08-18 10:41  喜欢与改变  阅读(210)  评论(0编辑  收藏  举报