1934 字
10 分钟
质数筛选算法详解

质数筛选算法详解#

学习目标: 掌握两种经典的质数筛选算法
难度等级: ⭐⭐⭐
应用场景: 大量质数判定、质因数分解、数论问题


目录#

  1. 问题背景
  2. 埃拉托斯特尼筛法(埃氏筛)
  3. 欧拉筛(线性筛)
  4. 性能对比
  5. 实战应用

问题背景#

什么是质数筛选?#

质数筛选是指:给定一个范围 [1, n],找出其中所有的质数。

为什么需要筛法?#

如果暴力判断每个数是否为质数:

// 暴力法:O(n√n)
bool isPrime(int x) {
if (x < 2) return false;
for (int i = 2; i * i <= x; i++) {
if (x % i == 0) return false;
}
return true;
}
// 筛选 1 到 n 的所有质数
for (int i = 1; i <= n; i++) {
if (isPrime(i)) {
// i 是质数
}
}

时间复杂度: O(n√n),当 n = 10^7 时非常慢!

筛法优势:

  • 埃氏筛: O(n log log n)
  • 欧拉筛: O(n) - 线性时间

埃拉托斯特尼筛法#

算法原理#

埃氏筛的核心思想:质数的倍数一定不是质数

步骤:

  1. 假设所有数都是质数
  2. 从 2 开始,标记 2 的所有倍数为合数(4, 6, 8, …)
  3. 找到下一个未被标记的数(3),标记它的所有倍数(9, 12, 15, …)
  4. 重复步骤 3,直到 √n

可视化过程 (n = 30):

初始: 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
标记2的倍数: 2 3 X 5 X 7 X 9 X 11 X 13 X 15 X 17 X 19 X 21 X 23 X 25 X 27 X 29 X
标记3的倍数: 2 3 X 5 X 7 X X X 11 X 13 X X X 17 X 19 X X X 23 X 25 X X X 29 X
标记5的倍数: 2 3 X 5 X 7 X X X 11 X 13 X X X 17 X 19 X X X 23 X X X X X 29 X
...
最终质数: 2 3 5 7 11 13 17 19 23 29

完整实现#

#include <iostream>
using namespace std;
const int MAXN = 10000000; // 一千万
bool isPrime[MAXN + 1];
void sieve() {
// 1. 初始化:假设所有数都是质数
for (int i = 2; i <= MAXN; i++) {
isPrime[i] = true;
}
// 2. 埃氏筛:标记合数
for (int i = 2; i * i <= MAXN; i++) {
if (isPrime[i]) {
// 标记 i 的所有倍数为合数
// 优化:从 i² 开始,因为 i×2, i×3, ..., i×(i-1)
// 已经被更小的质数标记过了
for (int j = i * i; j <= MAXN; j += i) {
isPrime[j] = false;
}
}
}
}
int main() {
sieve();
// 使用示例:查询质数
cout << "10 以内的质数: ";
for (int i = 2; i <= 10; i++) {
if (isPrime[i]) {
cout << i << " ";
}
}
// 输出: 2 3 5 7
return 0;
}

关键优化#

优化 1: 从 i² 开始标记#

// 不优化:从 i×2 开始
for (int j = i * 2; j <= MAXN; j += i)
// 优化:从 i² 开始
for (int j = i * i; j <= MAXN; j += i)

为什么?

  • 当处理质数 5 时:
    • 5×2=10 已被 2 标记
    • 5×3=15 已被 3 标记
    • 5×4=20 已被 2 标记
    • 5×5=25 才是第一个未被标记的

优化 2: 只循环到 √n#

for (int i = 2; i * i <= MAXN; i++)

为什么?

  • 如果 n 有大于 √n 的因子 a,必然有小于 √n 的因子 b = n/a
  • 所以只需检查 ≤ √n 的质数

时间复杂度分析#

理论复杂度: O(n log log n)

推导:

  • 标记 2 的倍数: n/2 次
  • 标记 3 的倍数: n/3 次
  • 标记 5 的倍数: n/5 次
  • 总次数: n(1/2 + 1/3 + 1/5 + 1/7 + …) ≈ n log log n

实际性能:

  • n = 10^6: 约 0.01 秒
  • n = 10^7: 约 0.1 秒
  • n = 10^8: 约 1 秒

欧拉筛#

为什么需要欧拉筛?#

埃氏筛的问题:重复标记

例如 30 = 2×15 = 3×10 = 5×6,会被标记 3 次!

欧拉筛的目标: 每个合数只被其最小质因子标记一次

算法原理#

核心思想: 用每个数的最小质因子来标记它

关键点:

  1. 维护一个质数表 primes[]
  2. 对每个数 i,用 i × primes[j] 标记合数
  3. i % primes[j] == 0 时停止,避免重复标记

标准实现#

#include <iostream>
#include <vector>
using namespace std;
const int MAXN = 10000000;
bool isPrime[MAXN + 1];
vector<int> primes; // 存储所有质数
void eulerSieve() {
// 1. 初始化
for (int i = 0; i <= MAXN; i++) {
isPrime[i] = true;
}
isPrime[0] = isPrime[1] = false;
// 2. 欧拉筛
for (int i = 2; i <= MAXN; i++) {
if (isPrime[i]) {
primes.push_back(i); // 记录质数
}
// 用已知质数标记合数
for (int j = 0; j < primes.size() && primes[j] * i <= MAXN; j++) {
isPrime[primes[j] * i] = false;
// 关键优化:避免重复标记
if (i % primes[j] == 0) break;
}
}
}
int main() {
eulerSieve();
cout << "前 10 个质数: ";
for (int i = 0; i < 10; i++) {
cout << primes[i] << " ";
}
// 输出: 2 3 5 7 11 13 17 19 23 29
return 0;
}

关键理解:为什么 i % primes[j] == 0 时要 break?#

例子: i = 6, primes = [2, 3, 5, …]

j = 0: primes[0] = 2
标记 6×2 = 12 (12 的最小质因子是 2) ✅
6 % 2 == 0, break!
// 如果不 break,会继续:
j = 1: primes[1] = 3
标记 6×3 = 18 (但 18 的最小质因子是 2,不是 3) ❌
这会导致重复标记!

原理:

  • i % primes[j] == 0 时,primes[j] 是 i 的最小质因子
  • 此时 i×primes[j+1] 的最小质因子是 primes[j],不是 primes[j+1]
  • 继续循环会导致错误标记

时间复杂度#

理论复杂度: O(n) - 线性时间

为什么是线性?

  • 每个合数只被标记一次
  • 标记次数 = 合数个数 = O(n)

性能对比#

代码对比#

特性埃氏筛欧拉筛
时间复杂度O(n log log n)O(n)
空间复杂度O(n)O(n)
代码难度简单中等
是否存储质数
重复标记

实际性能测试#

// 测试 n = 10^7 的运行时间
埃氏筛: 约 0.15
欧拉筛: 约 0.10
// 测试 n = 10^8 的运行时间
埃氏筛: 约 1.5
欧拉筛: 约 1.0

选择建议#

使用埃氏筛的情况:

  • ✅ 只需要判断质数(不需要存储)
  • ✅ n ≤ 10^7
  • ✅ 追求代码简洁

使用欧拉筛的情况:

  • ✅ 需要质数表
  • ✅ n ≥ 10^8
  • ✅ 追求极致性能

实战应用#

1. 统计质数个数#

int countPrimes(int n) {
sieve();
int count = 0;
for (int i = 2; i <= n; i++) {
if (isPrime[i]) count++;
}
return count;
}

2. 查询第 k 个质数#

int kthPrime(int k) {
eulerSieve(); // 使用欧拉筛,因为需要质数表
return primes[k - 1];
}

3. 区间质数查询#

vector<int> primesInRange(int L, int R) {
sieve();
vector<int> result;
for (int i = max(2, L); i <= R; i++) {
if (isPrime[i]) {
result.push_back(i);
}
}
return result;
}

常见错误与注意事项#

错误 1: 数组越界#

// 错误
const int MAXN = 1000000;
bool isPrime[MAXN];
for (int i = 2; i <= MAXN; i++) // 越界!
// 正确
const int MAXN = 1000000;
bool isPrime[MAXN + 1]; // 或者改为 i < MAXN

错误 2: 忘记初始化 0 和 1#

// 0 和 1 不是质数
isPrime[0] = isPrime[1] = false;

错误 3: 埃氏筛中从 i×2 开始#

// 低效
for (int j = i * 2; j <= MAXN; j += i)
// 高效
for (int j = i * i; j <= MAXN; j += i)

进阶技巧#

1. 区间筛(Segmented Sieve)#

用于筛选 [L, R] 范围内的质数,其中 R - L 很小但 R 很大:

// 例如:筛选 [10^12, 10^12 + 10^6] 的质数
// 先筛 [1, √R] 的质数,再用它们标记 [L, R]

2. 位压缩优化#

使用 bitset 或位运算减少空间:

#include <bitset>
bitset<MAXN> isPrime; // 比 bool 数组省 8 倍空间

记住这些#

“埃氏筛:用质数标记倍数"
"欧拉筛:每个合数只被最小质因子标记一次"
"n ≤ 10^7 用埃氏筛,n ≥ 10^8 用欧拉筛”


参考资源#


文档版本: v1.0
最后更新: 2025-11-04
建议练习: LeetCode 204. 计数质数

质数筛选算法详解
https://wuxie233.com/posts/prime-sieve/
作者
WuXie
发布于
2025-11-04
许可协议
CC BY-NC-SA 4.0