找出素数

  是不是很经典,还记得什么是素数吗。不记得,没事,我不讲。连接在这自己转跳 (●ˇ∀ˇ●)。我比较懒。看完之后让我们来思考一下如何求素数,

首先回顾一下素数的定义:

      质数是指在大于1的自然数中,除了1和它本身以外不再有其他因数的自然数。

由这一条限制可得出一个结论:

    存在一个数n,若n被[2,n-1]内的任何一个数整除,那么,这个数就是不是素数。

OK, 根据这个结论我们可以写出最最最暴力的算法,代码如下

 1 /**
 2  * @brief  素数基本求法
 3  * @note   
 4  * @param  num: 被判断的数
 5  * @author 杨文蓁的小迷弟
 6  */
 7 int IsPrime(int num)
 8 {
 9     if (num < 2)
10     {
11         return 0;
12     }
13     14     for (int i = 2; i < num; i++)
15     {
16         if (num % i == 0)
17         {
18             19             return 0;20         }
21     }
22     return 1;
23 }

 

  素数判定还有另一种方法:设存在任意一个数m。

    如果 m 不能被 2 ~  间任一整数整除,m 必定是素数。

  原因如下:

    若 m 能被 2 ~ m-1 之间任一整数整除,其两个因子必定有一个小于或等于 ,另一个大于或等于 。例如 16 能被 2、4、8 整除,16=2*8,2 小于 4,8 大于 4,16=4*4,4=√16,因此只需判定在 2~4 之间有无因子即可。

  所以可以写出相应的代码

 1 /**
 2  * @brief  sqrt求素数
 3  * @note   离散数学初等数论里有定理
 4  * @param  num: 要判断的数
 5  * @author 杨文蓁小迷弟
 6  */
 7 int IsPrime(int num)
 8 {
 9     if (num < 2)
10     {
11         return 0;
12     }
13     14     int k = (int)sqrt((double)num);
15     for (int i = 2; i <= k; i++)
16     {
17         if (num % i == 0)
18         {
19             20             return 021         }
22     }
23     return 1;
24 }

  

  我一直觉得O(√N)这种时间复杂度已经低了。但是直到我看到一篇博客,我才知道我错了,让我们一起学习一下大佬的思路:

  

  先看一个质数分布的规律:

    大于等于5的质数一定和6的倍数相邻。

      例如5和7,11和13,17和19等等;

  接着我们来证明一下:

证明:令x≥1,将大于等于5的自然数表示如下:

······ 6x-1,6x,6x+1,6x+2,6x+3,6x+4,6x+5,6(x+1),6(x+1)+1 ······

  可以看到,不在6的倍数两侧,即 6x 两侧的数为

      6x+2,6x+3,6x+4,

  由于 2(3x+1),3(2x+1),2(3x+2),所以它们一定不是素数,再除去6x本身,显然,素数要出现只可能出现在6x的相邻两侧。

  这里要注意的一点是,在6的倍数相邻两侧并不是一定就是质数。

  此时判断质数可以6个为单元快进,原本为1的步长加大为6,加快判断速度。

  原因如下:

    设要判定的数为n,则n必定是6x-1或6x+1的形式,对于循环中6i-1,6i,6i+1,6i+2,6i+3,6i+4

    1、如果n能被6i,6i+2,6i+4整除,则n至少得是一个偶数,但是6x-1或6x+1的形式明显是一个奇数,故不成立;

    2、如果n能被6i+3整除,则n至少能被3整除,但是6x能被3整除,故6x-1或6x+1(即n)不可能被3整除,故不成立。

  综上,循环中只需要考虑6i-1和6i+1的情况,即循环的步长可以定为6。

呼~,大佬就是大佬望尘莫及。下面贴一下代码:

 1 /**
 2  * @brief  O(((n^(1/2))/3)
 3  * @note   ozr,膜拜一下大佬的思想。
 4  * @param  num: 待测数据
 5  * @author 杨文蓁的小迷弟 
 6  */
 7 int IsPrime(int num)
 8 {
 9     // 0, 1特判
10     if (num < 2)
11     {
12         return 0;
13     }
14     //两个较小数另外处理
15     if (num == 2 || num == 3)
16     {
17         return 1;
18     }
19 
20     //不在6的倍数两侧的一定不是质数
21     if (num % 6 != 1 && num % 6 != 5)
22     {
23         return 0;
24     }
25 
26     int tmp = sqrt(num);
27     //在6的倍数两侧的也可能不是质数
28     for (int i = 5; i <= tmp; i += 6)
29     {
30         if (num % i == 0 || num % (i + 2) == 0)
31         {
32             return 0;
33         }
34     }
35     //排除所有,剩余的是质数
36     return 1;
37 }

  再次膜拜 ozr, ozr

   以上的方法应用于素数判断。

接着讲快速打出2~n的素数表:

  首先得提的就是埃氏筛法,什么是埃氏筛法?离散数学最后面的基本数论对其思想进行了大致的说明。

思路大致如下:

  对于不超过N的素数p,删除2*p,3*p,4*p…,当处理完所有的数之后,还没有被删除的数就都是素数。

代码顺着这个思路写下去就行了。

 1 /**
 2  * @brief  埃氏筛法
 3  * @note   偷懒不用memset,默认0为素数
 4  * @param  size: 要计算的范围大小
 5  * @author 杨文蓁的小迷弟
 6  */
 7 int make_prime(int size)
 8 {
 9     Isprime[0] = 1;
10     Isprime[1] = 1;
11     int cnt = 0;
12     for (int i = 2; i < size; i++)
13     {
14         if (!Isprime[i])
15         {
16             for (int k = i * i; k < size; k += i)
17             {
18                 Isprime[k] = 1;
19             }
20         }
21     }
22 }

  解释一下 j = i * i:  

  因为2*i, 3*i, 4*i, … , (i-1)*i 肯定已经被前面的2, 3, 4, … , (i-1) 这些里面的质数筛掉了。换句话说,我们筛掉的是素数的倍数,那么i的倍数中包括2, 3, … i, …, m,但是比i小的那些倍数,2, 3, … i-1,在i作为素数开始筛合数之前肯定已经i被当作2, 3, … i-1这些中素数的倍数被筛掉了,所以我们只需要从j=i*i 开始即可。

  但即使是这样埃氏筛法还是存在重复计算 例如 ,计算 50 以内素数时,在 i = 2,3,5 时,都计算了 30。为了解决这个问题,我们的更厉害的筛法——欧拉筛法(线性筛)就诞生了。

欧拉筛法的思路还是一样的,所以先贴代码。

 1 /**
 2  * @brief  Euler筛法
 3  * @note   12
 4  * @param  size: 范围
 5  * @author 杨文蓁的小迷弟
 6  */
 7 int Isprime[MAXN];
 8 int Prime[MAXN];//记录素数
 9 void Euler_prime(int size)
10 {
11     int cnt = 0;//记录素数个数
12     
13     Isprime[0] = Isprime[1] = 1;
14     for (int i = 2; i <= MAXN; i++)
15     {
16         if (!Isprime[i])//
17         {
18             Prime[cnt++] = i;
19         }
20         for (int j = 0; j < cnt && i * Prime[j] < MAXN; j++)//在MAXN范围内讲所有已得出的素数的倍数都清除
21         {
22             Isprime[i * Prime[j]] = 1;
23             if (!(i % Prime[j]))//如果被1某个素数整除的话那就意味着被其他素数筛过了,往后就不用继续了
24             {
25                 break;
26             }
27         }
28     }
29 }

   

  讲解一下(虽然我觉得注释已经够详细了),if (!(i % Prime[j])),这里用到了离散数学(真是好书)里书里的基本数论的 算数基本定理:

  一个合数可以被分解为限个质数的乘积

  无论 i 是否为素数,都会执行到20行,

    1、如果 i 都是是素数的话,

      一个大的素数 i 乘以不大于 i 的素数P,这样筛除的数跟之前的是不会重复的。

      筛出的数都是 N=P1*P2的形式, P1P2之间不相等

    2、如果 i 是合数,

      此时 i 可以表示成递增素数相乘 i = P1*P2*...*Pn, Pi都是素数(2<=i<=n),  pi<=pj  ( i<=j )P1是最小的系数。 

      根据 23行 的退出定义,当P1==prime[j] 的时候,筛除就终止了,也就是说,只能筛出不大于P1的质数*i构成的合数。

  

  我们可以直观地举个例子。

    i = 2 * 3 * 5 

    此时能筛除 2*i ,不能筛除 3*i。

    如果能筛除3*i 的话,当 i 等于 i=3*3*5 时,筛除2*i 就和前面重复了

  其实就是素数越小筛出范围越大,导致后面的比它大的素数进行了重复筛除,为了减小重复所以对小素数的筛除范围进行了限制。

 

目前我的能力只能写到这种程度了,正在学素数定理,后期应该会继续更。

 

算法不易,诸君共勉!

posted @ 2019-10-04 13:02  秦_殇  阅读(447)  评论(0编辑  收藏  举报