问题描述
素数(质数)是大于1的自然数中,除了1和它本身以外不再有其他因数的数。素数在密码学、哈希算法、随机数生成等领域有广泛应用。给定一个正整数 n,如何高效地找出所有小于等于 n 的素数?
埃拉托斯特尼筛法(Sieve of Eratosthenes)是最经典、最高效的素数生成算法之一,由古希腊数学家埃拉托斯特尼发明。
算法思路
基本思想
- 创建一个布尔数组
isPrime[0..n],初始时假设所有数都是素数 - 从第一个素数2开始,将2的所有倍数标记为合数
- 找到下一个未被标记的数,它也是素数,将其所有倍数标记为合数
- 重复步骤3,直到遍历到
√n
优化策略
- 只需筛到
√n:如果n是合数,它必有一个不大于√n的因数 - 从
i²开始标记:对于素数i,i×2, i×3, ..., i×(i-1)已被更小的素数标记过 - 跳过偶数:除了2以外,所有偶数都不是素数,可以只处理奇数
- 位图压缩:使用位运算压缩存储空间
Java实现
基础版筛法
import java.util.ArrayList;
import java.util.List;
public class SieveOfEratosthenes {
/**
* 基础版埃拉托斯特尼筛法
* 时间复杂度:O(n log log n)
* 空间复杂度:O(n)
*
* @param n 上限
* @return 所有小于等于n的素数列表
*/
public static List<Integer> sieveBasic(int n) {
if (n < 2) {
return new ArrayList<>();
}
boolean[] isComposite = new boolean[n + 1]; // false表示素数
List<Integer> primes = new ArrayList<>();
for (int i = 2; i <= n; i++) {
if (!isComposite[i]) {
primes.add(i);
// 将i的所有倍数标记为合数
// 优化:从i*i开始,因为i*2, i*3, ... i*(i-1)已被更小的素数标记
if ((long) i * i <= n) {
for (int j = i * i; j <= n; j += i) {
isComposite[j] = true;
}
}
}
}
return primes;
}
/**
* 优化版:只处理奇数,节省一半空间
*/
public static List<Integer> sieveOptimized(int n) {
if (n < 2) {
return new ArrayList<>();
}
List<Integer> primes = new ArrayList<>();
primes.add(2);
if (n < 3) {
return primes;
}
// 只存储奇数:索引i对应数字2*i+1
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开始标记,对应索引为(prime*prime-3)/2
long start = (long) prime * prime;
if (start <= n) {
for (long j = start; j <= n; j += 2 * prime) {
isComposite[(int) (j - 3) / 2] = true;
}
}
}
}
return primes;
}
/**
* 线性筛(欧拉筛):每个合数只被其最小质因数筛一次
* 时间复杂度:O(n)
*/
public static List<Integer> linearSieve(int n) {
if (n < 2) {
return new ArrayList<>();
}
boolean[] isComposite = new boolean[n + 1];
List<Integer> primes = new ArrayList<>();
for (int i = 2; i <= n; i++) {
if (!isComposite[i]) {
primes.add(i);
}
for (int p : primes) {
if ((long) i * p > n) {
break;
}
isComposite[i * p] = true;
if (i % p == 0) {
// p是i的最小质因数,保证每个合数只被筛一次
break;
}
}
}
return primes;
}
/**
* 判断单个数是否为素数(基于预处理的筛法结果)
*/
public static boolean isPrime(int n, boolean[] sieve) {
if (n < 2 || n >= sieve.length) {
return false;
}
return !sieve[n];
}
public static void main(String[] args) {
int n = 100;
System.out.println("基础筛法(n=" + n + "):" + sieveBasic(n));
System.out.println("优化筛法(n=" + n + "):" + sieveOptimized(n));
System.out.println("线性筛法(n=" + n + "):" + linearSieve(n));
// 性能测试
int testN = 10_000_000;
long start = System.currentTimeMillis();
List<Integer> largePrimes = sieveOptimized(testN);
long end = System.currentTimeMillis();
System.out.println("\n优化筛法生成 " + testN + " 以内素数:" + largePrimes.size() + " 个");
System.out.println("耗时:" + (end - start) + " ms");
}
}
扩展:素数计数函数与素性测试
public class PrimeUtils {
/**
* 快速判断大数是否为素数(确定性Miller-Rabin,适用于int范围)
*/
public static boolean isPrimeDeterministic(int n) {
if (n < 2) return false;
if (n == 2 || n == 3) return true;
if (n % 2 == 0) return false;
// 将n-1分解为d * 2^s
int d = n - 1;
int s = 0;
while (d % 2 == 0) {
d /= 2;
s++;
}
// 对于int范围,测试基{2, 7, 61}足够
int[] bases = {2, 7, 61};
for (int a : bases) {
if (a >= n) continue;
long x = modPow(a, d, n);
if (x == 1 || x == n - 1) continue;
boolean continueOuter = false;
for (int r = 1; r < s; r++) {
x = (x * x) % n;
if (x == n - 1) {
continueOuter = true;
break;
}
}
if (continueOuter) continue;
return false;
}
return true;
}
private static long modPow(long base, long exp, long mod) {
long result = 1;
base %= mod;
while (exp > 0) {
if ((exp & 1) == 1) {
result = (result * base) % mod;
}
base = (base * base) % mod;
exp >>= 1;
}
return result;
}
}
复杂度分析
| 版本 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|
| 基础筛法 | O(n log log n) | O(n) | 简洁易懂 |
| 奇数优化 | O(n log log n) | O(n/2) | 节省一半空间 |
| 线性筛 | O(n) | O(n) | 每个合数只筛一次,可求最小质因数 |
实际应用
- 密码学:RSA算法需要生成大素数,筛法用于快速排除合数候选
- 哈希表:素数大小的哈希表可以减少冲突
- 随机数生成:梅森旋转等算法需要素数参数
- 竞赛编程:快速预处理素数表,支持O(1)素性查询
总结
埃拉托斯特尼筛法是数论算法的基础,其核心思想是用”空间换时间”,通过预处理将素性判断从O(√n)降到O(1)。线性筛进一步优化到严格的O(n),是竞赛编程和工程实践中的常用技巧。理解筛法的原理,对掌握数论算法和优化思想至关重要。