Python一行代码实现质数生成器

6
我将尝试用一行Python代码生成质数,仅作为有趣的练习。 以下代码的功能正常,但速度太慢:
primes = lambda q: (i for i in xrange(1,q) if i not in [j*k for j in xrange(1,i) for k in xrange(1,i)])
for i in primes(10):
   print i,

所以我尝试只检查j和k的平方根:
primes = lambda q: (i for i in xrange(1,q) if i not in [j*k for j in xrange(1,int(round(math.sqrt(i)+1))) for k in xrange(1,int(round(math.sqrt(i)+1)))])
for i in primes(10):
   print i,

但是它输出的是:2 3 5 6 7 8

因此,我的索引 j 和 k 必须有问题,但我一点头绪也没有。


如果您可以接受非单行代码,这里有一个问题:https://dev59.com/6XRB5IYBdhLWcg3wn4fc - Andy
可能是重复的问题:Python- 厄拉多塞筛法- 紧凑的Python - ninjagecko
2
我能够用两行代码实现它: http://stackoverflow.com/a/9302299/5987 - Mark Ransom
1
你可以利用列表推导式内部正在构建的列表,使用表达式 locals['_[1]'] 来引用它。 - APerson
这是我今天看到的第三个要求一行代码的问题。除非你是Linus Torvalds并设计Linux,否则我不理解将多个LOC塞入单行的热潮。 - Abhijit Sarkar
@AbhijitSarkar “single-line” 的意思是单表达式,我认为。 - Will Ness
12个回答

14

这并不是埃拉托斯特尼筛法,尽管它看起来像是。实际上,它要糟糕得多。筛法是找素数的最佳算法。

参见 http://en.wikipedia.org/wiki/Sieve_of_Eratosthenes

编辑:我已经修改了https://stackoverflow.com/a/9302299/711085,使其成为一行代码(原本它不是真正的筛法,但现在可能是...):

reduce( (lambda r,x: r-set(range(x**2,N,x)) if (x in r) else r), 
        range(2,N), set(range(2,N)))

演示:
>>> primesUpTo(N): lambda N: reduce(...)
>>> primesUpTo(30)
{2, 3, 5, 7, 11, 13, 17, 19}

很遗憾,我认为尽管这在函数式编程语言中很有效,但由于Python的非持久性(共享状态和不可变)数据结构,它可能不像预期那样高效,任何Python筛子都需要使用突变来达到可比较的性能。如果我们非常想要,仍然可以将其塞入一行代码中。但首先...
正常的筛子:
>>> N = 100
>>> table = list(range(N))
>>> for i in range(2,int(N**0.5)+1):
...     if table[i]:
...         for mult in range(i**2,N,i):
...             table[mult] = False
... 
>>> primes = [p for p in table if p][1:]
>>> primes
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]

现在我们可以在同一行上定义和调用匿名函数,以及使用[...].__setitem__的hack来进行内联变异,以及使用... and foo的hack来在返回foo的同时评估...
>>> primesUpTo = lambda N: (lambda table: [[table.__setitem__(mult,False) for mult in range(i**2,N,i)] for i in range(2,int(N**0.5)+1) if table[i]] and [p for p in table if p][1:])(list(range(N)))
>>> primesUpTo(30)
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

继续惊恐地蜷缩,这个单行代码被扩展了(因为你几乎可以直接翻译控制流而变得奇怪美丽,但却滥用了一切):

lambda N:
    (lambda table: 
        [[table.__setitem__(mult,False) for mult in range(i**2,N,i)] 
            for i in range(2,int(N**0.5)+1) if table[i]] 
        and [p for p in table if p][1:]
    )(list(range(N)))

这个一行式的变异版本在我的计算机上在 108 左右就放弃了,而原始的变异版本在大约 109 处放弃,由于内存不足(奇怪)。

原始的 reduce 版本在 107 时放弃。因此,它可能并不是那么低效(至少对于您可以处理的计算机数字来说)。

编辑2 看来您可以更简洁地滥用副作用:

reduce( (lambda r,x: (r.difference_update(range(x**2,N,x)) or r)
                     if (x in r) else r), 
        range(2,N), set(range(2,N)))

它在大约108时放弃,与单行变异版本相同。

编辑3:这个运行时间为O(N)经验复杂度,而没有difference_update的情况下,它运行在O(n^2.2)的复杂度

限制减少范围,到上限的平方根,并且只处理奇数,都会导致额外的加速(分别为2倍1.6倍):

reduce( (lambda r,x: (r.difference_update(range(x*x,N,2*x)) or r)
                     if (x in r) else r), 
        range(3, int((N+1)**0.5+1), 2),
        set([2] + range(3,N,2)))

1
如果有人想知道为什么我一直在编辑/取消编辑,那是因为极其难以确定Sieve的非正统实现是否具有http://en.wikipedia.org/wiki/Sieve_of_Eratosthenes#Algorithm_complexity的运行时间;即使现在也要确保...不幸的是,我认为虽然这在函数式编程语言中很有效,但由于Python中的非共享数据结构,任何Python中的筛子都需要使用突变。 - ninjagecko
1
@MarkRansom 不,这就是真正的埃拉托斯特尼筛法所做的事情,它确实会多次删除倍数-每个数字的质因数都要删除一次。这是因为在使用数组时,它不是“删除”它们,而是将它们标记掉。从数组中删除条目会破坏随机访问,这是筛法效率的关键-每个标记只需要O(1)时间。对于集合来说,可能是O(log(size-of-set)),所以稍微差一点。在仅处理奇数时,交叉删除range(i**2,N,2*i)的改进确实值得编辑(特别是使用集合)。 :)(请注意,在仅处理奇数时,它是2*i)。 - Will Ness
@ninjagecko(你的最后一条评论),你的意思是 2i,3i ...(i-1)* i 被消除了。i 最好保持不变。:) 关于那个“优化”,如果访问复杂度不是 O(1) 而是 O(log(size-of-set)),那么它应该会有很大的回报,特别是在处理集合时。然后你确实希望尽可能少地进行划掉操作。因此,从奇数开始而不是偶数开始:reduce((lambda r,x: r-set(range(x*x,N,2*x))), range(3,int((N+1)**0.5+1),2), set([2]+range(3,N,2))) - Will Ness
@WillNess,我的意思是真正的埃拉托斯特尼筛法只标记质数的倍数,而不是每个奇数的倍数。 奇数比质数要多得多,使它变得不必要地缓慢。 - Mark Ransom
啊,是的,当然。:) 即使使用质数,许多/一些合数也会被多次消除。 - Will Ness
显示剩余10条评论

4
如何呢:
def primes(x):
  return [i for i in range(2,x) if 0 not in [i%j for j in range(2,i)]]

1
你可以将其重写为以下更短的形式: def primes(x): return [i for i in range(2,x) if all([i%j for j in range(2,i)])] - Eitan
1
如果数字大于1,则应添加一个条件,如下所示:[i for i in range(2,x) if ((0 not in [i%j for j in range(2,i)]) and i > 1)] - Atıfcan Ergin
为什么?它被 i in range(2,x) 覆盖了。 - Questions

3

这是您想要的内容:

def primes (q) :
 # return (i for i in xrange(2,q) if i not in [j*k for j in xrange(1,i) for k in xrange(1,i)])
 # return (i for i in xrange(2,q) if i not in [j*k for j in xrange(1,i) for k in xrange(1,j+1)])
 # return (i for i in xrange(2,q) if i not in [j*k for j in xrange(1,i/2+1) for k in xrange(1,j+1)])

 return (i for i in xrange(2,q) if i not in [j*k for j in xrange(1,i/2+1) for k in xrange(1,min(j+1,i/j+1))])

在Haskell中,范围是包含的,因此primes(542)是:
[n | n<-[2..541], not $ elem n [j*k | j<-[1..n-1],     k<-[1..n-1]]]  --  25.66s
[n | n<-[2..541], not $ elem n [j*k | j<-[1..n-1],     k<-[1..j]]]    --  15.30s
[n | n<-[2..541], not $ elem n [j*k | j<-[1..n`div`2], k<-[1..j]]]    --   6.00s
                                                                      --   0.79s
[n | n<-[2..541], not $ elem n [j*k | j<-[1..n`div`2], k<-[1..min j (n`div`j)]]] 

事实上,1*x == x,所以1不需要作为乘数,因此应该是

[n | n<-[2..541], not $ elem n [j*k | j<-[2..n`div`2], k<-[2..min j (n`div`j)]]] 

这需要仅0.59秒的时间。或者,用Python实现,

def primes (q) :
 return (i for i in xrange(2,q) if i not in [j*k for j in xrange(2,i/2+1) for k in xrange(2,min(j+1,i/j+1))])

更新:由于某些原因,在Haskell中使用min j ...并没有太大区别。因此,表达式变得更加简单。

[n | n<-[2..541], not $ elem n [j*k | j<-[2..n`div`2], k<-[2..n`div`j]]] 

3
不能仅通过平方根来检查数字的乘积以测试素数。以8为例- 8的平方根是2.8,因此它永远不会尝试4 * 2。(实际上,唯一不被视为素数的数字是平方数)。
ETA:与其尝试所有可能的j和k组合,为什么不检查i是否可被每个小于等于j的平方根的数字整除(使用“ i%j == 0”)? 这既需要更少的代码又更加高效(虽然它仍然远不及埃拉托斯特尼筛法效率高)。

1
这是一个基于埃拉托斯特尼筛法的一行代码。
primes = lambda N: (N>1)*[2] + [ p for s in [[1]*(N+1)]
                    for p in range(3,N+1,2) if s[p] 
                    and not s.__setitem__(slice(p,None,p),N//p*[0])]
                                        

在我的笔记本电脑上,它可以在0.097秒内生成不超过1,000,000的质数。

对于不超过10^7的质数:1.15秒,

对于不超过5*10^7的质数:6.43秒,

对于不超过10^8的质数:12.1秒,

对于不超过5*10^8的质数:67.1秒,

对于不超过10^9的质数:156秒(2.6分钟),

对于不超过10^10的质数:会导致IDLE shell崩溃。

性能曲线基本上是线性的,直到内存管理(虚拟内存页面池交换)开始干扰为止。例如,10^9个筛子标志占用了28G的内存,这超过了我的计算机的物理内存。

为了减少内存消耗(并在更大的集合上提高性能达15%),筛子可以限制只标记奇数,因为2已经单独处理:

primes = lambda N: (N>1)*[2] + [ p for s in [(N+1)//2*[1]]
                   for i,p in enumerate(range(3,N+1,2)) if s[i] 
                   and not s.__setitem__(slice(i,None,p),
                                         (len(s)-i+p-1)//p*[0])]

1
是的,最后一次更改弄坏了它(我的错)。我最终回到了我的原始答案。为了优化零赋值,需要进行额外的计算,但实际上使其变慢了。低级别的切片赋值(即使数量增加一倍)比额外的Python代码来确定切片大小要快得多。 - Alain T.
是的,我有点怀疑,但还是希望能有办法。 :) - Will Ness
原来将p*pp*2保存在局部变量中有助于使其比这个答案中的代码更快,大约快了20%(https://ideone.com/7RTBKj)。但是并没有复杂度上的改进,只是常数因子的提升(正如理论所说)。祝好,:) - Will Ness
你是如何衡量这些时间改进的呢?当我比较原始版本和你改进后的版本时,我发现你的版本要慢20-25%。(尽管我使用的是Python 3.7) - Alain T.
1
令人惊讶的是,确实是版本的差异。我尝试在我为另一个项目安装的Python 3.9.2版本上进行了测试,得到了与你观察到的类似的改进(质数:2.48对质数2:1.89)。我从未意识到3.x版本之间存在如此大的性能差异。今天我学到了一点,我会考虑将我的东西迁移到更新的Python版本。 - Alain T.
显示剩余5条评论

1
使用推导式
[x for x in range(4, 1000) if all(x % y != 0 for y in range(2, int(math.sqrt(x)) + 1))]

1
虽然这里有其他很好的答案,但我想要包含我的版本。
n = 1000
primes = [candidate for candidate in range(1, n) 
            if candidate not in [step * k
                   for step in range(2,n // 2 + 1) 
                   for k in range(2,n//x)]]

这个算法的运行时间复杂度为O(n^2 log n),因为调和级数的前n项和增长大约为log n。

0

这里有另一种不是一行代码但更高效的方法。优点是每次只检查以前发现的质数,而不是查找范围(n)中的所有值:

def prime(n):
    pm_list = [2]
    for i in range(3,n+1):
        pm_arr = np.array(pm_list)
        if any(i // pm_arr == i / pm_arr) == False:
            pm_list.append(i)
    return pm_list

0
以下的代码应该会打印出所有小于"limit"值的数字列表。
primes = lambda limit: [num for num in range(2, limit) if num > 1 and (num == 2 or num % 2 != 0) and all(num % divisor != 0 for divisor in range(3, int(num ** 0.5) + 1, 2))]

print(primes(30))

0

这里有一行代码可以完成它:

print((lambda n:[i for i in range(2, n) if ["A7A" for j in range(2, i) if i%j==0]==[]])(100))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接