这比Eratosthenes筛更有效吗?

时间:2013-05-22 11:30:41

标签: python performance primes sieve-of-eratosthenes

我在Python 2.7中编写了一个用于创建素数列表的代码。代码是

def primes_list(num):
    ans = [2]
    for i in range(3, num, 2):
        for j in ans:
            if i % j == 0:
                break
        else:
            ans.append(i)
    else:
        return ans

这比Sieve of Eratosthenes更有效吗?我认为内存效率应该更好但我对时间效率有疑问。如何计算时间和内存效率以及如何对效率进行基准测试?

4 个答案:

答案 0 :(得分:4)

你正在做的是trial divison - 测试每个候选素数与其下面的每个已知素数。在调用range时跳过奇数将为你节省一些分数,但这正是基于以下方法的筛选技术:你知道每一个数字都可以被2整除,因此是复合数。筛子只是扩展到:

  • 每三个数字可以除以三,因此是复合的;
  • 每隔五乘五,因此复合

等等。由于Sieve被认为是最具时效性的算法之一,并且试验部门是 least 之一(维基百科将筛子描述为O(n log log n),并根据以下评论,算法可能是O(n^2 / log n))),可以合理地假设具有类似Sieve的优化的试验部门没有筛选。

答案 1 :(得分:3)

不,那个试验师在时间复杂性上比eratosthenes的筛子差得多。它的空间复杂度有点更好,但是,因为素数约为n/log(n),所以你不会节省大量的内存。也可以使用位向量来完成筛选,这会将常数减少32/64倍(因此实际目的可能会更好)。


显示时间差异的小基准:

>>> timeit.timeit('primes_list(1000)', 'from __main__ import primes_list', number=1000)
0.901777982711792
>>> timeit.timeit('erat(1000)', 'from __main__ import erat', number=1000)
0.2097640037536621

正如您所看到的,即使使用n=1000 eratosthenes也要快4倍。 如果我们将搜索范围增加到10000

>>> timeit.timeit('primes_list(10000)', 'from __main__ import primes_list', number=1000)
50.41101098060608
>>> timeit.timeit('erat(10000)', 'from __main__ import erat', number=1000)
2.3083159923553467

现在eratosthenes的速度提高了21倍。正如你所看到的,很明显,eratosthenes 更快。


使用numpy数组很容易将内存减少32或64(取决于您的机器架构)并获得更快的结果:

>>> import numpy as np
>>> def erat2(n):
...     ar = np.ones(n, dtype=bool)
...     ar[0] = ar[1] = False
...     ar[4::2] = False
...     for j in xrange(3, n, 2):
...             if ar[j]:
...                     ar[j**2::2*j] = False
...     return ar.nonzero()[0]
... 
>>> timeit.timeit('erat2(10000)', 'from __main__ import erat2', number=1000)
0.5136890411376953

其他筛子比其他筛子快4倍。

答案 2 :(得分:1)

“如何衡量效率?”

衡量empirical orders of growth,就是这样! :)

e.g。来自the accepted answerlog_10(50.41/0.9) = 1.75log_10(2.31/0.21) = 1.04的数据,因此您的TD(试验部门)代码的经验增长顺序为 ~ n^1.75 对于Eratosthenes的筛子,范围为1,000 ... 10,000)与 ~ n^1.04

后者与 n log log n 一致,前者与 n^2 / log(n)^2 一致,应该如此。

使用g(n) = n^2 / log(n)^2,我们得到g(10000)/g(1000) = 56.25,这只比经验值50.41/0.9 = 56.01低0.4%。但是对于g2(n) = n^2 / log(n),我们有g2(10000)/g2(1000) = 75,这与证据不同。

关于时间复杂度:实际上,大多数复合材料早期失效(是小素数的倍数)。要生成k=n/log(n)素数,需要O(k^2)次,在所有前面的素数a.o.t中测试每个素数。只是那些不超过平方根的那些。

复合材料不会增加复杂性(M. ONeill的JFP文章(pg 4)中的精确分析为复合材料提供了与测试sqrt时质数相同的复杂性 - 每种复合材料都得到保证具有不大于其sqrt的素因子 - 因此复合材料的复杂性甚至低于此处素数的复杂性。

总的来说,它是O(k^2),也就是O(n^2/log(n)^2)


只需添加两行,就可以大大提高代码的速度,从O(n^2/log(n)^2)O(n^1.5/log(n)^2)时间复杂度:

    for j in ans: 
        if j*j > i: 
            ans.append(i); break; 
        if i % j == 0:
            break
    # else:
    #     ans.append(i)

时间复杂度的提高意味着17.8x的运行时间比率为10,000,超过1,000,而不是之前的56.25x。这转换为 ~ n^1.25 此范围内经济增长的经验顺序(而不是 ~ n^1.75 )。 10,000次通话的绝对运行时间将比旧的50.41秒更接近2.31秒。


BTW你的原始代码相当于David Turner着名的代码,

primes = sieve [2..]
sieve (x:xs) = x : sieve [y | y <- xs, rem y x /= 0]

和改进的代码,对此:

primes = 2 : sieve [3..] primes 
sieve xs (p:ps) | (h,t) <- span (< p*p) xs =
              h ++ sieve [y | y <- t, rem y p /= 0] ps 

(代码在Haskell中,我认为它足够可读。x:xs代表一个列表,其中x是head元素,xs列表的其余部分。)

答案 3 :(得分:0)

由于Python 2中的range返回一个列表,因此算法的空间复杂度仍为O(n),因此它的空间效率不高(至少不是渐近)。如果你使用了xrange(或Python 3中的范围),那么它会更节省空间,因为你只存储素数 - 而不是所有数字都高达n

你的时间效率会比筛子更差。