素数(质数)是数论的基础,而高效地筛选素数是许多算法的基石。本文深入讲解两种经典的素数筛法——埃拉托斯特尼筛(埃氏筛)和欧拉筛(线性筛),分析它们的原理差异,并给出完整的Java实现与性能对比。
问题背景
素数定义:大于1的自然数,除了1和它本身外,不能被其他自然数整除。
素数筛问题:给定上限 $N$,找出 $[2, N]$ 范围内的所有素数。
朴素做法:对每个数 $i$,检查 $2$ 到 $\sqrt{i}$ 是否能整除它。时间复杂度 $O(N\sqrt{N})$,当 $N = 10^7$ 时速度极慢。
埃拉托斯特尼筛(埃氏筛)
核心思想
埃氏筛基于一个简单事实:如果 $p$ 是素数,那么 $2p, 3p, 4p, …$ 都是合数。
算法步骤:
1. 初始化一个布尔数组,假设所有数都是素数
2. 从2开始遍历,如果当前数 $i$ 是素数,则将 $i$ 的所有倍数标记为合数
3. 最终未被标记的数即为素数
时间复杂度分析
$$O(N \log \log N)$$
推导:标记操作的次数约为 $N/2 + N/3 + N/5 + N/7 + … = N \sum_{p \leq N} 1/p$,而素数倒数和收敛于 $\log \log N$。
Java实现
import java.util.*;
/**
* 埃拉托斯特尼筛(埃氏筛)
* 时间复杂度 O(N log log N),空间复杂度 O(N)
*/
public class SieveOfEratosthenes {
/**
* 基础埃氏筛:找出 [2, n] 范围内的所有素数
*/
public List<Integer> sieve(int n) {
if (n < 2) {
return new ArrayList<>();
}
boolean[] isComposite = new boolean[n + 1]; // false 表示素数,true 表示合数
List<Integer> primes = new ArrayList<>();
for (int i = 2; i <= n; i++) {
if (!isComposite[i]) {
// i 是素数
primes.add(i);
// 标记 i 的所有倍数为合数
// 注意:从 i*i 开始即可,因为更小的倍数已经被更小的素数标记过了
if ((long) i * i <= n) {
for (int j = i * i; j <= n; j += i) {
isComposite[j] = true;
}
}
}
}
return primes;
}
/**
* 优化的埃氏筛:仅处理奇数,节省一半空间
*/
public List<Integer> sieveOptimized(int n) {
if (n < 2) return new ArrayList<>();
List<Integer> primes = new ArrayList<>();
primes.add(2); // 2是唯一的偶素数
if (n < 3) return primes;
// 只存储奇数:索引 i 对应数字 2i+3
int size = (n - 1) / 2;
boolean[] isComposite = new boolean[size];
for (int i = 0; i < size; i++) {
if (!isComposite[i]) {
int prime = 2 * i + 3;
primes.add(prime);
// 从 prime*prime 开始标记,步长为 2*prime(只标记奇数倍数)
long start = (long) prime * prime;
if (start <= n) {
int startIdx = (int) ((start - 3) / 2);
for (int j = startIdx; j < size; j += prime) {
isComposite[j] = true;
}
}
}
}
return primes;
}
/**
* 扩展1:计算每个数的最小质因子(用于后续质因数分解)
*/
public int[] minPrimeFactor(int n) {
int[] mpf = new int[n + 1];
for (int i = 2; i <= n; i++) {
if (mpf[i] == 0) { // i 是素数
mpf[i] = i;
if ((long) i * i <= n) {
for (int j = i * i; j <= n; j += i) {
if (mpf[j] == 0) {
mpf[j] = i;
}
}
}
}
}
return mpf;
}
/**
* 扩展2:利用最小质因子数组进行质因数分解
*/
public List<int[]> primeFactorization(int x, int[] mpf) {
List<int[]> factors = new ArrayList<>();
while (x > 1) {
int p = mpf[x];
int count = 0;
while (x % p == 0) {
x /= p;
count++;
}
factors.add(new int[]{p, count});
}
return factors;
}
/**
* 扩展3:计算区间 [l, r] 内的素数(分段筛)
* 适用于 r 很大但 r-l 不大的情况
*/
public List<Integer> segmentedSieve(long l, long r) {
int limit = (int) Math.sqrt(r) + 1;
List<Integer> basePrimes = sieve(limit);
int segmentSize = (int) (r - l + 1);
boolean[] isComposite = new boolean[segmentSize];
for (int p : basePrimes) {
// 找到区间内 p 的第一个倍数
long start = Math.max((long) p * p, ((l + p - 1) / p) * p);
for (long j = start; j <= r; j += p) {
isComposite[(int) (j - l)] = true;
}
}
List<Integer> primes = new ArrayList<>();
for (int i = 0; i < segmentSize; i++) {
if (!isComposite[i] && (l + i) > 1) {
primes.add((int) (l + i));
}
}
return primes;
}
public static void main(String[] args) {
SieveOfEratosthenes sieve = new SieveOfEratosthenes();
int n = 100;
System.out.println("=== 埃氏筛(基础版) ===");
List<Integer> primes = sieve.sieve(n);
System.out.println("2~" + n + " 的素数: " + primes);
System.out.println("素数个数: " + primes.size());
System.out.println("\\n=== 埃氏筛(奇数优化版) ===");
List<Integer> primesOpt = sieve.sieveOptimized(n);
System.out.println("2~" + n + " 的素数: " + primesOpt);
// 质因数分解演示
System.out.println("\\n=== 质因数分解演示 ===");
int[] mpf = sieve.minPrimeFactor(100);
int[] testNumbers = {60, 84, 97, 100};
for (int num : testNumbers) {
List<int[]> factors = sieve.primeFactorization(num, mpf);
StringBuilder sb = new StringBuilder(num + " = ");
for (int i = 0; i < factors.size(); i++) {
int[] f = factors.get(i);
sb.append(f[0]).append("^").append(f[1]);
if (i < factors.size() - 1) sb.append(" * ");
}
System.out.println(" " + sb);
}
// 分段筛演示
System.out.println("\\n=== 分段筛演示 ===");
List<Integer> segPrimes = sieve.segmentedSieve(1000, 1050);
System.out.println("1000~1050 的素数: " + segPrimes);
}
}
欧拉筛(线性筛)
核心思想
埃氏筛的缺陷在于同一个合数会被多个素数重复标记。例如 30 = 2×15 = 3×10 = 5×6,会被标记3次。
欧拉筛的关键优化:每个合数只被它的最小质因子标记一次。
具体规则:
– 对于每个数 $i$,遍历已知素数表中的素数 $p$
– 标记 $i \times p$ 为合数
– 如果 $i$ 能被 $p$ 整除,则停止遍历(保证每个合数只被最小质因子标记)
时间复杂度:O(N),每个数仅被标记一次
Java实现
import java.util.*;
/**
* 欧拉筛(线性筛)
* 时间复杂度 O(N),空间复杂度 O(N)
* 每个合数仅被其最小质因子标记一次
*/
public class EulerSieve {
/**
* 线性筛:同时得到素数表和每个数的最小质因子
*/
public SieveResult linearSieve(int n) {
if (n < 2) {
return new SieveResult(new ArrayList<>(), new int[0]);
}
boolean[] isComposite = new boolean[n + 1];
int[] minPrime = new int[n + 1]; // 最小质因子
List<Integer> primes = new ArrayList<>();
for (int i = 2; i <= n; i++) {
if (!isComposite[i]) {
// i 是素数
primes.add(i);
minPrime[i] = i;
}
// 用当前已知的素数标记合数
for (int j = 0; j < primes.size() && i * primes.get(j) <= n; j++) {
int p = primes.get(j);
isComposite[i * p] = true;
minPrime[i * p] = p;
// 核心优化:如果 i 能被 p 整除,则 p 是 i 的最小质因子
// 此时 i*p 的最小质因子也是 p,应停止,避免重复标记
if (i % p == 0) {
break;
}
}
}
return new SieveResult(primes, minPrime);
}
static class SieveResult {
List<Integer> primes;
int[] minPrime;
SieveResult(List<Integer> primes, int[] minPrime) {
this.primes = primes;
this.minPrime = minPrime;
}
}
/**
* 利用线性筛的结果快速进行质因数分解
*/
public Map<Integer, Integer> factorize(int x, int[] minPrime) {
Map<Integer, Integer> factors = new LinkedHashMap<>();
while (x > 1) {
int p = minPrime[x];
factors.merge(p, 1, Integer::sum);
x /= p;
}
return factors;
}
/**
* 计算欧拉函数 phi(n):小于等于n且与n互质的正整数的个数
* 利用线性筛可以 O(N) 计算 1~N 的所有欧拉函数值
*/
public int[] eulerTotient(int n) {
int[] phi = new int[n + 1];
boolean[] isComposite = new boolean[n + 1];
List<Integer> primes = new ArrayList<>();
phi[1] = 1;
for (int i = 2; i <= n; i++) {
if (!isComposite[i]) {
primes.add(i);
phi[i] = i - 1; // 素数p的欧拉函数为 p-1
}
for (int j = 0; j < primes.size() && i * primes.get(j) <= n; j++) {
int p = primes.get(j);
isComposite[i * p] = true;
if (i % p == 0) {
// p 是 i 的质因子:phi(i*p) = phi(i) * p
phi[i * p] = phi[i] * p;
break;
} else {
// p 不是 i 的质因子:phi(i*p) = phi(i) * (p-1)
phi[i * p] = phi[i] * (p - 1);
}
}
}
return phi;
}
/**
* 计算莫比乌斯函数 mu(n)
* mu(1)=1,若n有平方因子则mu(n)=0,否则mu(n)=(-1)^k(k为不同质因子个数)
*/
public int[] mobiusFunction(int n) {
int[] mu = new int[n + 1];
boolean[] isComposite = new boolean[n + 1];
List<Integer> primes = new ArrayList<>();
mu[1] = 1;
for (int i = 2; i <= n; i++) {
if (!isComposite[i]) {
primes.add(i);
mu[i] = -1; // 素数有1个质因子
}
for (int j = 0; j < primes.size() && i * primes.get(j) <= n; j++) {
int p = primes.get(j);
isComposite[i * p] = true;
if (i % p == 0) {
// p^2 是 i*p 的因子
mu[i * p] = 0;
break;
} else {
mu[i * p] = -mu[i];
}
}
}
return mu;
}
public static void main(String[] args) {
EulerSieve sieve = new EulerSieve();
int n = 100;
System.out.println("=== 欧拉筛(线性筛) ===");
SieveResult result = sieve.linearSieve(n);
System.out.println("2~" + n + " 的素数: " + result.primes);
System.out.println("素数个数: " + result.primes.size());
// 质因数分解
System.out.println("\\n=== 快速质因数分解 ===");
int[] testNumbers = {60, 84, 100};
for (int num : testNumbers) {
Map<Integer, Integer> factors = sieve.factorize(num, result.minPrime);
System.out.println(" " + num + " = " + factors);
}
// 欧拉函数
System.out.println("\\n=== 欧拉函数 phi(n) ===");
int[] phi = sieve.eulerTotient(30);
System.out.print(" n: ");
for (int i = 1; i <= 30; i++) System.out.printf("%3d", i);
System.out.print("\\n phi: ");
for (int i = 1; i <= 30; i++) System.out.printf("%3d", phi[i]);
System.out.println();
// 莫比乌斯函数
System.out.println("\\n=== 莫比乌斯函数 mu(n) ===");
int[] mu = sieve.mobiusFunction(30);
System.out.print(" n: ");
for (int i = 1; i <= 30; i++) System.out.printf("%3d", i);
System.out.print("\\n mu: ");
for (int i = 1; i <= 30; i++) System.out.printf("%3d", mu[i]);
System.out.println();
}
}
两种筛法对比
| 特性 | 埃氏筛 | 欧拉筛(线性筛) |
|---|---|---|
| 时间复杂度 | O(N log log N) | O(N) |
| 空间复杂度 | O(N) | O(N) |
| 标记次数 | 一个合数可能被多次标记 | 每个合数只标记一次 |
| 代码复杂度 | 简单 | 稍复杂 |
| 扩展性 | 可扩展分段筛 | 可同时计算积性函数 |
| 适用场景 | N < 10^7 | N >= 10^7 或需计算积性函数 |
性能测试
public void benchmark() {
int[] testSizes = {100000, 1000000, 10000000};
for (int n : testSizes) {
System.out.println("\\nN = " + n);
long start1 = System.currentTimeMillis();
new SieveOfEratosthenes().sieve(n);
long time1 = System.currentTimeMillis() - start1;
System.out.println(" 埃氏筛: " + time1 + "ms");
long start2 = System.currentTimeMillis();
new EulerSieve().linearSieve(n);
long time2 = System.currentTimeMillis() - start2;
System.out.println(" 欧拉筛: " + time2 + "ms");
}
}
应用场景
素数筛是许多高级算法的基础:
– 密码学:RSA等加密算法依赖大素数
– 哈希表:选择素数作为表大小可减少冲突
– 数论问题:GCD、LCM、同余方程等
– 组合数学:与欧拉函数、莫比乌斯函数密切相关
掌握埃氏筛和欧拉筛,是深入数论算法的第一步。