在数学的浩瀚宇宙中,质数一直是一个令人着迷的话题。它们像是数学世界中的原子,构成了所有自然数的基础。今天我们要探讨的是一个经典的算法问题:如何高效地找出指定范围内的所有质数。这个问题的解决方案就是著名的埃拉托斯特尼筛法(Sieve of Eratosthenes)。
历史背景
埃拉托斯特尼筛法由古希腊数学家埃拉托斯特尼在公元前240年左右提出,是最早的用于寻找质数的系统性方法之一。埃拉托斯特尼不仅是数学家,还是地理学家、天文学家和诗人,他甚至计算出了地球的周长,误差不到2%。
这个算法的基本思想非常直观:从2开始,将每个质数的倍数都标记为合数,剩下的未被标记的数就是质数。
问题描述
我们的任务是实现这样一个函数:
pub fn primes_up_to(upper_bound: u64) -> Vec<u64> {
unimplemented!("Construct a vector of all primes up to {}", upper_bound);
}
这个函数需要返回从2到给定上限之间的所有质数。
算法原理
埃拉托斯特尼筛法的核心思想是:
- 创建一个从2到n的连续整数列表
- 从列表中最小的数(2)开始
- 将该数的所有倍数(直到n)标记为合数
- 找到列表中下一个未标记的数,重复步骤3
- 重复这个过程直到处理完√n范围内的所有数
解决方案
让我们来实现这个经典的算法:
pub fn primes_up_to(upper_bound: u64) -> Vec<u64> {
// 处理边界情况
if upper_bound < 2 {
return vec![];
}
// 创建一个布尔向量,用来标记每个数是否为质数
// 索引i表示数字i,true表示是质数,false表示是合数
let mut is_prime = vec![true; (upper_bound + 1) as usize];
is_prime[0] = false; // 0不是质数
is_prime[1] = false; // 1不是质数
// 从2开始筛选
let limit = (upper_bound as f64).sqrt() as u64;
for num in 2..=limit {
if is_prime[num as usize] {
// 将num的所有倍数标记为合数
let mut multiple = num * num; // 从num²开始,因为小于num²的倍数已经被更小的质数标记过了
while multiple <= upper_bound {
is_prime[multiple as usize] = false;
multiple += num;
}
}
}
// 收集所有质数
let mut primes = Vec::new();
for i in 2..=upper_bound as usize {
if is_prime[i] {
primes.push(i as u64);
}
}
primes
}
测试案例详解
通过查看测试案例,我们可以更好地理解问题的各种情况:
#[test]
fn limit_lower_than_the_first_prime() {
assert_eq!(sieve::primes_up_to(1), []);
}
当上限小于最小质数2时,没有质数存在。
#[test]
fn limit_is_the_first_prime() {
assert_eq!(sieve::primes_up_to(2), [2]);
}
当上限正好是2时,只包含一个质数。
#[test]
fn primes_up_to_10() {
assert_eq!(sieve::primes_up_to(10), [2, 3, 5, 7]);
}
经典的例子,10以内的质数有4个。
#[test]
fn limit_is_prime() {
assert_eq!(sieve::primes_up_to(13), [2, 3, 5, 7, 11, 13]);
}
当上限本身是质数时,它也应该包含在结果中。
#[test]
fn limit_of_1000() {
let expected = vec![
2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89,
97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181,
191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281,
283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397,
401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503,
509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619,
631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743,
751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863,
877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997,
];
assert_eq!(sieve::primes_up_to(1000), expected);
}
大规模测试,验证算法的正确性和效率。
优化版本
上面的实现是标准的埃拉托斯特尼筛法,但我们还可以进行一些优化:
pub fn primes_up_to_optimized(upper_bound: u64) -> Vec<u64> {
if upper_bound < 2 {
return vec![];
}
if upper_bound == 2 {
return vec![2];
}
// 只处理奇数,因为除了2以外的所有偶数都不是质数
let size = ((upper_bound - 1) / 2) as usize;
let mut is_prime = vec![true; size];
let limit = ((upper_bound as f64).sqrt() as u64 - 1) / 2;
for i in 1..=limit as usize {
if is_prime[i] {
// 对应的奇数是 2*i + 1
let num = 2 * i + 1;
// 标记所有奇数倍数为合数
let mut j = i + num;
while j < size {
is_prime[j] = false;
j += num;
}
}
}
// 构建结果向量
let mut primes = vec![2]; // 2是唯一的偶数质数
for i in 1..size {
if is_prime[i] {
primes.push((2 * i + 1) as u64);
}
}
primes
}
这个优化版本只处理奇数,大大减少了内存使用和计算量。
Rust语言特性运用
在这个实现中,我们运用了多种Rust语言特性:
- 向量操作: 使用[vec!]宏创建和操作向量
- 类型转换: 在[u64]和[usize]之间进行安全转换
- 数学运算: 使用[sqrt]函数计算平方根
- 控制流: 使用[for]循环和[while]循环
- 内存安全: 利用Rust的所有权系统确保内存安全
算法复杂度分析
埃拉托斯特尼筛法的时间复杂度是O(n log log n),空间复杂度是O(n)。这使得它成为寻找大范围内质数的最有效算法之一。
相比试除法(对每个数测试所有小于其平方根的质数),筛法的优势在于:
- 一次性处理整个范围,而不是逐个检查
- 避免了重复计算
- 更好的缓存局部性
实际应用场景
质数在现代技术中有许多重要应用:
- 密码学: RSA加密算法依赖于大质数的性质
- 哈希表: 质数常被用作哈希表的大小
- 随机数生成: 某些伪随机数生成器使用质数
- 编码理论: 纠错码中使用质数
- 数论研究: 质数分布是数学研究的重要课题
扩展思考
除了埃拉托斯特尼筛法,还有其他筛法:
- 欧拉筛法: 线性时间复杂度O(n)的筛法
- 分段筛法: 用于处理超大范围的质数
- 轮筛法: 使用2,3,5等小质数进行预筛选
// 欧拉筛法示例
pub fn euler_sieve(upper_bound: usize) -> Vec<usize> {
if upper_bound < 2 {
return vec![];
}
let mut is_prime = vec![true; upper_bound + 1];
let mut primes = Vec::new();
is_prime[0] = false;
is_prime[1] = false;
for i in 2..=upper_bound {
if is_prime[i] {
primes.push(i);
}
for j in 0..primes.len() {
if i * primes[j] > upper_bound {
break;
}
is_prime[i * primes[j]] = false;
// 关键优化:避免重复筛选
if i % primes[j] == 0 {
break;
}
}
}
primes
}
总结
通过这个练习,我们学习到了:
- 经典算法的设计思想和实现方法
- 如何将数学概念转化为程序实现
- 算法优化的基本思路
- Rust在处理数值计算方面的优势
- 质数在计算机科学中的重要性
埃拉托斯特尼筛法不仅是一个高效的算法,更是古代数学智慧在现代计算机科学中的体现。通过实现这个算法,我们不仅能提高编程技能,还能感受到跨越千年的数学之美。
在实际应用中,根据具体需求选择合适的筛法实现非常重要。对于小范围的质数查找,标准筛法就足够了;而对于大规模应用,可能需要考虑更高级的优化技术。

被折叠的 条评论
为什么被折叠?



