Rust面试宝典第5题:判断素数

330 阅读4分钟

题目

判断一个正整数是否为素数有哪几种方法,每种方法的时间复杂度怎么样。

解析

素数又称质数,是指在大于1的自然数中,除了1和它本身以外,不再有其他因数的自然数。素数只有1和它本身两个正因数,最小的素数是2,素数的个数是无穷的。

埃拉托斯特尼筛法(Sieve of Eratosthenes),简称“筛法”,是一种简单判定素数的算法,也是一种历史悠久的筛法。要得到自然数n以内的全部素数,必须把不大于根号n的所有素数的倍数剔除,剩下的就是素数。其方法是,给出要筛数值的范围n,先用2去筛,把2留下,把2的倍数剔除掉;再用下一个素数,也就是3筛,把3留下,把3的倍数剔除掉;不断重复下去。

根据上面的思路,我们给出了下面的示例代码。

fn is_prime(num: u32) -> bool {
    if num <= 1 {
        return false;
    }

    let mut primes = vec![true; (num + 1) as usize];
    primes[0] = false;
    primes[1] = false;
    let sqrt_num = (num as f64).sqrt() as u32;
    for i in 2..=sqrt_num {
        if primes[i as usize] {
            let mut j = i * i;
            while j <= num {
                primes[j as usize] = false;
                j += i;
            }
        }
    }

    primes[num as usize]
}

fn main() {
    for i in 0..100 {
        if is_prime(i) {
            println!("{}", i);
        }
    }
}

通过上面的代码可以得知,埃拉托斯特尼筛法的时间复杂度为O(nlogn)。

判断一个数是否为素数,最直接也最常用的方法为:循环遍历从2到该数的平方根之间的所有整数,尝试能否整除该数。如果存在能整除该数的整数,则该数不是素数;否则,该数是素数。这种方法的时间复杂度为O(sqrt(n)),具体实现,可参考下面的示例代码。

use std::time::{Duration, Instant};

fn is_prime(num: u32) -> bool {
    if num <= 1 {
        return false;
    }

    let sqrt_num = (num as f64).sqrt() as u32;
    for i in 2..=sqrt_num {
        if num % i == 0 {
            return false;
        }
    }

    true
}

fn main() {
    let start = Instant::now();
    for i in 0..100 {
        if is_prime(i) {
            println!("{}", i);
        }
    }

    let duration = start.elapsed();
    println!("{}ms", duration.as_millis());
}

还有一种更高效的方法,需要用到素数分布的规律。该规律为:大于等于5的素数,一定和6的倍数相邻。比如:素数5、7在6的两侧,素数11、13在12的两侧,素数17、19在18的两侧。

该分布规律的证明其实并不难,假设有一个数x>=1,则可将大于等于5的自然数N表示为:6x-1、6x、6x+1、6x+2、6x+3、6x+4、6x+5、6(x+1)、6(x+1)+1、...。可以看到,不在6的倍数两侧的数为:6x+2、6x+3、6x+4。由于6x+2为2(3x+1),6x+3为3(2x+1),6x+4为2(3x+2),故它们一定不是素数。再除去6x本身,则素数只可能出现在6x的相邻两侧。但是注意,在6x的相邻两侧,并不一定就是素数。

对于N为6x-1、6x+1这两种情况,可以将其表示为:6i-1、6i、6i+1、6i+2、6i+3、6i+4。如果N能被6i、6i+2、6i+4整除,则N至少得是一个偶数,但6x-1、6x+1明显为一个奇数,故不成立。如果N能被6i+3整除,则N至少能被3整除,但6x-1、6x+1不可能被3整除,故亦不成立。综上,只需要考虑6i-1和6i+1的情况,即:循环的步长可以设置为6,每次判断循环变量k和k+2的情况。理论上,该方法的时间复杂度为O(sqrt(n)/3)。

根据上面的思路,我们给出了下面的示例代码。

use std::time::{Duration, Instant};

fn is_prime(num: u32) -> bool {
    // 单独处理0和1
    if num <= 1 {
        return false;
    }

    // 单独处理2和3
    if num == 2 || num == 3 {
        return true;
    }

    // 不在6的倍数两侧的,一定不是素数
    if num % 6 != 1 && num % 6 != 5 {
        return false;
    }

    // 在6的倍数两侧的,也可能不是素数
    let sqrt_num = (num as f64).sqrt() as u32;
    for i in (5..=sqrt_num).step_by(6) {
        // 只需要考虑6i-1和6i+1的情况
        if num % (i) == 0 || num % (i + 2) == 0 {
            return false;
        }
    }

    // 排除所有情况,剩余的肯定是素数
    true
}

fn main() {
    let start = Instant::now();
    for i in 0..100 {
        if is_prime(i) {
            println!("{}", i);
        }
    }

    let duration = start.elapsed();
    println!("{}ms", duration.as_millis());
}

总结

通过这道题,我们学习了判断一个正整数是否为素数的若干方法。既有历史悠久的埃拉托斯特尼筛法,也有常用的遍历法。在遍历法的基础上,我们利用素数的分布规律,对算法进行了优化,提升了其运行效率,降低了其时间复杂度。

💡 如果想阅读最新的文章,或者有技术问题需要交流和沟通,可搜索并关注小红书“希望睿智”。