【问题标题】:How to speed up Sieve of Eratosthenes python list generator如何加快 Eratosthenes python 列表生成器的 Sieve
【发布时间】:2016-04-05 01:55:35
【问题描述】:

我的问题直接来自 CS 圈子网站。这是this 页面底部的最后一个问题,名为“Primed for Takeoff”。基本的概要是他们想要一个长度为 1,000,001 的列表,如果索引是素数,则每个项目的索引为 True,如果不是素数,则每个项目的索引为 False。

因此,例如,isPrime[13] 为 True。 isPrime[14] 为假。

列表“isPrime”的第一个部分看起来像:

isPrime = [False, False, True, True, False, True, False, True, False, False, ...]

我的问题是他们有 7 秒的时间限制。我在下面有一个工作代码,其编号为 101,用于调试目的。当我将它增加到他们要求的 1,000,001 列表长度时,它花费的时间太长了,几分钟后我最终杀死了该程序。 这是我的工作(长度为 101),代码非常慢:

n = 101
c = 2
isPrime = [False,False]
for i in range(2,n):
    isPrime.append(i)

def ifInt(isPrime):
    for item in isPrime:
        if type(item) == int:
            return item
for d in range(2,n):
    if c != None:
        for i in range(c,n,c):
            isPrime[i] = False
        isPrime[c] = True
        c = ifInt(isPrime)

然后我找到了这个here。它的运行时间更快,但只输出素数列表,而不是长度为 n 的列表,其中 list[n] 对素数返回 True,否则返回 false。

我不确定这第二段代码是否真的是在 7 秒内创建长度为 1,000,001 的素数列表的关键,但这是我在研究中能找到的最相关的东西。

def primes_sieve1(limit):
limitn = limit+1
primes = dict()
for i in range(2, limitn): primes[i] = True

for i in primes:
    factors = range(i,limitn, i)
    for f in factors[1:]:
        primes[f] = False
return [i for i in primes if primes[i]==True]

print primes_sieve1(101)

CS 圆圈似乎很常用,但我无法找到适用于 Python 的工作版本。希望这对某人来说是一个简单的解决方案。

这个问题与this one 不同,因为我不是试图只快速创建一个素数列表,而是创建一个从 0 到 n 的所有正整数的列表,这些正整数被 True 标记为素数,而 False 标记为非素数。

【问题讨论】:

  • 您链接的代码中的答案实际上会在 7 秒内生成确切的数组。您应该尝试理解那段非常短的 sn-p 代码的作用。
  • return [i for i in primes if primes[i]==True] 此行仅在索引 i 为质数时过滤质数列表。希望此提示对您有所帮助。
  • 请注意,列出素数完全等同于您描述的真/假数组,并且很可能任何列出素数的程序只需要非常小的更改即可获得布尔数组。
  • @adhdj 我不确定如何以更清晰的方式重复此操作。您链接的答案产生了有问题的列表

标签: python list computer-science


【解决方案1】:

我意识到对 SO 有很多优化,但其他人很少为素筛算法解释它们,因此初学者或算法的第一次创建者很难接近它们。这里的所有解决方案都在 python 中,为了速度和优化在同一页面上。这些解决方案将逐渐变得更快、更复杂。 :)

原版解决方案

def primesVanilla(n):
    r = [True] * n
    r[0] = r[1] = False
    for i in xrange(n):
        if r[i]:
            for j in xrange(i+i, n, i):
                r[j] = False
    return r

这是 Sieve 的一个非常简单的实现。在继续之前,请确保您了解上面发生的情况。唯一需要注意的一点是,您开始在 i+i 而不是 i 处标记非素数,但这很明显。 (因为您假设 i 本身是素数)。为了使测试公平起见,列表中的所有数字都将达到 2500 万

real    0m7.663s  
user    0m7.624s  
sys     0m0.036s  

小改进 1(平方根):

我将尝试按照直截了当的更改对它们进行排序。观察到我们不需要迭代到 n,而只需要向上到 n 的平方根。原因是任何低于 n 的合数必须有一个小于或等于 n 平方根的素数。当您手动筛选时,您会注意到 n 的平方根上的所有“未筛选”数字默认都是素数。

另外一点是,当平方根变成整数时,你必须要小心一点,所以在这种情况下你应该加一个,这样它就会覆盖它。 IE,在 n=49 时,您希望循环直到 7 包含在内,或者您可能会得出结论 49 是素数。

def primes1(n):
    r = [True] * n
    r[0] = r[1] = False
    for i in xrange(int(n**0.5+1)):
        if r[i]:
            for j in xrange(i+i, n, i):
                r[j] = False
    return r

real    0m4.615s
user    0m4.572s
sys     0m0.040s

请注意,它的速度要快得多。仔细想想,你只是在循环直到平方根,所以现在需要 2500 万次顶层迭代的只有 5000 次顶层。

Minor Improvement 2(跳过内循环):

观察到在内循环中,我们可以从 i*i 开始,而不是从 i+i 开始。这是从与平方根非常相似的论点得出的,但重要的是 i 和 i*i 之间的任何复合都已经被较小的素数标记了。

def primes2(n):
    r = [True] * n
    r[0] = r[1] = False
    for i in xrange(int(n**0.5+1)):
        if r[i]:
            for j in xrange(i*i, n, i):
                r[j]=False
    return r

real    0m4.559s
user    0m4.500s
sys     0m0.056s

嗯,这有点令人失望。但是,嘿,它仍然更快。

有些重大改进 3(甚至跳过):

这里的想法是我们可以预先标记所有偶数索引,然后在主循环中跳过 2 次迭代。之后,我们可以从 3 开始外循环,而内循环可以跳过 2*i。 (因为通过 i 反而意味着它会是偶数, (i+i) (i+i+i+i) 等)

def primes3(n):
    r = [True] * n
    r[0] = r[1] = False
    for i in xrange(4,n,2):
        r[i] = False    
    for i in xrange(3, int(n**0.5+1), 2):
        if r[i]:
            for j in xrange(i*i, n, 2*i):
                r[j] = False
    return r

real    0m2.916s
user    0m2.872s
sys     0m0.040s

Cool Improvements 4(wim 的想法):

这个解决方案是一个相当高级的技巧。切片赋值比循环快,所以这里使用 python 的切片表示法:r[begin:end:skip]

def primes4(n):
    r = [True] * n
    r[0] = r[1] = False 
    r[4::2] = [False] * len(r[4::2])
    for i in xrange(3, int(1 + n**0.5), 2):
        if r[i]:
            r[i*i::2*i] = [False] * len(r[i*i::2*i])
    return r

10 loops, best of 3: 1.1 sec per loop

轻微改进 5

请注意,python 在计算长度时会重新切分r[4::2],因此这需要相当多的额外时间,因为我们只需要计算长度即可。不过,我们确实使用了一些讨厌的数学来实现这一点。

def primes5(n):
    r = [True] * n
    r[0] = r[1] = False 
    r[4::2] = [False] * ((n+1)/2-2)
    for i in xrange(3, int(1 + n**0.5), 2):
        if r[i]:
            r[i*i::2*i] = [False] * ((n+2*i-1-i*i)/(2*i))
    return r

10 loops, best of 3: 767 msec per loop

作业加速(Padraic Cunningham):

请注意,我们为数组分配了所有 True,然后将 half(偶数)设置为 False。我们实际上可以从一个交替的布尔数组开始。

def primes6(n):
    r = [False, True] * (n//2) + [True]
    r[1], r[2] = False, True
    for i in xrange(3, int(1 + n**0.5), 2):
        if r[i]:
            r[i*i::2*i] = [False] * ((n+2*i-1-i*i)/(2*i))
    return r

10 loops, best of 3: 717 msec per loop

不要引用我的话,但我认为没有一些讨厌的数学方法,最后一个版本没有明显的改进。我尝试过的一个可爱的属性,但并没有变得更快,它注意到除了 2,3 之外的素数必须是 6k+1 或 6k-1 的形式。 (注意,如果是 6k,那么可以被 6 整除,6k+2 | 2, 6k+3 | 3, 6k+ 4 | 2, 6k+5 等于 -1 mod 6。这表明我们每次可以跳过 6并检查双方。无论是从我这边的糟糕实现还是python内部,我都找不到任何有意义的速度提升。:(

【讨论】:

  • +1 不错。我一直认为 python 中的列表切片应该是视图,而不是副本。我很难过这不是在 python3 中完成的。
  • 创建一个切片只是为了找到它的长度是相当浪费时间和内存的,因为它很容易计算正确的长度。见我的boolean prime sieve
  • @PM2Ring,我完全同意。你能弄清楚 len(r[i*i::2*i]) 的长度应该是多少吗?我昨天没能正确计算:(
  • 另外,请注意,我跳过的算法比你的算法多一点,所以计算有点不同。
  • @PadraicCunningham,进行了彻底的检修,在之前的代码中有一些令人尴尬的错误。现在一切都应该在 python 2.7.9 下运行。
【解决方案2】:

我看到的第一件事是生成初始列表的方式(循环和追加)效率低下且不必要。您可以只 add 列表而不是循环和附加每个元素。

我看到的第二件事是你正在做的类型检查是不必要的,那个函数调用很昂贵,你可以重构来完全避免这种情况。

最后,我认为您可以在任何筛子实现中获得的“大事”是利用切片分配。您应该一次删除所有因素,而不是循环。示例:

from math import sqrt

def primes(n):
    r = [True] * n
    r[0] = r[1] = False
    r[4::2] = [False] * len(r[4::2])
    for i in xrange(int(1 + sqrt(n))):
        if r[i]:
            r[3*i::2*i] = [False] * len(r[3*i::2*i])
    return r

请注意,我还有一些其他技巧:

  • 立即划掉偶数,避免一半的工作。
  • 只需要迭代到 sqrt 的长度

在我那蹩脚的动力不足的 macbook 上,这段代码可以在大约 75 毫秒内生成 1,000,001 个列表:

>>> timeit primes(1000001)
10 loops, best of 3: 75.4 ms per loop

【讨论】:

  • 我认为您应该使用range 而不是enumerate,因为这样会复制列表。
  • 枚举不会,但切片会。
  • 啊,明白了。好主意,我会解决的。
  • 我不认为偶数交叉可以节省任何工作,考虑到当你在 2 上正常运行它时会发生这种情况。现在用的是骗人的说法吗?对于疯狂快速的初筛和简洁的优化,也已经有了很好的答案。为什么不链接到一个?
  • 如果你能在效率上打败它,同时保持简单,我会很感兴趣:)
【解决方案3】:

python2 和 3 中显示的一些时序 wim 的方法明显更快,可以通过创建列表的方式进一步稍微优化:

def primes_wim_opt(n):
    r = [False, True] * (n // 2)
    r[0] = r[1] = False
    r[2] = True
    for i in xrange(int(1 + n ** .5)):
        if r[i]:
            r[3*i::2*i] = [False] * len(r[3*i::2*i])
    return r

Python2 时序:

In [9]: timeit primesVanilla(100000)
10 loops, best of 3: 25.7 ms per loop

In [10]: timeit primes_wim(100000)
100 loops, best of 3: 3.59 ms per loop

In [11]: timeit primes1(100000)
100 loops, best of 3: 14.8 ms per loop

In [12]: timeit primes_wim_opt(100000)
100 loops, best of 3: 2.18 ms per loop

In [13]: timeit primes2(100000)
100 loops, best of 3: 14.7 ms per loop

In [14]: primes_wim(100000) ==  primes_wim_opt(100000) ==  primes(100000) == primesVanilla(100000) == primes2(100000)
Out[14]: True

python3 使用相同函数只是更改为范围的时间:

In [76]: timeit primesVanilla(100000)
10 loops, best of 3: 22.3 ms per loop

In [77]: timeit primes_wim(100000)
100 loops, best of 3: 2.92 ms per loop

In [78]: timeit primes1(100000)
100 loops, best of 3: 10.9 ms per loop

In [79]: timeit primes_wim_opt(100000)
1000 loops, best of 3: 1.88 ms per loop

In [80]: timeit primes2(100000)
100 loops, best of 3: 10.3 ms per loop
In [81]: primes_wim(100000) ==  primes_wim_opt(100000) ==  primes(100000) == primesVanilla(100000) == primes2(100000)
Out[80]: True

可以通过使用 range/xrange 的 len 而不是切片来进一步优化:

def primes_wim_opt(n):
    is_odd = n % 2 & 1    
    r = [False, True] * (n // 2 + is_odd)
    r[0] = r[1] = False
    r[2] = True
    for i in range(int(1 + n ** .5)):
        if r[i]:
            r[3*i::2*i] = [False] * len(range(3*i,len(r), 2 * i))
    return r

Python3 它敲掉了一大块:

In [16]: timeit primes_wim_opt_2(100000)
1000 loops, best of 3: 1.38 ms per loop

对于使用 xrange 的 python2 也是如此:

In [10]: timeit  primes_wim_opt_2(100000)
1000 loops, best of 3: 1.60 ms per loop

使用(((n - 3 * i) // (2 * i)) + 1) 也应该可以工作:

def primes_wim_opt_2(n):
    is_odd = n % 2 & 1
    r = [False, True] * ((n // 2) + is_odd)
    r[0] = r[1] = False
    r[2] = True
    for i in range(int(1 + n ** .5)):
        if r[i]:
            r[3*i::2*i] = [False] * (((n - 3 * i) // (2 * i)) + 1)
    return r

速度稍微快一点:

In [12]: timeit primes_wim_opt_2(100000)
1000 loops, best of 3: 1.32 ms per loop

In [6]: timeit primes5(100000)
100 loops, best of 3: 2.47 ms per loop

您也可以从第 3 步和第 2 步开始:

def primes_wim_opt_2(n):
    r = [False, True] * (n // 2)
    r[0] = r[1] = False
    r[2] = True
    for i in range(3, int(1 + n ** .5),2):
        if r[i]:
            r[3*i::2*i] = [False] * (((n - 3 * i) // (2 * i)) + 1)
    return r

哪个更快:

In [2]: timeit primes_wim_opt_2(100000)
1000 loops, best of 3: 1.10 ms per loop

Python2:

In [2]: timeit primes_wim_opt_2(100000)
1000 loops, best of 3: 1.29 ms per loop

【讨论】:

  • 你也成了计算的牺牲品 :) 1000001 的切片长度问题
  • 您介意我在答案中添加 [False,True] 技巧吗?加速 10% :)
  • "使用 range/xrange 的 len 代替切片"
猜你喜欢
  • 1970-01-01
  • 2012-02-21
  • 2018-08-10
  • 1970-01-01
  • 1970-01-01
  • 2012-12-02
  • 1970-01-01
  • 1970-01-01
  • 2023-04-07
相关资源
最近更新 更多