关于求N以内素数的python实现以及优化方法

  • 大纲:

    • 摘要
    • 一、素数的定义
    • 二、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, +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, +1]范围下,效率快了很多,时间可以接受的。

三、优化方法

原理层面

通过第二节的实现和测试,问题解决了,但有没有更加机智的方法来找出n以内的素数?答案是有。我们这里提两种:

1、我们思考这样一个问题,除了2以外,其余的偶数还有没有可能是素数?显然不可能,这样我们又在[2, +1]内排除了一半的计算量,再来看奇数,1不是素数,从3开始看,除了3以外,其余能被3整除的都是合数,那么我们在剩余计算量的基础上又排除了三分之一的计算量,再看5,除了5以外,其余能被5整除的都是合数,我们又在剩余计算量的基础上排除了五分之一的计算量,加起来,我们一共在[2, +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)
    flag[1] = 0         # 1 is not prime
    flag[n+1] = 1
    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)
    flag[1] = 0         # 1 is not prime
    flag[n+1] = 1
    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)
    flag[1] = 0         # 1 is not prime
    flag[n+1] = 1
    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。或许后面可以改造下。

时间: 2024-10-02 01:45:36

关于求N以内素数的python实现以及优化方法的相关文章

求100以内素数的5中基本方法及其优化

求100以内素数的5中基本方法及其优化方法1 基本做法 错解比较:进入了小循环:有时加pass也可以.错解:这里的print也同样注意不要写到循环内. 注释:1.两种条件运用:为合数.2.以上错误点.方法二 注释:1.条件运用,质数除了2都是奇数,因为偶数都能被除了自身外的2整除.改进自12.偶数除了2都不是质数.3.第二个for写的是到开方的奇数.4.奇数含有质数和非质数.偶数除了2一定不是质数.所以先剥离了偶数. 方法三:使用列表 注释:1.break continue pass2.质数i依

Java简单算法--求100以内素数

package cn.magicdu.algorithm; /** * 打印素数 * * @author xiaoduc * */ public class Prim { public static void main(String[] args) { for(int i=1;i<=100;i++){ if(isPrime(i)){ System.out.println(i); } } } /** * 判断是否是素数 * @param num * @return */ private stati

筛法求1000000以内素数个数---时间复杂度为o(n)

这样做倒是挺简单的,怎么容易怎么做 #include<stdio.h> #include<string.h> #define maxn 1000000 int isprime[maxn+10]; int prime[maxn/10]; int count; int n; int main() { while(scanf("%d",&n)!=EOF) { count=0; for(int i=0;i<=maxn+10;i++) isprime[i]=

一道面试题:用多线程求1000以内的素数有多少个?并给出消耗时间

我曾经去一个公司面试,遇到这么一个题目:求1000以内的素数有多少个?用多线程实现,并给出消耗时间.我想了半天,没有想出多线程的解决方案.今天因为机缘到了,我浅谈下我的解法. 这道题,显然得考虑两个问题: 1.多线程的问题 2.算法性能问题 有人觉得1000以内还考虑什么算法性能?这肯定很快.但是话说回来,这个都有必要用多线程吗?如果我们求10000000以内的素数有多少个?是不是必须考虑以上两个问题了?多线程和算法优化的目的都是为了提高程序执行的效率.我们首先来考虑算法问题,什么是素数?素数:

筛选法求n以内所有的素数

求n以内所有的素数? 筛选法:将2到n中所有的数都列出来,然后从2开始,先化掉所有2的倍数,然后每次从下一个剩下的数(必然是素数)开始,划掉其内所有的倍数,最后剩下来的数就都是素数 例:13  红色为删除的元素 第一轮 2的倍数: 2 3 4 5 6 7 8 9 10 11 12 13 第二轮 3的倍数: 2 3 4 5 6 7 8 9 10 11 12 13 第三轮 4的倍数: 2 3 4 5 6 7 8 9 10 11 12 13 ..... 1 #include<iostream> 2

求2000以内的素数

方法一:因此判断一个整数m是否是素数,只需把m被 2 ~ m-1 之间的每一个整数去除,如果都不能被整除,那么m就是一个素数. 方法二:另外判断方法还可以简化.m不必被 2 ~ m-1 之间的每一个整数去除,只需被 2 ~  之间的每一个整数去除就可以了.如果m不能被 2 ~ 间任一整数整除,m必定是素数.例如判别17是是否为素数,只需使17被2~4之间的每一个整数去除,由于都不能整除,可以判定17是素数. 原因:因为如果m能被 2 ~ m-1 之间任一整数整除,其二个因子必定有一个小于或等于,

用筛选法求100以内的素数(数组)

用筛选法求100以内的素数,要求使用数组. #include "stdafx.h" #include<iostream> using namespace std; int _tmain(int argc, _TCHAR* argv[]) { int a[100],i,b[100],j,n=0;     //数组a[100]来放用于判断的数,数组b[100]来放素数 for(i=0;i<100;i++)       //注意这里i不能从1开始,因为数组a的首位是a[0]

求n以内的所有素数之Golang实现

package main import (     "fmt"     "math" ) func prime(value int) bool {     if value <= 1 {         return false     }     if value == 2 || value == 3 || value == 5 || value == 7 {         return true     }     if value%2 == 0 || 

埃氏筛法(求n以内有多少个素数)

题目大意:给定整数n,请问n以内有多少个素数 思路:想必要判断一个数是否是素数,大家都会了,并且可以在O(根号n)的复杂度求出答案,那么求n以内的素数呢,那样求就显得有点复杂了,下面看一下这里介绍的??氏算法 其实呢,就是求出第一个素数,然后把n以内它的倍数都删掉就行了,很简单.然后找下一个素数,同样方法····· 看代码 #include<iostream> #include<string.h> #include<map> #include<cstdio>