筛选法求质数

最近在做C语言名题精选一书中的习题,在书中题2.3、2.4、2.5中提到了有关质数的求解问题。在这里作出总结:
质数问题由来以久,有着许多解法,这里列出我所知道的解法。
 
1、传统的解法
 
传统的解法就是利用"一个数 n 如果是合数,那么它的所有的因子不超过sqrt(n)"这一性质来求解,时间复杂度为o(n*sqrt(n)),显然当n很大时这种方法便不太适用。
 1 void getPrimeTrd(){

 2     int i,j;

 3     for(i = 2; i < MAX; ++i) {
       m = (int)sqrt(i);
4 for(j = 2; j <= m; ++j){ 5 if(i % j == 0) { 6 break;
         )
7 } 8 if(j > m) { 9 printf("%d ",i);
}
10 } 11 }
 
习题2-3里则给出了上述算法的优化版。作出了以下优化
1、在外层循环除去了2和3的倍数,在任意6个数中只需要测试两个数据,如6,7,8,9,10,11其中6,8,10为2的倍数,9为3的倍数,只余下7和11。
2、在测试是否为质数时取prime[]中的数据,而除去2,3倍数的测试(外层循环就已经限制了不可能被2和3的倍数整除 ),只与prime[]中的质数相除。
 1 void getPrimeTrdOpt(){

 2     int i,j,count;

 3     int prime[MAXSIZE];

 4     prime[0] = 2;

 5     prime[1] = 3;

 6     count = 2;

 7     for(i=5; i<MAXNUM; i+=2){

 8         if(i%3){

 9             for(j=0; prime[j]<=(int)sqrt(i); ++j){

10                 if(i%prime[j] == 0)

11                     break;

12             }

13             if(prime[j] > (int)sqrt(i))

14                 prime[count++] = i;

15         }

16     }

17     for(i=0; i<count; ++i)

18         printf("%d ",prime[i]);

19 }
上面是本人根据题意的解法,下面是作者的解法。
 
 1 void getPrimeTrdOpt2(){

 2      int  prime[MAXSIZE];   

 3      int  gap = 2;           

 4      int  count = 3;         

 5      int  may_be_prime = 5; 

 6      int  i;

 7      prime[0] = 2;           

 8      prime[1] = 3;          

 9      prime[2] = 5;

10      while (prime[count] < MAXNUM) {

11           may_be_prime += gap;

12           gap = 6 - gap;

13           for (i = 2; prime[i]*prime[i] <= may_be_prime; i++)

14                if (may_be_prime % prime[i] == 0)

15                     break;

16           if (prime[i] > (int)sqrt(may_be_prime))     

17                prime[count++] = may_be_prime; 

18      }

19      for (i = 0; i < count; i++) {

20           printf("%d ", prime[i]);

21      }   

22 }
显然作者的解答更加巧妙,其用了gap一个变量就达到了除去2和3倍数的功能(通过+2,+4,+2,+4....),观察除去2和3倍的连续数5,7,11,13,17.......,都是2,4间隔开的,当真十分巧妙。

2、筛选法求质数

筛选法又称筛法,是求不超过自然数N(N>1)的所有质数的一种方法。据说是古希腊的埃拉托斯特尼(Eratosthenes,约公元前274~194年)发明的,又称埃拉托斯特尼筛子。具体做法是:先把N个自然数按次序排列起来。1不是质数,也不是合数,要划去。第二个数2是质数留下来,而把2后面所有能被2整除的数都划去。2后面第一个没划去的数是3,把3留下,再把3后面所有能被3整除的数都划去。3后面第一个没划去的数是5,把5留下,再把5后面所有能被5整除的数都划去。这样一直做下去,就会把不超过N的全部合数都筛掉,留下的就是不超过N的全部质数。因为希腊人是把数写在涂腊的板上,每要划去一个数,就在上面记以小点,寻求质数的工作完毕后,这许多小点就像一个筛子,所以就把埃拉托斯特尼的方法叫做“埃拉托斯特尼筛”,简称“筛法”。(另一种解释是当时的数写在纸草上,每要划去一个数,就把这个数挖去,寻求质数的工作完毕后,这许多小洞就像一个筛子。)
 
版本一:
 1 void getPrimeErato(){

 2     int prime[MAXSIZE];

 3     int i,j;

 4     for(i=0; i<MAXSIZE; ++i){

 5         if(i!=2 && i%2==0)

 6             prime[i] = NO;

 7         else

 8             prime[i] = YES;

 9     }

10     for(i=3; i<=(int)sqrt(MAXSIZE); i+=2){

11         if(prime[i]){

12             for(j=i+i; j<MAXSIZE; j+=i){

13                 prime[j] = NO;

14             }

15         }

16     }

17     for(i=2; i<MAXSIZE; ++i){

18         if(prime[i])

19             printf("%d ",i);

20     }

21 }
上面应该是最常见的筛选法求质数的程序,在初始化数组时将2的倍数设为了NO,这样做是为了简化程序。
 
版本二:
 1 void getPrimeErato2(){

 2     int prime[MAXSIZE];

 3     int i,j;

 4     for(i=0; i<MAXSIZE; ++i){

 5         if(i!=2 && i%2==0)

 6             prime[i] = NO;

 7         else

 8             prime[i] = YES;

 9     }

10     for(i=3; i<=(int)sqrt(MAXSIZE); i+=2){

11         if(prime[i]){

12             for(j=2; j<=MAXSIZE/i; ++j)

13                 prime[i*j] = NO;

14         }

15     }

16  

17     for(i=2; i<MAXSIZE; ++i){

18         if(prime[i])

19             printf("%d ",i);

20     }

21 } 
版本二与版本一原理一样,版本二的可读性更好。
 
版本三(优化):
 1 void getPrimeEratoOpt(){

 2     int size = (MAXNUM - 3) / 2 + 1;

 3     int prime[size];

 4     int key;

 5     int i,j;

 6     for(i=0; i<size; ++i){

 7         prime[i] = YES;

 8     }

 9     for(i=0; i<size; ++i){

10         if(prime[i]){

11             key = i + i +3;

12             for(j=key+2*i*i+4*i; j<size; j+=key){ 

13                 prime[j] = NO;

14             }

15         }

16     }

17     printf("%d ",2);   

18     for(i=0; i<size; ++i){

19         if(prime[i])

20             printf("%d ",2*i+3);

21     }

22 }
这里的程序,prime[i]不再表示i为质数式合数,而表示2i+3为质数式合数,如果prime[i] = OK,则2i+3为质数,否则为合数。这样做有什么好处?因为所在的质都是奇数,2i+3能表示所有的质数(2除外),这样做一方面减少了数组的空间,如需找出99以内的质数,用普通的筛选法则需定义一个100个长度的数组prime[100],而使用上面的程序只需定义一个(99-3)/2+1=49个长度的数组。空间个减少了一半。另一方面这做使得得到的质数全为奇数,直接排除了所有的偶数,提高了效率。程序中for(j=key+i;j<MAXSIZE/2-1;j+1=key){}这个for循环的作用是排除所有非质数,它是通过排除所有质数的奇数倍来排除奇数中的合数.具体操作如下,首先,确定一个质数2i+3,然后将2i+3的奇数倍对应的i值设为NO,如3(2i+3)为合数,则6i+9 = 2x+3,x=3i+3,于是将prme[3i+3]设为NO, 同理5(2i+3)=10+15=2x+3,x=5i+6,将prime[5i+6]设为NO。其中考虑到质数为5时会重复排除15(在3的时侯已经排除了),所以for循环中j=key+2*i*i+4*i,即从(2i+3)^2开始排除,而不是3(2i+3),为什么3 5 7 9 11 ... ...中的素数n的倍数(奇数)要从n开始?
就是说从1开始和从n开始的效果是一样,或者说n以前的奇数乘n都可以等于n以前(比n小的)素数的更大(比n大的)奇数倍数。由于n是奇数,可以设n以前(比n小的)的奇数为n-2i,比n大的奇数为n+2j;那么我们的任务就是,证明n*(n-2i)可以等于m*(n+2j),其中m为小于n的素数,i、j都是正整数。即n(n-m)=2mj+2ni。这有变成证
明n-m是偶数,这是显然的,两个奇数之差必然是偶数。
 

版本四(优化):

 1 void getPrimeEratoOpt2(){

 2     int i,j;

 3     int size = (MAXNUM-1)/2+1;

 4     int prime[size];

 5     for(i=0; i<size; ++i){

 6         prime[i] = YES;

 7     }

 8     for (i=3;i<size;i+=2)

 9     {

10        if(prime[(i-1)/2]){

11             for (j=i*i;j<MAXNUM;j+=i<<1)

12                 prime[(j-1)/2] = NO;

13        }

14     }

15     printf("%d ", 2);

16     for (i=1;i<size;++i)

17     {

18         if (prime[i])

19             printf("%d ", (i<<1)+1);

20     }

21
版本四与版本三思路基本一致,区别在于一个用2i+3另一个用2i+1,最主要的区别在于在排除时过程与三正好相反,一个是prime[j] = NO,另一个是prime[(j-1)/2] = NO, 一个是循环直接控制i,而一个则是控制2i+1。从可读性上来说版本四的可读性更强,从空间利用率上来说版本三则更好。
 
版本五(线性筛选)
 1 void getPrimeEratoLine(){

 2     int prime[MAXSIZE];

 3     int is_prime[MAXNUM];

 4     int count,i,j;

 5     count = 0;

 6     for(i=0; i<MAXNUM; ++i){

 7         is_prime[i] = 0;

 8     }

 9     for(i=2; i<MAXNUM ; ++i){

10         if(is_prime[i] == 0){

11             prime[count++] = i;

12         }

13         for(j=0; j<count && (i*prime[j]) < MAXNUM; ++j){

14             is_prime[i*prime[j]] = 1;

15             if(i % prime[j] == 0) break;

16         }

17     }

18     for(i=0; i<count; ++i){

19         printf("%d ",prime[i]);

20     }   

21 }
这个算法有两层循环,第一层遍历2到n之间的所有自然数i,看看它是不是质数,如果是,则把i放进prime数组中。第二层循环是对所有未来的数进行筛选。对于当前正在处理的i,显然它乘以任何一个已经找到的素数的结果肯定是和数,它们将会被剔除。整个算法最核心的一句是第7行:当i可以被某个已经找到的质数整除的时候,循环退出,不再进行剔除工作。这样做的原因是:当prime[j]是i的因子的时候,设i=prime[j]*k,首先,我们可以肯定的说,prime[j]是i的最小质因数,这是因为第二层循环是从小到大遍历素数的;其次,我们可以肯定的说,i已经无需再去剔除prime[j']*i (j'>j) 形式的和数了,这是因为,prime[j']*i可以写成prime[j']*(prime[j]*k)=prime[j]*(prime[j']*k),也就是说所有的prime[j']*i将会被将来的某个i'=prime[j']*k剔除掉,当前的i已经不需要了。
    然后我们再看看时间复杂度。虽然有两层循环,但是我们发现,正因为有了第7句,所有is_prime数组里面的所有false都只被赋值了一次,而且是在发现它的最小质因数的时候被赋值的。在外层循环执行了O(n)次操作的同时,内层循环里面总的操作次数也是O(n)次。因此,总的时间复杂度是O(n)。因为用了is_prime数组,空间复杂度也为O(n)。
 
 
 
 
 

你可能感兴趣的:(质数)