Chinaunix首页 | 论坛 | 博客
  • 博客访问: 254612
  • 博文数量: 44
  • 博客积分: 1052
  • 博客等级: 少尉
  • 技术积分: 742
  • 用 户 组: 普通用户
  • 注册时间: 2010-12-17 16:51
文章分类

全部博文(44)

文章存档

2013年(7)

2012年(14)

2011年(23)

分类: C/C++

2011-12-06 10:45:54

正在看《algorithm in C》,看到了Sieve of Eratosthenes的素数筛法,于是在网上查了下,然后自己写了几个。

这里的问题是:给定N,求出小于N的所有素数,N > 0。至于求出前N个素数,利用素数定理,也可以转换为该问题

关于相应的方法,最近在编程随想的博客里看到一篇文章,写的不错。下面是链接:http://blog.csdn.net/program_think/article/details/7032600,自行前往观赏。我这里讲的会比较简略。

方法1: 除法
用一个数组来保存以求得的素数,然后判断下一个数是不是素数,就用数组里的素数去除。
例如刚开始数组里有[2, 3, 5, 7],要判断9是不是素数,就先用9出2,然后再除3,发现能除尽,因此9不是素数。相应的能发现11是素数,该方法比较简单。

方法2: 筛法
大致过程是这样的,对于数2,3,...N-1. 对每个数给个标记,标记为1表示素数。初始时所有数的标记都为1.
首先知道2是素数,然后将里面2*2, 2*3, 2*4.... 这些数的标记都设为0,也就是把2的倍数都给筛掉了。
然后知道3的标记为1,3是素数,将3的倍数的标记都设为0。
依次下去,最后保留的标记为1的数,就是素数。

筛法的实现的时候,可以用字符数组来保存标记,也可以用比特串来保存标记。

下面是具体的代码。
先看除法的。 前面的代码里面CLOCK相关的宏是用来计时的,宏M(op, arrp, us)是用来测试函数的。这里的代码和下面给出的代码都放到一个文件里就能编译运行了,所以这里看起来很复杂的样子,其实只要看里面的div_prime函数的实现就行了。
  1. #include <stdlib.h>
  2. #include <assert.h>
  3. #include <string.h>
  4. #include <math.h>
  5. #include <stdio.h>
  6. #include <sys/time.h>         /* linux里的头文件 */


  7. #define CLOCK_BEGIN(start) (gettimeofday(&(start), NULL))
  8. #define CLOCK_END(end) (gettimeofday(&(end), NULL))
  9. #define CLOCK_TIME(start, end) ((1e6*((end).tv_sec-(start).tv_sec)+(end).tv_usec-(start).tv_usec))

  10. #define M(op, arrp, us) \
  11.     CLOCK_BEGIN(start);\
  12.     (arrp) = (op);\
  13.     CLOCK_END(end);\
  14.     free((arrp));\
  15.     (us) = CLOCK_TIME(start, end);    

  16. unsigned *div_prime(unsigned N, unsigned *count)
  17. {
  18.     int i, j, g;
  19.     int isprime = 1;
  20.     double t;
  21.     unsigned total; /* count of prime number, init is 3 */
  22.     unsigned *prime;

  23.     /* estimate the number of primes */
  24.     /* 分配内存,分配的大小用N*1.17/log(N)来计算,这个公式是根据素数定理来的,具体参见编程随想的那篇文章,这里要注意的是1.17这个数字,如果使用这个数字,则N最好要大于1000,不然会出问题 */
  25.     prime = malloc(sizeof(unsigned) * (int)(N*1.17/log(N)));   
  26.     if(prime == NULL)
  27.     {
  28.         fprintf(stderr, "div_prime: Insufficent memory!\n");
  29.         exit(0);
  30.     }
  31.     prime[0] = 2; prime[1] = 3, prime[2] = 5;
  32.     total = 3; /* found prime's number */
  33.     /* 明显偶数不是素数(2除外),3的倍数也不是素数,2*3的倍数也不是素数,即我们不用判断6n形式的数,只需判断6n+1,6n+2,6n+3,6n+4,6n+5,但这里面6n+2和6n+4是2的倍数,不需判断,6n+3是3的倍数不需判断,因此只需要判断具有6n+1和6n+5的数是不是素数,即只判断数7, 11, 13, 17, 19, 23....,这里两个数的间隔是2和4的交替。 */
     g = 2;     
  1.     for(i = 7; i < N; i+=g)
  2.     {
  3.         g = 6 - g; /* only check 6i+1 and 6i+5 */
  4.         for(j = 0; prime[j] * prime[j] <= i; j++)
  5.             if(i % prime[j] == 0)
  6.             {
  7.                 isprime = 0; /* non-prime */
  8.                 break;
  9.             }
  10.         if(isprime == 0)
  11.             isprime = 1;
  12.         else
  13.             prime[total++] = i; /* isprime is 1, i is prime */
  14.     }
  15.     *count = total;       /* count 保存找到的素数的个数,这个通过参数返回 */
  16.     return prime
  17. }
这里关于内存分配和只判断6n+1和6n+5形式的数需要注意一下,其他的就比较容易懂了。

下面是筛法,这里采用unsigned char型数组来保存标记
  1. unsigned char *sieve_prime(unsigned N)
  2. {
  3.     unsigned int i, j;
  4.     unsigned char *prime;
  5.     unsigned int of; /* test overflow */

  6.     prime = malloc(sizeof(unsigned char) * N);
  7.     if(prime == NULL)
  8.     {
  9.         fprintf(stderr, "sieve_prime: Insufficient memory.\n");
  10.         exit(0);
  11.     }
  12.     for(i = 0; i < N; i++)
  13.         prime[i] = 1;
  14.     prime[0] = 0; prime[1] = 0;
  15.     for(i = 2; i < (sqrt(N)); i++) /* 原来这里使用(unsigned)(sqrt(N)), 但后来发现不正确 */
  16.     {
  17.         if(prime[i] == 1)
  18.         {
  19.             /* be caution about i*j overflow */
  20.             for(j = i,of=i*j; i==of/j && of < N; j++,of+=i)
  21.                 prime[of] = 0;
  22.         }
  23.     }
  24.     return prime;
  25. }
关于筛法需要注意的是,需要判断溢出,原来没有判断溢出,并且用int型来保存i和j时,当i=46349就溢出了,因为46349*46349 > 2147483647 = INT_MAX.   
另外这里采用of = i*j, 然后根据i == of/j来判断是否溢出。  也许有更好的溢出判断方法,但我不知道。

老实说,我还真不能保证这代码完全正确,虽然我进行了一些测试,比如比较它与div_prime的结果是否相同等,但我还是怕这里会出问题,主要是溢出这个东西有点麻烦。

下面是利用比特串的方法实现的筛法
  1. #define SHIFT 5
  2. #define MASK 0x1F
  3. #define BITPERWORD 32 /* 8*sizeof(int) */

  4. /* set the ith bit to 1 */
  5. void seti(unsigned *a, unsigned i)
  6. {
  7.     /* i>>SHIFT means i/32
  8.      * i & MASK means i%32 */
  9.     a[i>>SHIFT] |= (1 << (i & MASK));
  10. }

  11. /* clear the ith bit to 0 */
  12. void clri(unsigned *a, unsigned i)
  13. {
  14.     a[i>>SHIFT] &= ~(1 << (i & MASK));
  15. }

  16. /* if the i-th bit is 0, return 0, otherwise 1 */
  17. int testi(unsigned *a, unsigned i)
  18. {
  19.     unsigned t;
  20.     t = a[i>>SHIFT] & (1 << (i & MASK));
  21.     return ((t > 0) ? 1 : 0);
  22. }

  23. unsigned *bitsieve_prime(unsigned N)
  24. {
  25.     unsigned int i, j;
  26.     unsigned *prime;
  27.     unsigned int of; /* test overflow */

  28.     /* store N bits need at least 1+N/BITPERWORD int */
  29.     prime = (unsigned *) malloc(sizeof(unsigned) * (1+N/BITPERWORD));
  30.     if(prime == NULL)
  31.     {
  32.         fprintf(stderr, "bitsieve_prime: Insufficient memory.\n");
  33.         exit(0);
  34.     }
  35.     for(i = 2; i < N; i++)
  36.         seti(prime, i); /* set all to 1 */
  37.     clri(prime, 0);clri(prime, 1); /* set prime[0] and prime[1] to 0 */

  38.     for(i = 2; i < (sqrt(N)); i++)
  39.     {
  40.         if(testi(prime, i))
  41.         {
  42.             /* be caution about i*j overflow */
  43.             for(j = i,of=i*j; i==of/j && of < N; j++,of+=i)
  44.                 clri(prime, of);
  45.         }
  46.     }
  47.     return prime;
  48. }
采用bit串后,除了置0和置1的操作要另外实现,其他和unsigned char型的筛法一样。
这里采用的bit串的操作的代码,是在《programming pearls》的第一章中看到的。比较简洁。
我这里采用的函数实现的方法,应该也可以采用宏来实现。

下面是对这三个函数的时间测试函数
  1. void time_test(void)
  2. {
  3.     struct timeval start, end;
  4.     unsigned *div_p, *bit_p;
  5.     unsigned char *sieve_p;
  6.     unsigned div_us, bit_us, sieve_us;
  7.     unsigned count;
  8.     int i;
  9. /* 仅测试1000到3百万之间的数,并间隔为9999,每个数都测就太花时间了 */
  10.     for(i = 1000; i < 3000000; i += 9999)   
  11.     {
  12.         M(div_prime(i, &count), div_p, div_us);  /* 这里的M宏就是div_prime代码里的宏了 */
  13.         M(sieve_prime(i), sieve_p, sieve_us);
  14.         M(bitsieve_prime(i), bit_p, bit_us);
  15.         fprintf(stdout, "%d\t%lf\t%lf\t%lf\n", i, div_us/1e3, sieve_us/1e3, bit_us/1e3);
  16.     }
  17. }
M宏并不复杂,就不解释了。求出的div_us,sieve_us, bit_us都是微秒,最后输出的时候,就改成毫秒了。
要注意,这里的i是从1000开始的,,这个数字与div_prime里的数字1.17有关。如果从小于1000的数开始,内存分配上可能会有问题。

为完整性,下面是main函数,就是调用time_test。
  1. int main(int argc, char *argv[])
  2. {
  3.     time_test();

  4.     return 0;
  5. }
下面是用time_test测试时间后,绘出的图,采用gnuplot工具,学一个绘图工具,对于编程性能分析有很大帮助,:-),我是初学gnuplot工具。



明显还是筛法更快一些,其中采用unsigned char保存素数的筛法要快一些,不过当N变大之后,就和采用bit串的越来越接近了。有可能bit串的筛法慢的原因是我采用函数的方式实现的seti,clri。如果用宏来实现可能会快一些,但我没进行测试。 不过bit串的空间消耗要比unsigned char的小8倍了,所以综合来看选择比特串的筛法要更实惠一些。
额, 图中的div表示除法的div_prime, sieve表示unsigned char的sieve_prime,bit表示bit串的bitsieve_prime.

别看这是个简单的问题,但实际写起代码来还是有困难的,而且很容易犯错。我看《algorithm in C》一书的官网上给出的筛法的代码就是错误的。这里的代码如有错误还望指出,非常感谢。


阅读(3003) | 评论(0) | 转发(0) |
0

上一篇:python计时与profile

下一篇:二分搜索

给主人留下些什么吧!~~