【C/C++】线性筛质数(素数)

1,170 阅读2分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情


质数(素数)定义

质数又称素数。一个大于1的自然数(规定1既不是质数也不是合数),除了1和它自身外,不能被其他自然数整除的数叫做质数;否则称为合数。

埃氏筛法

埃氏筛法的全名为埃拉托斯特尼筛法,简称埃氏筛或爱氏筛,是一种由希腊数学家埃拉托斯特尼所提出的一种简单检定素数的算法。要得到自然数n以内的全部素数,必须把不大于根号n的所有素数的倍数剔除,剩下的就是素数。图解如下:

20191027172732460.gif

时间复杂度

O(nlog2(log2n))O(n*log_2(log_2n))优化后可以看成线性O(n)O(n)级别的

代码实现

一般筛质数(素数)都是预处理某个范围n内的素数。创建数组prime[]prime[i]表示判断i是否为质数,如果i为质数prime[i] = 1prime[i] = true,否则prime[i] = 0prime[i] = false

bool prime[n + 1];//预处理n范围内的素数,prime[i] = 1表示i是素数
void InitPrime(){  
    int i, j;
    //初始化数组
    for(i = 0; i <= n; i++) prime[i] = 1;
    //规定0和1不是质数
    prime[0] = prime[1] = 0;
    //筛质数
    for(i = 2; i <= n; i++){  
        //如果i是质数
        if(prime[i]){  
            //把i的倍数都标记为不是质数
            for(j = i * 2; j <= n; j += i) prime[j] = 0;
        }
    }
}

对于上面的代码我们还可以继续优化

  • 对于第一层循环 for(i = 2; i <= n; i++) 优化为 for(int i = 2; i * i <= n; i++)
    这里把i <= n 改为 i * i <= n是因为我们需要将n以内的合数标记为0,而我们知道n以内合数的质因数一定不会超过根号n。(这是因为我们对于一个数x求其因子时,只需要遍历到根号x的位置即可,而在[根号x,x][根号x, x]区间的因子,我们在遍历[2,根号x][2, 根号x]区间的时候就已经可以求出来了,用x除以[2,根号x][2, 根号x]区间x的因子即可求出,那么由此可得n以内合数的质因子也就肯定不会超过根号n了,也就是[根号n,n][根号n, n]之间的合数一定会由根号n之前的质数翻倍标记
  • 对于第二层循环for(j = i * 2; j <= n; j += i) 优化为 for(int j = i * i; j <= n; j += i)
    这里把 j = i * 2 改为 j = i * i 是因为在[i2,ii][i * 2, i * i]之间的合数已经被[2,i1][2,i-1]区间的某个数标记过了,也就是[i2,i3,i4,...,i(i1)][i * 2, i * 3, i * 4,..., i * (i - 1)]是被优化省略掉的,那么为什么可以省略呢。比如 i(i2)i*(i-2) 在遍历 i2i-2 时已经被标记过,所以不需要用 ii 再去跑一遍。(比如在遍历 1111 的时候,直接从111111 * 11开始,那么对于1111之前的质数在翻倍标记的时候肯定都翻过1111倍,那么1111之前的合数可以拆解成一个质数乘以某个数再乘以1111,比如 8118*11 我们可以看成 24112 * 4 * 11,也就是 2442 * 44 那么我们可以理解为 881111 倍就是 224444倍,也就是我们在遍历质数 22 的倍数的时候就已经遍历标记过了)

优化后的代码

bool prime[n + 1];//预处理n范围内的素数,prime[i] = 1表示i是素数
vector<int> p;//存放范围内所有素数  p.size() = 素数个数
void InitPrime(){
    //初始化
    for(int i = 0; i <= n; i++) prime[i] = 1;
    prime[0] = prime[1] = 0;
    p.clear();
    //注意优化后的 i * i <= n 和 j = i * i 开始
    for(int i = 2; i * i <= n; i++)
	if(prime[i])
            for(int j = i * i; j <= n; j += i) prime[j] = 0;
    //也可将所有的质数存放进vector中方便使用,例如p.size() = n范围内素数的个数
    for(int i = 0; i <= n; i++){
        if(prime[i]) p.push_back(i);
    }
}

总结

首先理解埃氏筛法的原理,先用代码实现最基本的思想,然后再考虑优化,这样循序渐进的优化更能够理解和接受,其优化难点在于如何思考为什么把不大于根号n的所有素数的倍数剔除,剩下的就是素数呢为什么可以省略掉 iii * i 之前 ii 的倍数,这样我们就以接近线性O(n)O(n)级别时间复杂度的方法得到 nn 以内的素数了。


结束语

清明,教会我们不忘本,不忘从哪里出发;清明,教会我们在爱中告别,最好的缅怀是记得,也是放下;清明,是“传承”的日子,有哀思、有纪念,也有责任和感恩。愿你不负时光、珍惜当下,带着所爱之人的嘱托,勇敢从容地过好每一天。