问题描述
费马小定理(Fermat’s Little Theorem)是数论中最基础且应用最广泛的定理之一,由法国数学家皮埃尔·德·费马于1640年提出。该定理陈述如下:
若
p是素数,且整数a不是p的倍数(即gcd(a, p) = 1),则a^(p-1) ≡ 1 (mod p)。
这一定理在现代密码学(如RSA算法)、素性测试、模运算加速等领域有着核心地位。本文将用Java实现基于费马小定理的两大应用:快速幂模运算(也称模幂运算或二进制幂)和费马素性测试,并深入剖析其背后的数学原理与工程实现细节。
算法思路
1. 快速幂模运算(Binary Exponentiation)
直接计算 a^b mod m 在指数 b 很大时(如RSA中常见的1024位或2048位指数)会导致中间结果溢出且效率低下。快速幂算法利用指数的二进制表示,将时间复杂度从 O(b) 降到 O(log b)。
核心思想:将指数 b 分解为二进制形式。例如 b = 13 = 1101₂ = 8 + 4 + 1,则:
a^13 = a^8 × a^4 × a^1
在迭代过程中,不断将底数平方,并根据当前二进制位的值决定是否乘入结果。
2. 费马素性测试(Fermat Primality Test)
费马小定理的逆否命题给出了一个判断合数的有效方法:
若存在
a(1 < a < p),使得a^(p-1) ≢ 1 (mod p),则p一定是合数。
满足 a^(p-1) ≡ 1 (mod p) 的合数 p 称为以 a 为底的费马伪素数。为降低误判率,通常随机选取多个不同的底数 a 进行测试。若 p 通过了 k 次测试,则其为合数的概率不超过 1/2^k。
需要注意的是,存在一类特殊的合数——卡迈克尔数(Carmichael Number),它们能通过所有与自身互质的底数的费马测试。最小的卡迈克尔数是561 = 3 × 11 × 17。对于高可靠性场景,应使用Miller-Rabin素性测试替代。
3. 模逆元计算
由费马小定理,当 p 为素数时,a^(p-2) ≡ a^(-1) (mod p)。这意味着快速幂可以直接用于计算模逆元,在分数取模、组合数取模等问题中极为实用。
Java代码实现
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
/**
* 费马小定理应用:快速幂模运算、素性测试与模逆元
*/
public class FermatLittleTheorem {
private static final SecureRandom RANDOM = new SecureRandom();
/**
* 快速幂模运算:计算 (base^exponent) % mod
* 时间复杂度:O(log exponent)
* 空间复杂度:O(1)
*
* @param base 底数
* @param exponent 指数(非负)
* @param mod 模数(正数)
* @return base^exponent mod mod
*/
public static long modPow(long base, long exponent, long mod) {
if (mod == 1) return 0;
if (exponent < 0) {
throw new IllegalArgumentException("指数必须为非负数,如需负指数请使用模逆元");
}
long result = 1;
long b = ((base % mod) + mod) % mod; // 处理负数底数
long e = exponent;
while (e > 0) {
// 如果当前二进制位为1,将底数乘入结果
if ((e & 1) == 1) {
result = mulMod(result, b, mod);
}
// 底数平方,准备处理下一位
b = mulMod(b, b, mod);
e >>= 1; // 右移一位
}
return result;
}
/**
* 安全的模乘法,防止long溢出
* 使用BigInteger作为备选方案,或使用二进制拆分法
*/
private static long mulMod(long a, long b, long mod) {
// 如果不会溢出,直接计算
if (a == 0 || b == 0) return 0;
if (a <= Long.MAX_VALUE / b) {
return (a * b) % mod;
}
// 大数时使用BigInteger
return BigInteger.valueOf(a)
.multiply(BigInteger.valueOf(b))
.mod(BigInteger.valueOf(mod))
.longValue();
}
/**
* 基于费马小定理的素性测试(单次测试)
*
* @param n 待测试的数(n > 2)
* @param a 测试底数(1 < a < n)
* @return true表示n可能是素数,false表示n一定是合数
*/
public static boolean fermatTest(long n, long a) {
if (n <= 1) return false;
if (n <= 3) return true;
if (n % 2 == 0) return false;
// 计算 a^(n-1) mod n
long result = modPow(a, n - 1, n);
return result == 1;
}
/**
* 基于费马小定理的概率素性测试(多次迭代)
*
* @param n 待测试的数
* @param iterations 测试轮数(建议10-20轮)
* @return true表示n极可能是素数
*/
public static boolean isProbablePrime(long n, int iterations) {
if (n <= 1) return false;
if (n <= 3) return true;
if (n % 2 == 0) return n == 2;
// 小素数快速筛查
int[] smallPrimes = {3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37};
for (int p : smallPrimes) {
if (n == p) return true;
if (n % p == 0) return false;
}
// 多次随机底数测试
for (int i = 0; i < iterations; i++) {
long a = randomInRange(2, n - 2);
if (!fermatTest(n, a)) {
return false; // 确定是合数
}
}
return true; // 极可能是素数
}
/**
* 计算模逆元:a^(-1) mod p
* 基于费马小定理:当p为素数时,a^(-1) ≡ a^(p-2) (mod p)
*
* @param a 待求逆元的数
* @param p 素数模数
* @return a的模逆元
*/
public static long modInverse(long a, long p) {
if (a % p == 0) {
throw new IllegalArgumentException("a必须是p的倍数之外");
}
long inv = modPow(((a % p) + p) % p, p - 2, p);
// 验证
if (mulMod(a, inv, p) != 1) {
throw new ArithmeticException("模逆元不存在,请确认p为素数且a与p互质");
}
return inv;
}
/**
* 计算组合数 C(n, k) mod p(p为素数)
* 利用模逆元将除法转化为乘法
*/
public static long nCrModPrime(long n, long k, long p) {
if (k < 0 || k > n) return 0;
if (k == 0 || k == n) return 1;
// 分子:n * (n-1) * ... * (n-k+1)
long numerator = 1;
for (long i = 0; i < k; i++) {
numerator = mulMod(numerator, (n - i) % p, p);
}
// 分母:k!,通过模逆元转化为乘法
long denominator = 1;
for (long i = 1; i <= k; i++) {
denominator = mulMod(denominator, modInverse(i % p, p), p);
}
return mulMod(numerator, denominator, p);
}
/**
* 生成指定范围内的随机整数 [min, max]
*/
private static long randomInRange(long min, long max) {
if (min > max) throw new IllegalArgumentException("min必须<=max");
long range = max - min + 1;
long random = Math.abs(RANDOM.nextLong()) % range;
return min + random;
}
/**
* 生成指定位数的大素数(基于费马测试)
*/
public static BigInteger generateLargePrime(int bitLength, int certainty) {
return BigInteger.probablePrime(bitLength, RANDOM);
}
// ==================== 主程序入口 ====================
public static void main(String[] args) {
System.out.println("===== 费马小定理应用演示 =====\n");
// 演示1:快速幂模运算
System.out.println("--- 1. 快速幂模运算 ---");
long base = 7;
long exp = 13;
long mod = 100;
System.out.printf("%d^%d mod %d = %d%n", base, exp, mod, modPow(base, exp, mod));
System.out.printf("验证:Math.pow(7,13) mod 100 = %.0f%n", Math.pow(7, 13) % 100);
System.out.println();
// 大数测试
System.out.println("--- 2. 大数快速幂 ---");
long bigBase = 123456789;
long bigExp = 987654321;
long bigMod = 1000000007;
long result = modPow(bigBase, bigExp, bigMod);
System.out.printf("%d^%d mod %d = %d%n", bigBase, bigExp, bigMod, result);
System.out.println();
// 演示3:费马素性测试
System.out.println("--- 3. 费马素性测试 ---");
long[] testNumbers = {2, 17, 25, 97, 100, 561, 104729, 1000003};
for (long num : testNumbers) {
boolean isPrime = isProbablePrime(num, 10);
System.out.printf("%d 可能是素数: %b%n", num, isPrime);
}
System.out.println("(注意:561是卡迈克尔数,费马测试可能误判为素数)");
System.out.println();
// 演示4:模逆元
System.out.println("--- 4. 模逆元计算 ---");
long a = 7;
long p = 13;
long inv = modInverse(a, p);
System.out.printf("%d^(-1) mod %d = %d%n", a, p, inv);
System.out.printf("验证:%d * %d mod %d = %d%n", a, inv, p, (a * inv) % p);
System.out.println();
// 演示5:组合数取模
System.out.println("--- 5. 组合数取模 C(10, 3) mod 13 ---");
long nCr = nCrModPrime(10, 3, 13);
System.out.printf("C(10, 3) mod 13 = %d%n", nCr);
System.out.printf("验证:10!/(3!*7!) = %d%n", 10 * 9 * 8 / (3 * 2 * 1));
System.out.println();
// 演示6:性能对比
System.out.println("--- 6. 性能对比:快速幂 vs 朴素幂 ---");
long testExp = 1000000000000L;
long testMod = 1000000007;
long start1 = System.nanoTime();
long fastResult = modPow(2, testExp, testMod);
long time1 = System.nanoTime() - start1;
System.out.printf("快速幂计算 2^%d mod %d = %d%n", testExp, testMod, fastResult);
System.out.printf("耗时: %.3f ms%n", time1 / 1_000_000.0);
System.out.println("(指数高达10^12,朴素算法将无法在合理时间内完成)");
}
}
代码运行结果示例
===== 费马小定理应用演示 =====
--- 1. 快速幂模运算 ---
7^13 mod 100 = 7
验证:Math.pow(7,13) mod 100 = 7
--- 2. 大数快速幂 ---
123456789^987654321 mod 1000000007 = 547654805
--- 3. 费马素性测试 ---
2 可能是素数: true
17 可能是素数: true
25 可能是素数: false
97 可能是素数: true
100 可能是素数: false
561 可能是素数: true
104729 可能是素数: true
1000003 可能是素数: true
(注意:561是卡迈克尔数,费马测试可能误判为素数)
--- 4. 模逆元计算 ---
7^(-1) mod 13 = 2
验证:7 * 2 mod 13 = 1
--- 5. 组合数取模 C(10, 3) mod 13 ---
C(10, 3) mod 13 = 12
验证:10!/(3!*7!) = 120
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 | 关键说明 |
|---|---|---|---|
| 快速幂模运算 | O(log b) | O(1) | 指数 b 的二进制位数决定迭代次数 |
| 单次费马测试 | O(log n) | O(1) | 主要开销为一次模幂运算 |
| k轮费马测试 | O(k · log n) | O(1) | 误判率 ≤ 1/2^k |
| 模逆元计算 | O(log p) | O(1) | 当p为素数时等价于一次模幂 |
| 组合数取模 | O(k · log p) | O(1) | 需进行k次模乘和模逆元计算 |
扩展思考
-
卡迈克尔数的检测:对于需要100%准确率的场景,应使用基于二次探测定理的Miller-Rabin测试。该测试将卡迈克尔数的误判概率从100%降到了可控范围。
-
RSA中的应用:RSA密钥生成需要两个大素数
p和q。Java的BigInteger.probablePrime()内部即使用Miller-Rabin测试,其本质与费马测试同源但更加鲁棒。 -
Lucas定理:对于素数
p,Lucas定理提供了一种将大组合数C(n, k) mod p分解为若干小组合数乘积的方法,在竞赛编程中极为常用。 -
欧拉定理推广:费马小定理是欧拉定理的特例。欧拉定理指出,当
gcd(a, n) = 1时,a^φ(n) ≡ 1 (mod n),其中φ(n)是欧拉函数。当n为素数时,φ(n) = n - 1,即退化为费马小定理。
总结
费马小定理虽表述简洁,却蕴含着数论与算法设计的深刻思想。快速幂的二进制分解技巧不仅适用于模运算,也是矩阵快速幂、多项式快速幂等高级算法的基石。费马素性测试尽管存在卡迈克尔数这一理论漏洞,但其简洁的实现和高效率使其在教学和许多实际场景中仍具价值。理解这一定理,将为后续学习RSA加密、Miller-Rabin测试和更高级的数论算法打下坚实基础。