质数(prime number)又称素数,有无限个。除了1和它本身以外不再有其他的除数整除。根据算术基本定理,每一个比1大的整数,要么本身是一个质数,要么可以写成一系列质数的乘积,最小的质数是2。(摘自百度百科,更多讨论请参考关键词“素数”)
从定义,我们可以得到一些边界信息,例如,1不是素数,最小的素数是2。那么再根据定义,我们很容易得到一个判断素数的思路,N以内的素数,对于任意2<=X<=N,只要判断1和X以外是否还能被其他除数整除。初始拟定的除数的选择范围是[2,X],这是根据定义可以得出的最直观的思路。后来我们知道可以通过缩小这个除数的选择范围来进行达到一定的优化目的,这是最传统的优化方法,更多优化方法在本文第三节讨论。
#!/bin/env python
#-*-coding:utf-8-*-
import math
import sys
def prime(n):
if n <= 1:
return 0
#for i in range(2,int(math.sqrt(n)+1)):
for i in range(2,n):
if n%i == 0:
return 0
return 1
if __name__ == "__main__":
n = int(sys.argv[1])
for i in range(2,n+1):
if prime(i):
print i
$ time ./old_prime.py 100000 |wc -l
9592
real 2m49.129s
user 2m48.567s
sys 0m0.024s
我们把函数prime中的for语句替换成注释行,再测试下:
$ time ./old_prime.py 100000 |wc -l
9592
real 0m0.843s
user 0m0.830s
sys 0m0.012s
显然,[2,√n+1]范围下,效率快了很多,时间可以接受的。
#!/bin/env python
#-*-coding:utf-8-*-
import sys
import math
def prime(n):
if n%2 == 0:
return n==2
if n%3 == 0:
return n==3
if n%5 == 0:
return n==5
for p in range(7,int(math.sqrt(n))+1,2): #只考虑奇数作为可能因子
if n%p == 0:
return 0
return 1
if __name__ == "__main__":
n = int(sys.argv[1])
for i in range(2,n+1): #1不是素数,从2开始
if prime(i):
print i
再来实现第二种思路,代码如下:
#!/bin/env python
#-*-coding:utf-8-*-
#寻找n以内的素数,看执行时间,例子100000内的素数
import sys
def prime(n):
flag = [1]*(n+2)
p=2
while(p<=n):
print p
for i in range(2*p,n+1,p):
flag[i] = 0
while 1:
p += 1
if(flag[p]==1):
break
# test
if __name__ == "__main__":
n = int(sys.argv[1])
prime(n)
统一测试下(以下测试都是测了好多遍,结果都是类似的),这里我们把N增加了一个数量级,有原来的100000,变成了1000000,
$ time ./sushu_v0.1.py 1000000 |wc -l
78498
real 0m6.203s
user 0m6.169s
sys 0m0.031s
$ time ./sushu2.py 1000000 |wc -l
78498
real 0m0.754s
user 0m0.730s
sys 0m0.033s
好了,差异很清楚了。第二种方法要优于第一种,这里我们优化下代码
$ time ./sushu_v0.1.py 1000000 |wc -l
78498
real 0m5.440s
user 0m5.404s
sys 0m0.018s
$ time ./sushu2.py 1000000 |wc -l
78498
real 0m0.624s
user 0m0.615s
sys 0m0.013s
两种方法速度都有提升。我们来讨论下range和xrange的差异,按我的理解,range是一次性连续返回一个列表,而xrange是每次只生成一个,并且不保留上次生成的值。更多的讨论可以自行搜索相关资料。
致命错误:对于range(2*p,n+1,p),还有一种实现方法,range(2*p,n+1)[::p],但这两种写法,完全不相干,range(2*p,n+1,p)返回的列表就是按照p步长来生成的,而range(2*p,n+1)[::p],是生成了步长为1的列表,最后列表执行切片操作,只取p步长的值返回,显然没有range(2*p,n+1,p)的实现更为直接,两者虽然返回值一样,但经过实际测试发现,效率差异非常大,甚至可以颠覆算法的优势,那么下面我们来颠覆下:
#!/bin/env python
#-*-coding:utf-8-*-
#寻找n以内的素数,看执行时间,例子100000内的素数
import sys
def prime(n):
flag = [1]*(n+2)
p=2
while(p<=n):
print p
for i in range(2*p,n+1)[::p]:
flag[i] = 0
while 1:
p += 1
if(flag[p]==1):
break
# test
if __name__ == "__main__":
n = int(sys.argv[1])
prime(n)
$ time ./sushu2.py 100000 |wc -l
9592
real 0m16.049s
user 0m15.836s
sys 0m0.014s
注意,我这里N是100000,还不是1000000。得到的结果仍然不尽人意,甚至会让我们怀疑算法的优异性,所以我们在怀疑理论之前,一定要充分把自己代码里的问题检查清楚。
$ time ./sushu2.py 10000000 |wc -l
664579
real 0m7.648s
user 0m7.538s
sys 0m0.182s
while 1的情况:
$ time ./sushu2.py 10000000 |wc -l
664579
real 0m7.399s
user 0m7.214s
sys 0m0.160s
#!/bin/env python
#-*-coding:utf-8-*-
import sys
import math
def prime(n):
if n%2 == 0:
return n==2
if n%3 == 0:
return n==3
if n%5 == 0:
return n==5
for p in xrange(7,int(math.sqrt(n))+1,2): #只考虑奇数作为可能因子
if n%p == 0:
return 0
return 1
if __name__ == "__main__":
n = int(sys.argv[1])
for i in xrange(2,n+1): #1不是素数,从2开始
if prime(i):
print i
第二种
#!/bin/env python
#-*-coding:utf-8-*-
#寻找n以内的素数,看执行时间,例子100000内的素数
import sys
def prime(n):
flag = [1]*(n+2)
p=2
while(p<=n):
print p
for i in xrange(2*p,n+1,p):
flag[i] = 0
while 1:
p += 1
if(flag[p]==1):
break
# test
if __name__ == "__main__":
n = int(sys.argv[1])
prime(n)
可能依然有些遗留问题,比如,第一种方法中,为什么只考虑2,3,5,为什么不把7也考虑进去甚至把11也考虑进去?如果考虑了7,代码是否有别扭的地方?比如,n=6是什么情况,n=7是否有必要单独考虑,思考到这里,大概就明白了。
for p in xrange(7,int(math.sqrt(n))+1,2): #只考虑奇数作为可能因子
if n%p == 0:
return 0
return 1
这样n在49之前,是不是把所有的奇数都算进去了?我觉得这个问题,可以自己动手测试下就都清楚了,请不要忽略前面的三个if语句。