项目Euler 10 - 为什么第一个python代码运行速度比第二个快得多?

时间:2012-02-10 19:11:36

标签: python primes number-theory

Project Euler中的第10个问题:

  

低于10的素数之和为2 + 3 + 5 + 7 = 17.

     

找出200万以下所有素数的总和。

我找到了这个片段:

sieve = [True] * 2000000 # Sieve is faster for 2M primes
def mark(sieve, x):
    for i in xrange(x+x, len(sieve), x):
        sieve[i] = False

for x in xrange(2, int(len(sieve) ** 0.5) + 1):
    if sieve[x]: mark(sieve, x)

print sum(i for i in xrange(2, len(sieve)) if sieve[i]) 

发布here 运行3秒钟。

我写了这段代码:

def isprime(n):
    for x in xrange(3, int(n**0.5)+1):
        if n % x == 0:
            return False
    return True

sum=0;
for i in xrange(1,int(2e6),2):
    if isprime(i):
        sum += i

我不明白为什么我的代码(第二个)慢得多?

4 个答案:

答案 0 :(得分:10)

您的算法分别从2到N(其中N = 2000000)检查每个数字的原始性。

Snippet-1使用大约2200年前发现的 sieve of Eratosthenes 算法。 它不会检查每个数字,但是:

  • 对所有数字进行“筛选”,从2到2000000。
  • 找到第一个数字(2),将其标记为素数,然后从筛子中删除其所有倍数。
  • 然后找到下一个未删除的数字(3),将其标记为素数并从筛子中删除其所有倍数。
  • 然后找到下一个未删除的数字(5),将其标记为素数并从筛子中删除其所有倍数。
  • ...
  • 直到找到素数1409并从筛子中删除所有倍数。
  • 然后找到所有1414~ = sqrt(2000000)的素数并停止
  • 不必检查从1415到2000000的数字。所有未被删除的人都是素数。

因此算法产生的所有素数都达到N.

请注意,它不进行任何除法,只进行加法(甚至不是乘法,而不是因为数字如此之小而重要,但可能会有更大的数字)。时间复杂度为O(n loglogn),而您的算法接近O(n^(3/2))(或@ O(n^(3/2) / logn)为@Daniel Fischer评论),假设分割成本与乘法相同。

来自维基百科(上面链接)的文章:

  

随机访问机器模型的时间复杂度是O(n log log n)运算,这是prime harmonic series渐近逼近log log n这一事实的直接后果。

(在这种情况下为n = 2e6

答案 1 :(得分:4)

第一个版本预先计算范围内的所有素数并将它们存储在sieve数组中,然后找到解决方案就是在数组中添加素数的简单问题。它可以被视为memoization的一种形式。

第二个版本测试范围内的每个数字,以查看它是否为素数,重复先前计算已经完成的大量工作。

总之,第一个版本避免重新计算值,而第二个版本一次又一次地执行相同的操作。

答案 2 :(得分:2)

为了便于理解差异,请尝试考虑将每个数字用作潜在分隔符的次数:

在您的解决方案中,当该数字作为素数进行测试时,将测试数字2的每个数字。您沿途传递的每个号码都将用作下一个号码的潜在分隔符。

在第一个解决方案中,一旦你跨过一个你永远不会回头的数字 - 你总是从你到达的地方向前移动。顺便说一句,一个可能的常见优化是只有在你标记为2之后才能获得奇数:

mark(sieve, 2)
for x in xrange(3, int(len(sieve) ** 0.5) + 1, 2):
    if sieve[x]: mark(sieve, x)

通过这种方式,您只需查看每个数字并清除其所有乘法,而不是一次又一次地检查所有可能的分频器,并检查每个数字及其所有前一个数字,而if语句会阻止您为你以前遇到的号码重复工作。

答案 3 :(得分:2)

正如Óscar的回答所示,你的算法重复了很多工作。要查看其他算法保存的处理量,请考虑mark()isprime()函数的以下修改版本,这些函数会跟踪调用函数的次数以及for循环的总次数迭代:

calls, count = 0, 0
def mark(sieve, x):
    global calls, count
    calls += 1
    for i in xrange(x+x, len(sieve), x):
        count += 1
        sieve[i] = False

使用这个新函数运行第一个代码后,我们可以看到mark()被调用223次,for循环中总共有4,489,006(~450万)次迭代。

calls, count = 0
def isprime(n):
    global calls, count
    calls += 1
    for x in xrange(3, int(n**0.5)+1):
        count += 1
        if n % x == 0:
            return False
    return True

如果我们对您的代码进行类似的更改,我们可以看到isprime()被称为1,000,000(100万)次,其中for循环的177,492,735(~177.5百万)次迭代。

计算函数调用和循环迭代并不总是确定算法为什么更快的决定性方法,但通常更少的步骤= =更少的时间,并且显然您的代码可以使用一些优化来减少步骤数。