【问题标题】:Is this more efficient than Sieve of Eratosthenes?这比埃拉托色尼筛法更有效吗?
【发布时间】:2013-05-22 11:30:41
【问题描述】:

我在 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 更有效?我认为内存效率应该更好,但我对时间效率有疑问。如何计算时间和内存效率以及如何对效率进行基准测试?

【问题讨论】:

标签: python performance primes sieve-of-eratosthenes


【解决方案1】:

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

  • 每三个数都可以被三整除,因此是合数;
  • 每五分之一乘五,因此是复合的

等等。由于筛子被认为是可用的最省时的算法之一,而试验划分是最少之一(维基百科将筛子描述为O(n log log n),并且根据下面的 cmets,您的算法是可能是 O(n^2 / log n)) ) ,因此可以合理地假设具有一些类似筛子优化的试除法无法进行筛分。

【讨论】:

  • 那些时间复杂度不可能是正确的。使用试除法检查单个数字的素数确实是O(sqrt n)(如果你只尝试到 sqrt(n) 的数字,这不会在这里发生),但是不可能找到直到 n 的所有素数亚线性。
  • @sepp2k 可能 lvc 表示每个素数的O(sqrt(n)),这应该导致总共O(n sqrt(n)),筛子也是如此。它实际上是一个O(nlog(log(n))) 算法。
  • @Bakuriu 这可能是他的意思,但 OP 的代码也不是 O(n sqrt(n))。这是O(n * number_of_primes_below(n))(也可能是Theta),根据你在回答中关于n以下的素数在n / log n左右的回答中所说的,这将是O(n^2 / log n)
  • @sepp2k 确实如此。缺少的n 是一个错字,但审判部门的整体复杂性仅基于维基百科所说的内容。我很难理解它的逻辑,而你的逻辑更有意义。
  • 感谢 cmets 和解释。这对我有帮助。
【解决方案2】:

不,那是试除法,在时间复杂度上比埃拉托色尼筛法差得多。它的空间复杂度稍微好一点,但是,由于素数大约是n/log(n),你并没有节省大量的内存。筛子也可以使用位向量来完成,将 constants 减少 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=1000eratosthenes 也快 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 倍。

【讨论】:

  • 我的记忆问题是关于标志的。它们必须是 n 阶。我试图使用位向量,但它们是 .tar 文件。我该如何使用它们?对 Python 完全陌生,所以不知道如何在我的脚本中导入自定义模块。关于比较 2 个不同 python 脚本的效率有什么想法吗?
  • @Zel Memory O(n/log(n))O(n) 变化不大。您也可以使用incremental sieve 来避免同时存储直到n 的所有数字。
  • 我在docs.python.org/2/library/timeit.html 上查找了timeit.timeit,最后一个示例与您使用的类似。您在示例末尾使用的数字参数是什么。你刚才也定义了erat吗?
  • @Zel 如果您对计时算法感兴趣,您应该学习使用它,并查看profile 模块。您可能还对this 有关内存分析的问题感兴趣。我运行了您在此处看到的代码(性能可能会根据您的计算机处理矢量化操作等的方式而改变)。 erat 函数是erat2 的纯python 版本,使用显式for 循环而不是切片。
  • 感谢您的建议。只有一件事,我如何添加新模块?我到处都能找到的解释是针对 linux/unix 系统的,但我使用的是 Windows。有什么建议吗?
【解决方案3】:

“如何衡量效率?”

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

例如使用来自 the accepted answerlog_10(50.41/0.9) = 1.75log_10(2.31/0.21) = 1.04 的数据,因此它是 ~ n^1.75 TD(试用部门)代码的经验增长顺序(在 1,000 ... 10,000 范围内)与 ~ n^1.04 相比,Eratosthenes 的筛子。

后者与 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 秒。


顺便说一句,您的原始代码等同于 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 是头部元素,xs 是列表的其余部分)。

【讨论】:

    【解决方案4】:

    由于 Python 2 中的 range 返回一个列表,因此您的算法的空间复杂度仍然是 O(n),因此它的空间效率并不高(至少不是渐近的)。如果您使用 xrange(或 Python 3 中的 range),它的空间效率会更高一些,因为您只存储素数 - 不是所有直到 n 的数字。

    无论哪种方式,您的时间效率都会比筛子差。

    【讨论】:

    • 最后一句没看懂。你能改写一下吗?
    猜你喜欢
    • 1970-01-01
    • 2011-12-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-07-03
    相关资源
    最近更新 更多