JAVA:实现Factorial阶乘算法(附带源码)

项目背景详细介绍

阶乘(Factorial)函数是数学与计算中最基础也最常见的函数之一:对非负整数 nnn,阶乘定义为

并约定 0!=1。阶乘出现于组合数学(排列与组合)、概率论(多项式系数)、数论、分析(伽玛函数的离散形式)、物理学(统计力学)等诸多领域。对程序员而言,阶乘不仅是递归与迭代的经典教学例子,而且延伸出许多算法优化话题:溢出检测、大整数计算、并行/分治乘积(binary splitting)、流式生成、记忆化缓存、模运算(模阶乘)、性能基准与大规模计算策略等。

在 Java 生态中,计算阶乘既可以用原生 long(高效,但范围有限,最多安全到 20!)也可以用 BigInteger(精确,支持任意大,但开销显著)。高级场景还可用二分乘积(binary splitting)或分治并行化来在大整数场景下显著提升性能(常用于大整数阶乘或多精度库实现)。


项目需求详细介绍

  1. 代码功能要求

    • 原始递归实现(教学);

    • 迭代实现(常量空间);

    • 尾递归风格实现(说明 Java 不保证尾递归优化);

    • long 版本含溢出检测;

    • BigInteger 版本(迭代);

    • BigInteger 的分治乘积(binary splitting)实现(用于高性能大整数阶乘);

    • 基于 Java Stream 的实现(流式生成与聚合);

    • 带模(mod)版本(longBigInteger);

    • 记忆化 / 缓存版(在短时间内重复请求时有优势);

    • main 演示:正确性验证(小 n 与对照)、耗时对比(纳秒)、大 n 示例(BigInteger);

  2. 鲁棒性要求

    • 参数校验(n < 0 抛 IllegalArgumentException;mod <= 0 抛异常);

    • 对于 long 溢出需检测并抛出 ArithmeticException(或建议使用 BigInteger);

    • 注释说明哪些方法会修改参数或分配大量内存。


相关技术详细介绍

在实现与讲解中会涉及下列技术点,供课堂/博客时展开讲解:

  1. 递归与尾递归:用阶乘讲解递归函数调用与栈帧开销。Java 通常不做尾递归优化,因此尾递归形式在 Java 中并不能避免栈溢出;仍然适合教学尾递归概念。

  2. 迭代与常量空间:迭代方法简单高效、时间 O(n)、空间 O(1),通常是计算 longBigInteger 较小 n 的首选。

  3. 溢出检测:对 long 计算逐步检测乘法是否会溢出(可用 Math.multiplyExact 捕获 ArithmeticException 或按数值比较判断),并在溢出时提示使用 BigInteger 或带模运算。

  4. BigInteger 与性能问题java.math.BigInteger 提供无限精度整数运算,但乘法与内存分配开销大。对大 n(例如 n 数千或更多)应采用更高效的乘积策略(binary splitting / divide-and-conquer / prime swing / Borwein 等),以减少中间大整数乘法次数与位数增长带来的开销。

  5. 通过分治减少乘法次数对大整数乘法(高成本)的影响,常能获得显著加速,尤其与快速大整数乘法算法(Karatsuba / FFT-based)结合时效果更好。

  6. 流式(Streams)实现:使用 LongStreamStream<BigInteger> 展示函数式/流式表达,但注意流式聚合在大整数场景下仍受乘法开销限制,且流式方式需要合理选择并行/串行策略。

  7. 带模计算:在很多竞赛/工程场景只需 n! mod m,通过在每步做 % m 可以避免大整数,但当 m 非素时有特殊性质(0 的出现与周期)。对于非常大的 n 和模,需考虑阶乘在模下为 0 的阈值(若 m 有小质因子)。


实现思路详细介绍

总体上,我们将实现多种算法来满足教学与工程需求,并在 main 中进行对比验证:

  1. 递归实现(教学):简单实现 factRecursive(n),展示递归栈深为 n,时间 O(n)。用于演示,不用于生产大 n。

  2. 迭代实现(工程常用):实现 factIterativeLong(n)long,带溢出检测),并实现 factIterativeBigInteger(n)(BigInteger,迭代累乘)。

  3. 尾递归风格:实现 factTailRecursive(n)(调用辅助方法),并在注释中强调 Java 不保证尾递归消除,仍可能栈溢出。

  4. 分治乘积(binary splitting):实现 factBinarySplitBig(n),采用递归分治乘积策略计算 BigInteger 阶乘,能在大 n 下比单纯迭代快。此方法会对区间 [l, r] 分成两半递归,底层直接返回 ll*l+1 等。

  5. 流式实现:提供 factStreamLong(n)LongStream.rangeClosed(1, n).reduce(1, Math::multiplyExact),捕获溢出异常)与 factStreamBig(n)Stream.iterateLongStream mapToObj 到 BigInteger 然后 reduce)。

  6. 带模实现:提供 factModLong(n, mod)(在每步 % mod,同时在乘法前可检测是否之前为 0)与 factModBig(n, mod)BigInteger.mod),并校验 mod > 0

  7. 缓存 / 预计算:实现 FactorialCache,允许预计算并缓存 k! 到一定上限,之后快速返回或以分段乘积计算更大值。适合重复查询小到中等 n 的场景(如 web 服务)。

  8. 性能测试main 中对比多种实现:对小 n 检查一致性;对中等 n(例如 n=20)检查 long;对大 n(例如 n=1000、n=5000)比较 BigInteger 迭代与分治的耗时(提示真实基准应使用 JMH)。


完整实现代码

/* ============================================================
   文件: FactorialProject.java
   说明: 多种 Factorial(阶乘)算法实现(教学 + 工程)
     - long 版本(递归/迭代/尾递归/stream),包含溢出检测
     - BigInteger 版本(迭代/分治 binary-splitting/stream)
     - 带模(mod)版本(long / BigInteger)
     - 缓存/预计算模块
     - main() 演示正确性与简单性能对比(纳秒)
   编译: javac FactorialProject.java
   运行: java FactorialProject
   ============================================================ */

import java.math.BigInteger;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.LongStream;
import java.util.stream.Stream;

public class FactorialProject {

    // ============================
    // 文件: FactorialUtils.java
    // 说明: 包含各种阶乘实现与辅助函数
    // ============================
    public static class FactorialUtils {

        /* ----------------------------
           参数校验与辅助方法
           ---------------------------- */

        private static void requireNonNegative(long n) {
            if (n < 0) throw new IllegalArgumentException("n 必须为非负整数 (n >= 0)。");
        }

        private static void requirePositiveMod(long mod) {
            if (mod <= 0) throw new IllegalArgumentException("mod 必须为正整数 (mod > 0)。");
        }

        /**
         * 检查乘法 a * b 是否会导致 long 溢出(不抛异常,仅返回 boolean)
         */
        public static boolean willMultiplyOverflowLong(long a, long b) {
            if (a == 0 || b == 0) return false;
            if (a == Long.MIN_VALUE && b == -1) return true; // 特殊情形
            long absA = Math.abs(a), absB = Math.abs(b);
            return absA > Long.MAX_VALUE / absB;
        }

        /* ====================================================
           1) 简单递归实现(教学)
           - 时间: O(n), 空间: O(n) 递归栈
           - 返回 long,可能溢出(请谨慎)
           ==================================================== */
        public static long factRecursive(long n) {
            requireNonNegative(n);
            if (n == 0 || n == 1) return 1L;
            // 小心:对于较大的 n 可能出现 StackOverflowError 或 long 溢出
            return Math.multiplyExact(n, factRecursive(n - 1)); // Math.multiplyExact 会在溢出时抛 ArithmeticException
        }

        /* ====================================================
           2) 迭代实现(常用,常量空间)
           - 时间: O(n), 空间: O(1)
           - 带溢出检测:如果溢出抛出 ArithmeticException,提示使用 BigInteger
           ==================================================== */
        public static long factIterativeLong(long n) {
            requireNonNegative(n);
            if (n == 0L) return 1L;
            long res = 1L;
            for (long i = 2L; i <= n; i++) {
                // 使用 multiplyExact 捕获溢出
                res = Math.multiplyExact(res, i);
            }
            return res;
        }

        /* ====================================================
           3) 尾递归风格(教学)
           - Java 通常不做尾调用优化(Tail Call Elimination),因此对很大 n 仍会栈溢出。
           - 我们用辅助方法实现尾递归形式。
           ==================================================== */
        public static long factTailRecursive(long n) {
            requireNonNegative(n);
            return factTailHelper(n, 1L);
        }

        private static long factTailHelper(long n, long acc) {
            if (n == 0 || n == 1) return acc;
            // 仍然会在乘法处溢出或在递归深度很大时产生 StackOverflowError
            return factTailHelper(n - 1, Math.multiplyExact(acc, n));
        }

        /* ====================================================
           4) Stream (LongStream) 版本(long)
           - 使用 LongStream.rangeClosed 并 reduce
           - reduce 使用 Math.multiplyExact 会在溢出时抛出 ArithmeticException
           ==================================================== */
        public static long factStreamLong(long n) {
            requireNonNegative(n);
            if (n == 0L) return 1L;
            try {
                return LongStream.rangeClosed(1, n).reduce(1L, (a, b) -> Math.multiplyExact(a, b));
            } catch (ArithmeticException ae) {
                throw new ArithmeticException("long 溢出:请使用 BigInteger 或带模计算以处理更大的 n。");
            }
        }

        /* ====================================================
           5) BigInteger 迭代版本(精确)
           - 时间: O(n * M(k)) 其中 M(k) 是 k 位整数乘法的成本
           - 空间: O(1) 额外(BigInteger 本身占用随结果增长)
           - 适合中等 n 或当需要精确时使用
           ==================================================== */
        public static BigInteger factIterativeBig(long n) {
            requireNonNegative(n);
            BigInteger res = BigInteger.ONE;
            for (long i = 2L; i <= n; i++) {
                res = res.multiply(BigInteger.valueOf(i));
            }
            return res;
        }

        /* ====================================================
           6) BigInteger 分治乘积 (binary splitting)
           - 思路: 用分治计算区间乘积,减少高位整数乘法的位数膨胀带来的开销
           - 对大 n(几千到几万或更大)通常比简单迭代更快
           - 复杂度近似 O(M(k) log n) 的常数因子更优(依赖于底层大整数乘法)
           ==================================================== */

        /**
         * public API: 使用 binary split 计算 n!
         */
        public static BigInteger factBinarySplit(long n) {
            requireNonNegative(n);
            if (n <= 1L) return BigInteger.ONE;
            return productRange(BigInteger.ONE, 1L, n);
        }

        /**
         * 计算区间 [l, r] 的乘积(包含边界),返回 BigInteger
         * 若 l > r 返回 1
         */
        private static BigInteger productRange(BigInteger one, long l, long r) {
            if (l > r) return BigInteger.ONE;
            if (l == r) return BigInteger.valueOf(l);
            if (r - l == 1) return BigInteger.valueOf(l).multiply(BigInteger.valueOf(r));
            long m = (l + r) >>> 1;
            BigInteger left = productRange(one, l, m);
            BigInteger right = productRange(one, m + 1, r);
            return left.multiply(right);
        }

        /* ====================================================
           7) BigInteger Stream 版本(示例)
           - 使用 LongStream -> mapToObj(BigInteger::valueOf) -> reduce(BigInteger::multiply)
           - 性能不一定优于 factIterativeBig(主要用于风格演示)
           ==================================================== */
        public static BigInteger factStreamBig(long n) {
            requireNonNegative(n);
            if (n <= 1L) return BigInteger.ONE;
            return LongStream.rangeClosed(1, n)
                    .mapToObj(BigInteger::valueOf)
                    .reduce(BigInteger.ONE, BigInteger::multiply);
        }

        /* ====================================================
           8) 带模 (mod) 版本
           - long mod: 每步做 % mod(在 long 范围内安全,若 mod 约大仍需注意乘法溢出)
           - BigInteger mod: 使用 BigInteger.mod 在每步中取模
           ==================================================== */

        public static long factModLong(long n, long mod) {
            requireNonNegative(n);
            requirePositiveMod(mod);
            long res = 1L % mod;
            for (long i = 2L; i <= n; i++) {
                // 为避免 a * i 溢出再 %,可以先用 Math.multiplyExact 捕获溢出并用 BigInteger 回退
                // 这里直接使用 modular multiplication safe 方法:
                res = multiplyModLong(res, i % mod, mod);
            }
            return res;
        }

        // 安全的 (a * b) % mod,使用 long 算法避免溢出(循环加法在 mod 小或 b 小情形下足够)
        // 采用 Russian peasant multiplication(分治乘法)以避免溢出
        private static long multiplyModLong(long a, long b, long mod) {
            a %= mod; b %= mod;
            long result = 0;
            while (b > 0) {
                if ((b & 1L) == 1L) {
                    result = (result + a) % mod;
                }
                a = (a << 1) % mod;
                b >>= 1;
            }
            return result;
        }

        public static BigInteger factModBig(long n, BigInteger mod) {
            requireNonNegative(n);
            if (mod == null) throw new IllegalArgumentException("mod 不能为 null");
            if (mod.compareTo(BigInteger.ONE) <= 0) throw new IllegalArgumentException("mod 必须 > 0");
            BigInteger res = BigInteger.ONE.mod(mod);
            for (long i = 2L; i <= n; i++) {
                res = res.multiply(BigInteger.valueOf(i)).mod(mod);
            }
            return res;
        }

        /* ====================================================
           9) 缓存/预计算模块(适合重复查询小 n 的场景)
           - 支持预先计算 upTo 的所有阶乘并缓存
           - 查询大于 upTo 时可以快速分段乘积
           ==================================================== */
        public static class FactorialCache {
            private final List<BigInteger> cache; // cache.get(k) == k!
            private final long maxPrecomputed;    // 已预计算的上限

            /**
             * 构造并预计算 0..upTo 的阶乘
             */
            public FactorialCache(int upTo) {
                if (upTo < 0) throw new IllegalArgumentException("upTo 必须 >= 0");
                this.cache = new ArrayList<>(upTo + 1);
                BigInteger cur = BigInteger.ONE;
                cache.add(cur); // 0! = 1
                for (int i = 1; i <= upTo; i++) {
                    cur = cur.multiply(BigInteger.valueOf(i));
                    cache.add(cur);
                }
                this.maxPrecomputed = upTo;
            }

            /**
             * 获取 k!(若 k <= maxPrecomputed 直接返回缓存,否则用分治乘积或迭代计算)
             */
            public synchronized BigInteger getFactorial(long k) {
                if (k < 0) throw new IllegalArgumentException("k 必须 >= 0");
                if (k <= maxPrecomputed) return cache.get((int) k);
                // 使用缓存最后的值并接着计算
                BigInteger res = cache.get((int) maxPrecomputed);
                for (long i = maxPrecomputed + 1; i <= k; i++) {
                    res = res.multiply(BigInteger.valueOf(i));
                }
                return res;
            }
        }
    } // end FactorialUtils

    // ============================
    // 文件: DemoMain.java
    // 说明: main() 演示多种实现的正确性与简单性能对比(纳秒)
    // ============================
    public static void main(String[] args) {
        System.out.println("=== FactorialProject Demo ===");

        // 正确性检验(小 n)
        long smallN = 10;
        System.out.println("\n-- 正确性 (小 n = " + smallN + ") --");
        System.out.println("factRecursive: " + FactorialUtils.factRecursive(smallN));
        System.out.println("factIterativeLong: " + FactorialUtils.factIterativeLong(smallN));
        System.out.println("factTailRecursive: " + FactorialUtils.factTailRecursive(smallN));
        System.out.println("factStreamLong: " + FactorialUtils.factStreamLong(smallN));
        System.out.println("factIterativeBig: " + FactorialUtils.factIterativeBig(smallN));
        System.out.println("factBinarySplitBig: " + FactorialUtils.factBinarySplit(smallN));
        System.out.println("factStreamBig: " + FactorialUtils.factStreamBig(smallN));

        // long 溢出演示(20! 在 long 内,21! 会溢出)
        System.out.println("\n-- long 溢出示例 --");
        try {
            System.out.println("20! = " + FactorialUtils.factIterativeLong(20L));
            System.out.println("21! = " + FactorialUtils.factIterativeLong(21L)); // 预计抛出 ArithmeticException
        } catch (ArithmeticException ae) {
            System.out.println("检测到 long 溢出: " + ae.getMessage());
        }

        // BigInteger 大 n 示例与性能对比
        int nMedium = 1000;
        System.out.println("\n-- BigInteger 性能对比 (n = " + nMedium + ") --");
        long t0 = System.nanoTime();
        BigInteger v1 = FactorialUtils.factIterativeBig(nMedium);
        long t1 = System.nanoTime();
        BigInteger v2 = FactorialUtils.factBinarySplit(nMedium);
        long t2 = System.nanoTime();
        System.out.println("迭代 BigInteger: digits=" + v1.toString().length() + ", time(ns)=" + (t1 - t0));
        System.out.println("分治 BigInteger: digits=" + v2.toString().length() + ", time(ns)=" + (t2 - t1));
        System.out.println("一致性校验: iterative.equals(binarySplit)? " + v1.equals(v2));

        // 带模示例
        System.out.println("\n-- 带模示例 --");
        long mod = 1_000_000_007L;
        long nMod = 100000L; // 注意:这个 n 对于逐步模乘可能耗时,但演示用
        System.out.println("10! mod " + mod + " = " + FactorialUtils.factModLong(10L, mod));
        System.out.println("100! mod " + mod + " = " + FactorialUtils.factModLong(100L, mod));

        // 缓存/预计算示例
        System.out.println("\n-- 缓存示例 --");
        FactorialUtils.FactorialCache cache = new FactorialUtils.FactorialCache(20);
        System.out.println("15! from cache = " + cache.getFactorial(15));
        System.out.println("25! from cache (computed by continuing) length digits = " + cache.getFactorial(25).toString().length());

        // 流式并行示例(演示但不推荐用于大整数乘积)
        System.out.println("\n-- Stream 并行示例(仅示范) --");
        long nPar = 20;
        BigInteger parRes = LongStream.rangeClosed(1, nPar).parallel()
                .mapToObj(BigInteger::valueOf)
                .reduce(BigInteger.ONE, BigInteger::multiply);
        System.out.println("parallel stream factorial (n=20) = " + parRes);

        System.out.println("\n=== Demo End ===");
    }
}

代码详细解读

  • FactorialUtils.willMultiplyOverflowLong(long a, long b)
    作用:检测 a * b 是否会导致 long 溢出(布尔返回),用于在做原生 long 乘法前做预检。

  • FactorialUtils.factRecursive(long n)
    作用:朴素递归实现阶乘(教学用),对 n 做递归调用并使用 Math.multiplyExact 在溢出时抛出 ArithmeticException

  • FactorialUtils.factIterativeLong(long n)
    作用:迭代实现 long 版本阶乘(循环累乘),使用 Math.multiplyExact 捕获并报告溢出。推荐用于 nlong 范围内且不频繁调用的大多数工程场景。

  • FactorialUtils.factTailRecursive(long n)
    作用:尾递归风格实现(辅助累积参数),用于教学尾递归概念并说明 Java 不保证尾递归优化。

  • FactorialUtils.factStreamLong(long n)
    作用:使用 LongStream 的函数式流式实现阶乘,内部使用 Math.multiplyExact 以检测溢出。

  • FactorialUtils.factIterativeBig(long n)
    作用:使用 BigInteger 的迭代实现阶乘,精确但开销较大,适合需要完整精确结果的场景。

  • FactorialUtils.factBinarySplit(long n)(及私有 productRange
    作用:使用分治乘积(binary splitting)计算阶乘,通过分治区间乘积减少大整数乘法的昂贵成本,对大 n 性能更优。

  • FactorialUtils.factStreamBig(long n)
    作用:使用流式(Stream)配合 BigInteger 来计算阶乘,主要用于演示函数式写法。

  • FactorialUtils.factModLong(long n, long mod)
    作用:计算 n! mod mod,使用安全的乘法模实现(避免 long 直接溢出),适合只关心模值的场景。

  • FactorialUtils.factModBig(long n, BigInteger mod)
    作用:使用 BigInteger 计算 n! mod mod

  • FactorialUtils.FactorialCache(类)
    作用:提供简单的预计算与缓存机制,预先计算并缓存 0..upTo 的阶乘,后续查询在缓存范围内直接返回,超出则基于缓存值接续计算。适合重复查询中小 n 的场景。


项目详细总结

  1. 方法对比

    • 对于小 n 且需要最高性能:factIterativeLong 是首选(前提是 nlong 可承载范围内)。

    • n 导致 long 溢出或需要精确值:使用 BigInteger 版本。factIterativeBig 在中等 n 表现良好,但对于非常大的 n(几千至几万甚至更大)推荐使用 factBinarySplit(分治乘积),因为它在大整数乘法方面更有效率。

    • 流式实现适合教学与声明式编码风格,但通常有额外开销;并行流对大整数乘法的好处并不一定明显,实际并行效率应以 JMH 基准评估。

    • 带模运算在竞赛与工程中的常见用例应优先使用(节省内存且可避免大整数)。

  2. 工程建议

    • 如果你编写的是库或服务,务必在 API 文档中明确:哪些方法会抛 ArithmeticException、哪些方法会修改缓存或消耗大量内存、以及建议使用 BigInteger 的阈值(例如当 n >= 21 推荐使用 BigInteger,因为 21! 超出 long)。

    • 对性能敏感的任务:在 JVM 上做基准(JMH),避免微基准误导,考虑内存池复用 BigInteger 中间结果不现实但可以复用数组 / 缓冲策略。


项目常见问题及解答

Q1:long 能支持最大的 n 是多少?
A1:20! = 2_432_902_008_176_640_000long 范围内;21! 超出 Long.MAX_VALUE。因此若 n >= 21,请使用 BigInteger 或带模运算。

Q2:为什么要用 binary splitting?迭代不够快吗?
A2:在大整数情形下,随阶乘增长位数快速增加,普通迭代会在早期就产生非常大的中间数,随之引发高昂的乘法成本。分治乘积能让乘法的操作数规模更均衡,减少中间乘法对大位数数值的反复膨胀,从而在实际中显著提升性能,尤其当底层乘法采用 Karatsuba 或 FFT-based 算法时效果更明显。

Q3:流(Stream)并行化能提升阶乘性能吗?
A3:并不总是。对于 BigInteger 乘法,单纯把 1..n 分成多段并行乘再合并,合并步骤仍需做大整数乘法,而且并行会引起额外的上下文切换和内存带宽压力。是否能提升需要基准测试(JMH)。通常推荐用 binary splitting 的分治本身易于并行化(在合适的实现中可能优于直接并行 stream)。

Q4:如何计算 n! mod m 当 m 很大或 n 很大?
A4:如果只关心模值,每步对乘积做 % m 即可。但若 m 非素数或含有小质因子,并且 n >= 某个临界值,则 n! mod m 可能为 0(因为 n! 包含 m 的质因子)。在特殊竞赛问题中可能需要用逆元、分解质因子或 Lucas / CRT 等方法做进一步优化。

Q5:如何处理极大的 n(例如 n = 10^6 或更多)?
A5:直接计算完整 n! 的十进制表示在存储与时间上通常不可行(结果位数非常巨大)。如果确实需要完整值,通常需要专业的高性能多精度库与分布式计算资源,或者采用数论方法(若仅需部分位或模),例如计算阶乘尾数、阶乘的位数、或阶乘的低位/高位,使用专门算法与技巧(如分段乘法、FFT-based 大整数乘法、外部存储分块等)。


扩展方向与性能优化建议

  1. 引入专业大整数乘法与并行化

    • 对于极大 n,将 BigInteger 的乘法替换为高性能本地实现(使用 JNI 调用 GMP / FFTW / libgmp)并并行化分治计算,可显著加速。

  2. JMH 基准与性能回归测试

    • 使用 JMH 对不同实现(迭代、binary split、流式、并行分治)做严谨基准,以确定在你的 JVM 与硬件上最优策略,并为未来代码改动做性能回归检测。

  3. 内存 / 对象分配优化

    • 在 Java 中频繁创建 BigInteger 可能触发大量垃圾回收。可在高频场景中复用中间结果池、减少短寿命对象或使用原始字节数组与自定义大整数实现以降低 GC 压力(但会增加实现复杂度)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值