埃拉托斯特尼筛法用于一次列举上限 n 以下的所有素数,包含上限本身。这里用Rust输出不大于1000的168个素数。0和1不是素数。
筛法步骤与从平方开始的原因
- 要使用0到
n的下标,需要分配n + 1个布尔元素。基本版先初始化为真,再将0和1设为假。上限小于2时,在访问下标1之前返回空结果。 - 从2开始按递增顺序检查候选
p。若标记仍为真,p就是素数:从 \(p^2\) 开始,以p为步长将倍数设为假。不要将p本身设为假。 - 只需对满足 \(p^2\le n\) 的候选执行筛除,因为每个合数都有一个不大于其平方根的素因子。
- 最后扫描2到
n,输出仍标记为真的所有数。输出范围并不局限于平方根以内。
小于 \(p^2\) 的倍数 \(kp\)(\(2\le k<p\))已经被 k 的某个小于 p 的素因子筛除。这并不意味着所有较小的数都已经完成了列举。

图片:SKopp,Suricata更新版本,Wikimedia Commons,CC BY-SA 3.0。文件未经修改。图片的范围是2到120,而下方代码使用的上限是1000。
Rust基本实现
| |
保存为 sieve.rs,使用 rustc --edition=2021 sieve.rs -o sieve 编译。在Windows上,将可执行文件名指定为 sieve.exe。运行后输出168行,首个数为2,最后一个为997。这些示例已在Rust 1.90.0上运行。
sieve(0) 和 sieve(1) 返回空向量,sieve(2) 返回 [2]。条件 p <= n / p 确保只在平方不超过上限时计算 p * p。每次增加倍数之前还会检查剩余距离,不需要使用浮点平方根。
只筛除奇数候选的实现
大于2的偶数都是合数。因此可以只将2标记为真,并将奇数初始化为候选。对于奇素数 p,其平方也是奇数;从平方开始以 \(2p\) 为步长,就只会访问奇数倍数。
| |
将这一版保存到另一个文件并编译。它输出相同的168个素数,对0和1返回空结果,对2返回 [2]。不要把两个 main 函数放在同一个文件中。
这一版省略了部分偶数处理,但“初始化为假就一定更快”并无保证。初始化、写入、内存访问的开销,以及编译器和目标机器都会影响运行时间。这里没有进行计时比较。为使下标直接对应数值,数组仍有 n + 1 个元素。
复杂度与上限的适用范围
在通常的RAM模型下,筛法的时间复杂度为 \(O(n\log\log n)\),标记所需的存储空间为 \(O(n)\)。这是渐近的操作数和存储量估计,并非实测时间或任意精度整数的位复杂度。Princeton的算法分析教材中的筛法习题也给出了这一增长率。
Rust的 bool 占一个字节,Vec<T> 连续存储元素。这里的 Vec<bool> 不是位压缩数组。标记的逻辑元素个数为 n + 1,还需要向量的管理信息、分配容量以及返回的 Vec<usize> 所占的空间。
若无法表示 n + 1,checked_add 会返回 None,随后的 expect 会触发panic。其他情况下也必须有足够的可用内存,并不能处理任意大的上限。对于较大的区间,可以考虑分段筛。列举一个范围内的素数,与判断单个大整数是否为素数,适用场景不同。
