1934 字
10 分钟
质数筛选算法详解
质数筛选算法详解
学习目标: 掌握两种经典的质数筛选算法
难度等级: ⭐⭐⭐
应用场景: 大量质数判定、质因数分解、数论问题
目录
问题背景
什么是质数筛选?
质数筛选是指:给定一个范围 [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) - 线性时间
埃拉托斯特尼筛法
算法原理
埃氏筛的核心思想:质数的倍数一定不是质数
步骤:
- 假设所有数都是质数
- 从 2 开始,标记 2 的所有倍数为合数(4, 6, 8, …)
- 找到下一个未被标记的数(3),标记它的所有倍数(9, 12, 15, …)
- 重复步骤 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 次!
欧拉筛的目标: 每个合数只被其最小质因子标记一次
算法原理
核心思想: 用每个数的最小质因子来标记它
关键点:
- 维护一个质数表
primes[] - 对每个数 i,用
i × primes[j]标记合数 - 当
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. 计数质数