Rust 练习册 102:埃拉托斯特尼筛法与质数探索

在数学的浩瀚宇宙中,质数一直是一个令人着迷的话题。它们像是数学世界中的原子,构成了所有自然数的基础。今天我们要探讨的是一个经典的算法问题:如何高效地找出指定范围内的所有质数。这个问题的解决方案就是著名的埃拉托斯特尼筛法(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到给定上限之间的所有质数。

算法原理

埃拉托斯特尼筛法的核心思想是:

  1. 创建一个从2到n的连续整数列表
  2. 从列表中最小的数(2)开始
  3. 将该数的所有倍数(直到n)标记为合数
  4. 找到列表中下一个未标记的数,重复步骤3
  5. 重复这个过程直到处理完√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语言特性:

  1. 向量操作: 使用[vec!]宏创建和操作向量
  2. 类型转换: 在[u64]和[usize]之间进行安全转换
  3. 数学运算: 使用[sqrt]函数计算平方根
  4. 控制流: 使用[for]循环和[while]循环
  5. 内存安全: 利用Rust的所有权系统确保内存安全

算法复杂度分析

埃拉托斯特尼筛法的时间复杂度是O(n log log n),空间复杂度是O(n)。这使得它成为寻找大范围内质数的最有效算法之一。

相比试除法(对每个数测试所有小于其平方根的质数),筛法的优势在于:

  1. 一次性处理整个范围,而不是逐个检查
  2. 避免了重复计算
  3. 更好的缓存局部性

实际应用场景

质数在现代技术中有许多重要应用:

  1. 密码学: RSA加密算法依赖于大质数的性质
  2. 哈希表: 质数常被用作哈希表的大小
  3. 随机数生成: 某些伪随机数生成器使用质数
  4. 编码理论: 纠错码中使用质数
  5. 数论研究: 质数分布是数学研究的重要课题

扩展思考

除了埃拉托斯特尼筛法,还有其他筛法:

  1. 欧拉筛法: 线性时间复杂度O(n)的筛法
  2. 分段筛法: 用于处理超大范围的质数
  3. 轮筛法: 使用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
}

总结

通过这个练习,我们学习到了:

  1. 经典算法的设计思想和实现方法
  2. 如何将数学概念转化为程序实现
  3. 算法优化的基本思路
  4. Rust在处理数值计算方面的优势
  5. 质数在计算机科学中的重要性

埃拉托斯特尼筛法不仅是一个高效的算法,更是古代数学智慧在现代计算机科学中的体现。通过实现这个算法,我们不仅能提高编程技能,还能感受到跨越千年的数学之美。

在实际应用中,根据具体需求选择合适的筛法实现非常重要。对于小范围的质数查找,标准筛法就足够了;而对于大规模应用,可能需要考虑更高级的优化技术。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

少湖说

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值