简短回答:计数筛比 Turner 的(又名“朴素”)筛慢,因为它通过顺序计数模拟直接 RAM 访问,这迫使它通过流未筛分 在标记阶段之间。这很讽刺,因为 计数 使它成为 Eratosthenes 的 “真正” 筛子,而不是特纳的试验师筛子。实际上去除倍数,就像特纳的筛子所做的那样,会打乱计数。
这两种算法都非常慢,因为它们过早从每个找到的素数而不是平方数开始进行多重消除工作,从而创建了太多不需要的流处理阶段(无论是过滤还是标记) - @987654322他们中的@,而不仅仅是~ 2*sqrt n/log n,可以产生价值高达n的素数。在输入中看到 49 之前,不需要检查 7 的倍数。
This answer 解释了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 产生的素数中。但是,当我们可以按增量计数时,为什么要按个数来计数。 (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 质数,快速-n-脏测试编译加载到 GHCi 中,产生高达 100k - 150k 质数,在大约 0.5 万个质数时恶化到 O(k^1.3)。
那么这样可以实现什么样的加速?将 OP 代码与“维基百科”的特纳筛进行比较,
primes = sieve [2..] :: [Int]
where
sieve (x:xs) = x : sieve [y | y <- xs, rem y x /= 0]
2k 的 W/OP 有 8x 加速(即产生 2000 个素数)。但在 4k 时,这是一个 15x 加速。特纳筛在产生k = 1000 .. 6000 素数时的经验复杂度似乎约为O(k^1.9 .. 2.3),而计数筛在O(k^2.3 .. 2.6) 的运行范围相同。
对于此答案中的两个版本,v1/W 在 4k 和 43x 时更快 20x在 8k。 v2/v1 分别是 5.2x 在 20k、5.8x 在 40k 和 6.5x 更快地产生 80,000 个素数。
(作为比较,Melissa O'Neill 的优先级队列代码在 k 生成的素数中以大约 O(k^1.2) 的经验复杂度运行。当然,它的可扩展性比这里的代码要好得多。
这是埃拉托色尼定义的筛子:
P = {3,5, ...} \ ⋃ { { p*p, p*p + 2*p, ...} | p in P }
埃拉托色尼筛法效率的关键是直接生成素数的倍数,方法是从每个素数中以(两次)素数的值递增;以及它们的直接消除,通过合并值和地址来实现,就像在整数排序算法中一样(仅适用于可变数组)。它是否必须产生预设数量的素数或无限期地工作并不重要,因为它总是可以分段工作。