• 大纲:
  • 摘要
  • 一、素数的定义
  • 二、N以内素数常用实现方法
  • 三、优化方法
  1. 原理层面
  2. 代码层面
  • range和xrange
  • while 1和while True真的重要吗

摘要


   本文主要是参考《编程珠玑-续订版》第一章关于求素数的解释,描述素数的定义,以及N以内素数的常用求解方法,最后一步步给出优化方法。代码用python实现两个优化方法,并给出原理层面和代码层面的分析。其中比较有意思的是,最后的部分,最开始代码写的只是考虑功能实现,而没有考虑怎样优化,最后的结果却大跌眼镜。反思之后,发现代码中的问题,并进行代码优化以后,还是证明了较为先进的算法原理在同样代码实现的基础上,性能更佳。


    然而也从侧面得到一个结论:就算原理再先进,如果代码写的不行,性能也上不去。江湖有类似传言“编程语言或者算法原理的理论性能都是浮云,写代码的人才是关键”。同样留给自己的启发也很重要,功能实现了以后,一定要多想想还有没有进一步优化的可能。


一、素数的定义

 质数(prime number)又称素数,有无限个。除了1和它本身以外不再有其他的除数整除。根据算术基本定理,每一个比1大的整数,要么本身是一个质数,要么可以写成一系列质数的乘积,最小的质数是2。(摘自百度百科,更多讨论请参考关键词“素数”)

    从定义,我们可以得到一些边界信息,例如,1不是素数,最小的素数是2。那么再根据定义,我们很容易得到一个判断素数的思路,N以内的素数,对于任意2<=X<=N,只要判断1和X以外是否还能被其他除数整除。初始拟定的除数的选择范围是[2,X],这是根据定义可以得出的最直观的思路。后来我们知道可以通过缩小这个除数的选择范围来进行达到一定的优化目的,这是最传统的优化方法,更多优化方法在本文第三节讨论。

二、N以内素数常用实现方法


    首先我们实现一个教科书里都有提到的方法,相信大家耳熟能详,直接上代码(暂时不做任何代码优化):


#!/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



代码中注释行是取了[2,√n+1]作为除数范围,我们测试下N=100000的性能,本机是8核redhat5.3主机,配置还行:

$ 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]范围下,效率快了很多,时间可以接受的。

三、优化方法

    原理层面


有没有更加机智的方法来找出n以内的素数?答案是有。我们这里提两种:


[2,√n+1]内排除了一半的计算量,再来看奇数,1不是素数,从3开始看,除了3以外,其余能被3整除的都是合数,那么我们在剩余计算量的基础上又排除了三分之一的计算量,再看5,除了5以外,其余能被5整除的都是合数,我们又在剩余计算量的基础上排除了五分之一的计算量,加起来,我们一共在[2,√n+1]范围内排除了近3/4的计算量。


    2、我们再换个角度延伸一下,我们构造一个大小为n的列表,初值都是1,每发现一个素数,我们把所有它的倍数都置为0,直到发现下一个为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

       

好了,差异很清楚了。第二种方法要优于第一种,这里我们优化下代码

        首先,将range换成xrange,再测试下:


$ 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。得到的结果仍然不尽人意,甚至会让我们怀疑算法的优异性,所以我们在怀疑理论之前,一定要充分把自己代码里的问题检查清楚。

         while 1和while True真的重要吗


我们把sushu2.py中的while 1改成while True测试下,


while True的情况:


$ 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语句。

    在这几种方案中,最后一种速度最快,效率最高,但有个应用前提,就是待搜索列表必须是有序且连续的,所以比较适合N以内符合某条件的数字,而前几种方法,是对列表中每个元素都做判断,返回类型是bool。或许后面可以改造下。