简介:本文主要记录了博主对一段使用python实现的素数生成代码的不断优化过程。
背景:最近在刷Project Euler的题目,刷到第十题(计算2百万以下素数的和)的时候发现之前的素数生成代码效率太低导致几分钟都出不来。于是通过不断的调优,终于得到一个能在秒级算出2百万以内的素数的generator。 本文的调优过程基本不涉及基于数论的调优,如果您希望得到一个拥有极致性能的python素数生成代码,可以使用pyprimes
第一版
素数也即:无法被除了1和本身以外的任何自然数整除的自然数。因此第一版的程序实现起来也格外的直白。把从2
开始到数值本身-1
范围内的自然数都去除一下待判断的数就能得到结论了。于是就有了第一版程序如下:
class Prime:
def __init__(self):
self.prime_list = []
self.v = 3
def get_prime(self):
yield 2
while True:
for i in range(2, self.v):
if self.v % i == 0:
break
else:
self.prime_list.append(self.v)
yield self.v
self.v += 1
第二版
当然这段代码对于小的素数是可以work的。单当使用这段代码调试Euler第三题的时候就会发现在判断大数是否是素数时耗时很久。判断一个数是否是素数的时间复杂度目前是O(N^2)
。于是就想到需要通过减少判断次数来达到提升代码效率的方法。最容易想到的一种方法是:大于被判断值的平方根的数,无需再去判断是否可以整除被判断数了。现在单个素数的判断时间复杂度优化到O(logN)
class Prime:
def __init__(self):
self.prime_list = [2]
self.v = 3
def get_prime(self):
yield 2
while True:
for i in (3, self.v):
if self.v % i == 0:
break
elif i * i > self.v + 1:
yield self.v
self.prime_list.append(self.v)
break
else:
self.prime_list.append(self.v)
yield self.v
self.v += 1
第三版
当然这还远远不够,接踵而来的是第7题(找到第1000个素数)。寻找第N个素数的命题大大放大了我们时间复杂度。目前程序的时间复杂度是O(NlogN)
。于是再次寻找是否还有其他可以跳过的检查项。我们可以发现所有偶数其实是可以完全可以跳过不判断的。但是判断偶数本身也是一次计算操作,所以简单的通过if self.v % 2 == 0
来跳过一次检查并不能带来很大的性能提升,所以这里用了一个比较tricky方式跳过偶数:由于序列是从3开始检查,因此把增长步进调整为2,那么就自然跳过了所有偶数
class Prime:
def __init__(self):
self.prime_list = []
self.v = 3
def get_prime(self):
yield 2
while True:
for i in self.prime_list:
if self.v % i == 0:
break
else:
self.prime_list.append(self.v)
yield self.v
self.v += 2
这部分优化后,找到第N个素数的时间比原来少了一半。
第四版(最终版)
实际运行后发现即使时间相较之前少了一半,但总体运行时间仍然不理想(十几秒级别)。冥想。。。过后又有一个大招:由于所有合数都能写成素数相乘,因此如果能够被某个合数整除,也必然能被某个素数整除。那么我们的所有除数只需从被判断的数小的素数集合中选择即可。于是得到以下优化代码
class Prime:
def __init__(self):
self.prime_list = [2]
self.v = 3
def get_prime(self):
yield 2
while True:
for i in self.prime_list:
if self.v % i == 0:
break
elif i * i > self.v + 1:
yield self.v
self.prime_list.append(self.v)
break
else:
self.prime_list.append(self.v)
yield self.v
self.v += 2
这部分优化后,找到第N个素数的时间从O(N/2logN)
变成了O((logN)^2)