简短的回答:计数筛比特纳的(又名“幼稚”)筛慢,因为它模拟了具有顺序计数的直接 RAM 访问,这迫使它沿着标记阶段之间未筛分的流传递。这是具有讽刺意味的,因为计数使它成为 Eratosthenes 的“真正”筛子,而不是 Turner 的试验师筛子。实际上去除倍数,就像特纳的筛子正在做的那样,会弄乱计数。
这两种算法都非常慢,因为它们过早地从每个找到的素数而不是它的平方开始多重消除工作,从而创建了太多不需要的流处理阶段(无论是过滤还是标记)——O(n)
它们,而不是仅仅~ 2*sqrt n/log n
,在产生最多n
在价值上。在输入中看到7
之前不需要检查 的倍数。49
这个答案解释了如何sieve
被视为在其背后构建流处理“转换器”的管道,因为它正在工作:
[2..] ==> sieve --> 2
[3..] ==> mark 1 2 ==> sieve --> 3
[4..] ==> mark 2 2 ==> mark 1 3 ==> sieve
[5..] ==> mark 1 2 ==> mark 2 3 ==> sieve --> 5
[6..] ==> mark 2 2 ==> mark 3 3 ==> mark 1 5 ==> sieve
[7..] ==> mark 1 2 ==> mark 1 3 ==> mark 2 5 ==> sieve --> 7
[8..] ==> mark 2 2 ==> mark 2 3 ==> mark 3 5 ==> mark 1 7 ==> sieve
[9..] ==> mark 1 2 ==> mark 3 3 ==> mark 4 5 ==> mark 2 7 ==> sieve
[10..]==> mark 2 2 ==> mark 1 3 ==> mark 5 5 ==> mark 3 7 ==> sieve
[11..]==> mark 1 2 ==> mark 2 3 ==> mark 1 5 ==> mark 4 7 ==> sieve --> 11
特纳筛子用来nomult p = filter ((/=0).(`rem`p))
代替mark _ p
条目,但在其他方面看起来是一样的:
[2..] ==> sieveT --> 2
[3..] ==> nomult 2 ==> sieveT --> 3
[4..] ==> nomult 2 ==> nomult 3 ==> sieveT
[5..] ==> nomult 2 ==> nomult 3 ==> sieveT --> 5
[6..] ==> nomult 2 ==> nomult 3 ==> nomult 5 ==> sieveT
[7..] ==> nomult 2 ==> nomult 3 ==> nomult 5 ==> sieveT --> 7
[8..] ==> nomult 2 ==> nomult 3 ==> nomult 5 ==> nomult 7 ==> sieveT
每个这样的转换器都可以实现为闭包框架(也称为“thunk”),或者具有可变状态的生成器,这并不重要。每个这样的生产者的输出直接作为输入进入其链中的继任者。这里没有未评估的 thunk,每个都被其消费者强迫,以产生其下一个输出。
所以,回答你的问题,
我怀疑这与在标记函数中迭代列表中的每个元素有关。
是的,正是。否则,他们都运行非延期计划。
因此,可以通过推迟流标记的开始来改进代码:
primes = 2:3:filter (>0) (sieve [5,7..] (tail primes) 9)
sieve (x:xs) ps@ ~(p:t) q
| x < q = x:sieve xs ps q
| x==q = sieve (mark xs 1 p) t (head t^2)
where
mark (y:ys) k p
| k == p = 0 : (mark ys 1 p) -- mark each p-th number in supply
| otherwise = y : (mark ys (k+1) p)
O(k^1.5)
根据经验,它现在在k
产生的素数上运行。但是,当我们可以按增量计数时,为什么要按个数来计数。(每个第 3 个奇数9
可以通过添加6
, 一次又一次地找到。)然后我们可以立即剔除这些数字,而不是标记,让自己成为一个真正的 Eratosthenes 筛子(即使不是最有效的筛子):
primes = 2:3:sieve [5,7..] (tail primes) 9
sieve (x:xs) ps@ ~(p:t) q
| x < q = x:sieve xs ps q
| x==q = sieve (weedOut xs (q+2*p) (2*p)) t (head t^2)
where
weedOut i@(y:ys) m s
| y < m = y:weedOut ys m s
| y==m = weedOut ys (m+s) s
| y > m = weedOut i (m+s) s
O(k^1.2)
这在生成的素数中运行在上面,k
编译加载到 GHCi 中的快速 n 脏测试,产生高达 100k - 150k 的素数,退化到O(k^1.3)
大约 0.5 万个素数。
那么这样可以实现什么样的加速呢?将 OP 代码与“维基百科”的特纳筛进行比较,
primes = sieve [2..] :: [Int]
where
sieve (x:xs) = x : sieve [y | y <- xs, rem y x /= 0]
W/OP 有2k的8x
加速(即产生 2000 个素数)。但在4k时,这是一个加速。特纳筛似乎在产生素数时以经验复杂度运行,而计数筛在相同范围内运行。15x
O(k^1.9 .. 2.3)
k = 1000 .. 6000
O(k^2.3 .. 2.6)
对于此答案中的两个版本,v1/W在4k和8k时20x
更快。v2/v1 为20k , 40k甚至更快,产生 80,000 个素数。43x
5.2x
5.8x
6.5x
(作为比较,Melissa O'Neill 的优先级队列代码以O(k^1.2)
经验复杂度运行,k
产生的素数。当然,它的扩展性比这里的代码好得多)。
这是埃拉托色尼定义的筛子:
P = { 3 , 5 , ...} \ ⋃ { { p*p , p*p + 2*p , ...} | p中的p }
埃拉托色尼筛法效率的关键是直接生成多个素数,通过从每个素数开始计数(两次)素数值的增量;以及它们的直接消除,这可以通过值和地址的融合来实现,就像整数排序算法一样(仅适用于可变数组)。它是否必须产生预设数量的素数或无限期地工作并不重要,因为它总是可以分段工作。