当前位置: 首页 > news >正文

埃拉托斯特尼筛法(Sieve of Eratosthenes)——原理、复杂度与多种 C++ 实现

目录

1. 算法简介

2. 正确性与复杂度分析

3. 常见优化与变体

4. 经典实现(可读、适合教学)

5. 优化实现:只处理奇数 + 使用 vector

6. 高性能实现(位压缩 + 只处理奇数)

7. 分段筛(Segmented Sieve)

8. 应用与注意事项

9. 小结


本文目标:用通俗但严谨的语言介绍埃拉托斯特尼筛法的原理、时间与空间复杂度、常见优化手段以及若干实用的 C++ 实现(含注释与使用示例)。文章适合算法学习者、竞赛选手与需要在工程中高效生成素数表的开发者。


1. 算法简介

埃拉托斯特尼筛法是一种古老且高效的找出区间 [2, n] 内所有素数的方法。基本思想:

  1. 从最小的素数 2 开始,将其所有倍数(大于自身的)标记为合数。

  2. 找到下一个未被标记的数,将其视为素数,继续标记其倍数。

  3. 重复直到当前素数的平方大于 n(因为如果 p * p > n,那么对于任何 k >= p 的倍数 p*k 要么已经被比 p 小的素数标记,要么超出范围)。

因此,只需对 p 遍历到 sqrt(n)


2. 正确性与复杂度分析

  • 正确性:任何合数 m 都有一个不超过 sqrt(m) 的素因子,因此在遍历到小于等于 sqrt(n) 的素数时,m 会被某个素因子标记为合数。

  • 时间复杂度:经典的分析表明,埃筛的时间复杂度约为 O(n log log n)。直观上,对每个素数 p,我们标记大约 n/p 个数,所以总工作量是 n * (1/2 + 1/3 + 1/5 + 1/7 + ...),由素数倒数和约等于 log log n

  • 空间复杂度:需要 O(n) 的额外空间来存放标记(布尔数组或 bitset)。通过一些技巧(只处理奇数)可将空间减半。


3. 常见优化与变体

  1. 只存储奇数:除了 2 之外的所有偶数都不是素数。把原问题映射到表示奇数的索引上,可将空间与工作量大致减半。

  2. 使用位集合(bitset)或压缩存储:用位而不是 bool(或 char)来存储标记,能进一步节省内存并提升缓存效率。

  3. 跳过已知非必要的标记起点:当用奇数映射时,标记 p*p 开始的倍数,并以 2*p 为步长跳过偶数。

  4. 线性筛(Euler 筛):复杂度更好,能在 O(n) 时间内生成素数列表,但实现和常数因子不同。适合需要线性复杂度的场景(另外会产生每个数的最小质因子)。

  5. 分段筛(Segmented Sieve):当 n 很大(比如 n 无法一次性装入内存或 n 超过内存限制)时,分段处理区间 [L, R]。需要首先用常规埃筛得到 sqrt(n) 范围内的素数,然后用这些小素数在每个段上标记合数。常用于生成高达 10^12 或更大的素数区间。

  6. 轮子筛(Wheel Factorization):通过剔除 2、3、5 等小素数的倍数来减少标记次数,是更高级的常数级优化。


4. 经典实现(可读、适合教学)

// basic_sieve.cpp
// 使用 vector<bool> 的经典实现,适合 n 不超过 ~1e8(视机器内存而定)
#include <bits/stdc++.h>
using namespace std;vector<int> simple_sieve(int n) {if (n < 2) return {};vector<bool> is_prime(n + 1, true);is_prime[0] = is_prime[1] = false;int limit = floor(sqrt(n));for (int p = 2; p <= limit; ++p) {if (is_prime[p]) {for (long long m = 1LL * p * p; m <= n; m += p)is_prime[m] = false;}}vector<int> primes;for (int i = 2; i <= n; ++i)if (is_prime[i]) primes.push_back(i);return primes;
}int main() {int n = 100;auto primes = simple_sieve(n);for (int p : primes) cout << p << " ";cout << '\n';return 0;
}

说明vector<bool> 在 C++ 标准库中被特化为位容器,但其引用代理类型在某些情形下会带来小的性能负担;对于高性能应用,建议使用 std::vector<char>/std::string(每个元素一个字节)或手工位操作的 vector<uint64_t>


5. 优化实现:只处理奇数 + 使用 vector<char>

// odd_sieve.cpp
#include <bits/stdc++.h>
using namespace std;// 将序号 i (从0开始) 映射到实际数 2*i+1(只表示奇数)
// 我们不存储 2,单独处理。
vector<int> odd_sieve(int n) {if (n < 2) return {};vector<int> primes;primes.push_back(2);if (n == 2) return primes;int m = (n - 1) / 2; // 表示的奇数个数,例如 n=10 时 m=4 表示 1,3,5,7,9 共4个(但1不是素数)vector<char> is_composite(m, 0);int limit = (int)floor((sqrt(n) - 1) / 2);for (int i = 1; i <= limit; ++i) {if (!is_composite[i]) {int p = 2 * i + 1;// p*p 对应的索引:int start = (p * p - 1) / 2;for (int j = start; j < m; j += p) is_composite[j] = 1;}}for (int i = 1; i < m; ++i) if (!is_composite[i]) primes.push_back(2 * i + 1);return primes;
}int main() {int n = 100;auto primes = odd_sieve(n);for (int p : primes) cout << p << " ";cout << '\n';return 0;
}

说明:该实现将空间和一部分工作量降低约 2 倍,并且通常在实际运行中更快速、更节省内存。


6. 高性能实现(位压缩 + 只处理奇数)

下面给出将布尔数组压缩到 64 位块(uint64_t)中的实现。适合想在大 n 下优化内存与缓存利用的人。实现涉及位操作,代码更复杂,但性能较好。

// bit_sieve.cpp
#include <bits/stdc++.h>
using namespace std;
using u64 = unsigned long long;vector<int> bit_sieve(int n) {if (n < 2) return {};vector<int> primes;primes.push_back(2);int m = (n - 1) / 2; // 只表示奇数size_t blocks = (m + 63) / 64;vector<u64> bits(blocks, 0);int limit = (int)floor((sqrt(n) - 1) / 2);for (int i = 1; i <= limit; ++i) {if ( (bits[i >> 6] >> (i & 63)) & 1ULL ) continue; // 已标记int p = 2 * i + 1;int start = (p * p - 1) / 2;for (int j = start; j < m; j += p) bits[j >> 6] |= (1ULL << (j & 63));}for (int i = 1; i < m; ++i) if ( ((bits[i >> 6] >> (i & 63)) & 1ULL) == 0 ) primes.push_back(2 * i + 1);return primes;
}int main() {int n = 10000000; // 示例:生成 1e7 内的素数auto primes = bit_sieve(n);cout << "count = " << primes.size() << '\n';// 输出前 20 个做示例for (size_t i = 0; i < primes.size() && i < 20; ++i) cout << primes[i] << " ";cout << '\n';return 0;
}

7. 分段筛(Segmented Sieve)

n 很大时(例如 n 达到 10^12 或更高)无法一次性分配 O(n) 内存来做标准埃筛,这时使用分段筛:

  1. 先用简单的埃筛生成 sqrt(n) 以内的全部素数(这是内存可承受的,因为 sqrt(1e12)=1e6)。

  2. 将区间 [2, n] 划分为若干长度为 segment_size(例如 1e6)的小段 [L, R]

  3. 对每个小段,初始化一个布尔数组,再用先前获得的小素数标记该段内的合数(计算在段内每个小素数的第一个倍数的起点),之后收集该段中的素数并处理或输出。

分段筛的优点是只需要 O(segment_size) 的额外内存,并且可以在线输出素数、不需要一次性保存全部素数。

下面是一个简单的分段筛实现:

// segmented_sieve.cpp
#include <bits/stdc++.h>
using namespace std;vector<int> simple_sieve_for_limit(int limit) {vector<char> is_prime(limit + 1, 1);is_prime[0] = is_prime[1] = 0;for (int p = 2; p * p <= limit; ++p) if (is_prime[p]) {for (int q = p * p; q <= limit; q += p) is_prime[q] = 0;}vector<int> primes;for (int i = 2; i <= limit; ++i) if (is_prime[i]) primes.push_back(i);return primes;
}vector<int> segmented_sieve(long long n) {if (n < 2) return {};long long limit = floor(sqrt(n));vector<int> primes = simple_sieve_for_limit((int)limit);long long segment_size = max(limit, (long long)32768); // 选择段大小:至少 limit,适合缓存vector<int> result;for (long long low = 2; low <= n; low += segment_size) {long long high = min(n, low + segment_size - 1);vector<char> is_prime(high - low + 1, 1);for (int p : primes) {if (1LL * p * p > high) break;long long start = max(1LL * p * p, ((low + p - 1) / p) * 1LL * p);for (long long j = start; j <= high; j += p) is_prime[j - low] = 0;}for (long long i = low; i <= high; ++i) if (is_prime[i - low]) result.push_back((int)i);}return result;
}int main() {long long n = 1000000000LL; // 示例:生成到 1e9(实际可能较慢,视机器而定)auto primes = segmented_sieve(n);cout << "count = " << primes.size() << '\n';// 这里不输出全部素数return 0;
}

说明与建议:实际使用中,将段大小调整为适合 L1/L2 缓存的值(例如 32768 ~ 1e6)常能获得较好性能;对于极大 n,建议把分段的结果写到文件或逐段处理以避免内存占用过高。


8. 应用与注意事项

  • 常见应用:质因子分解的预处理(用素数表做 trial division)、寻找素数区间、生成测试数据、数论研究、密码学中的素数测试的预筛等。

  • 边界与陷阱

    • 使用 int 存储 p * p 可能溢出(当 p 接近 sqrt(INT_MAX) 时)。在循环中请用 long long 计算或检查溢出。

    • vector<bool> 的特殊化在并行或一些索引中会带来开销,必要时用 vector<char> 或手工位压缩替代。

    • n 很大时,注意内存和 IO 瓶颈,分段筛和按需输出是常见策略。


9. 小结

埃拉托斯特尼筛法是产生素数的经典方法。对于常见的范围(比如 n <= 1e8),经过适当优化(只处理奇数 + 位压缩)可以在常见竞赛与工程场景中高效运行。对于非常大的 n,分段筛能够把内存需求控制在合理范围,同时保持接近原始筛法的时间复杂度。

如果你需要我:

  • 把某个实现改成并行版本(例如用 OpenMP)来提高速度;

  • 提供可直接运行的测试用例或性能基准;

  • 把代码封装成可复用的类或库接口(例如 class Sieve { ... });

告诉我你的目标(例如 n 的典型大小、是否需要低内存实现、是否要并行化),我可以基于此做进一步优化或生成可编译的项目模板。


作者备注:本文中的示例代码均为自包含的 C++ 源文件,使用常见的 GCC/Clang 编译器(例如 g++ -O2 file.cpp -o file)即可编译运行。

http://www.dtcms.com/a/398819.html

相关文章:

  • 【大模型-金融】Trading-R1 多阶段课程学习
  • 建网站知乎怎么样上传网站资料
  • jupyter notebook 使用集锦(持续更新)
  • 部署开源PPTagent 生成工具
  • Python的大杀器:Jupyter Notebook处理.ipynb文件
  • 物流网站建设与管理规划书七牛wordpress插件
  • 【同源策略】跨域问题解决方法(多种)
  • 【数据结构】链表 --- 单链表
  • ArcGIS JSAPI 高级教程 - 自由绘制线段、多边形
  • 【2025最新】ArcGIS 点聚合功能实现全教程(进阶版)
  • Express使用教程(二)
  • 大模型部署基础设施搭建 - Docker
  • 芜湖建设机械网站企业管理系统软件下载
  • 永嘉县住房和城乡规划建设局网站自助贸易网
  • 华为云学习笔记(1):ECS 实例操作与密钥登录实践
  • 有一次django开发实录
  • RISC-V 中的 Wait For Interrupt 指令 (wfi) 详解
  • 前端核心框架vue之(指令案例篇1/5)
  • 企业静态网站源码增城建设局网站
  • 网站兼容9公司logo和商标一样吗
  • 题解:AT_abc206_e [ABC206E] Divide Both
  • 链改2.0总架构师何超秘书长重构“可信资产lPO与数链金融RWA”
  • 网站开发技术包括网站建设专业培训
  • 无人机航拍WiFi图传模块,16公里实时高清图传性能和技术参数
  • 视频元素在富文本编辑器中的光标问题
  • 企业网站内容如何搭建推荐做木工的视频网站
  • grounding dino 源码部署 cuda12.4 开放词汇目标检测(Open-Vocabulary Object Detection, OVOD)模型
  • 一个虚拟主机可以做几个网站吗毕设做网站心得体验
  • Spring使用SseEmitter实现后端流式传输和前端Vue数据接收
  • 湖南省新闻最新消息十条深圳seo网站推广方案