我正在尝试使用列表和过滤器而不是数组和循环来实现Eratosthenes的Sieve。我不确定为什么以下表现比命令式等效表现差得多。 100万应该绝对飞,但我的机器停止了。
val max = 1000000
def filterPrimes(upper: Int, seed: Int = 2, sieve: List[Int] = List()): List[Int] =
sieve.map(x => if (x % seed == 0 && x > seed) 0 else x).filter(_ > 0)
var filtered: List[Int] = (2 to max).toList
for (i <- 2 to max / 2) filtered = filterPrimes(max, i, filtered)
filtered.foreach(println(_))
答案 0 :(得分:10)
如果您希望看到筛子的功能性方法,请查看The Genuine Sieve of Eratosthenes。
答案 1 :(得分:10)
有一些潜在的问题,虽然我没有真正看到一支“吸烟枪”......无论如何,这就是我所拥有的。第一:
sieve.map(x => if (x % seed == 0 && x > seed) 0 else x).filter(_ > 0)
可以更简洁地写成:
sieve.filter(x => x <= seed || x % seed != 0)
接下来,upper
中未使用filterPrimes
(但这应该对性能没有影响)。
第三,如果你想真正使用纯函数式,请不要使用var
和for
循环,而是将filterPrimes
转换为尾递归函数。如果你这样做,编译器可能足够巧妙地优化副本(尽管我不会屏住呼吸)。
最后,也许最重要的是,您的for
循环浪费了大量时间来过滤掉必须已经过滤的值。例如,它在已经过滤了所有2的倍数后尝试过滤4的倍数。如果要有效地使用此筛选算法,则需要从列表中的其余元素中选择种子。
换句话说,将索引保留在列表中,并从索引中确定种子,例如:
iteration 0: 2 3 4 5 6 7 8 9 ...
index: ^
iteration 1: 2 3 5 7 9 ...
index: ^
iteration 2: 2 3 5 7 ...
index: ^
这避免了重复的努力。此外,在到达max
之前,您不需要继续进行迭代,我认为当您超过sqrt(max)
时,您实际上可以停止。
答案 2 :(得分:5)
我会做一些修改。
filterPrimes
和2
之间为所有数字执行max / 2
似乎很奇怪,“实际”筛选技术要求您只执行{{1在filterPrimes
和2
之间的所有素数。sqrt(max)
,而是可以随时收集素数;不需要一遍又一遍地通过过滤器。filterPrimes
然后map
的做法相当奇怪,因为地图只是标记要过滤的元素,你只需使用过滤器即可完成相同的操作。所以这是我第一次尝试这些修改:
filter
然而,这反映了我的Haskell偏见,并且有一个巨大的缺陷:由于def filterFactors(seed: Int, xs: List[Int]) = {
xs.filter(x => x % seed != 0)
}
def sieve(max: Int) = {
def go(xs: List[Int]) : List[Int] = xs match {
case y :: ys => {
if (y*y > max) y :: ys
else y :: go(filterFactors(y, ys))
}
case Nil => Nil
}
go((2 to max).toList)
}
辅助函数中的递归调用y :: go(...)
,它将占用大量的堆栈空间。运行go
会导致我出现“OutOfMemoryError”。
让我们尝试一个常见的FP技巧:使用累加器进行尾递归。
sieve(1000000)
通过添加累加器值,我们能够以尾递归形式编写def sieve(max: Int) = {
def go(xs: List[Int],
acc: List[Int])
: List[Int] = xs match {
case y :: ys => {
if (y*y > max) acc.reverse ::: (y :: ys)
else go(filterFactors(y, ys), y :: acc)
}
case Nil => Nil
}
go((2 to max).toList, Nil)
}
辅助函数,从而避免了之前的巨大堆栈问题。 (Haskell的评估策略非常不同;因此它既不需要也不会受益于尾递归)
现在让我们将速度与基于突变的命令式方法进行比较。
go
在这里,我使用def mutationSieve (max: Int) = {
var arr: Array[Option[Int]] =
(2 to max).map (x => Some (x)).toArray
var i = 0
var seed = (arr (i)).get
while (seed * seed < max) {
for (j: Int <- (i + seed) to (max - 2) by seed) {
arr (j) = None
}
i += 1
while (arr (i).isEmpty) {
i += 1
}
seed = (arr (i)).get
}
arr.flatten
}
,并通过将其条目替换为“无”来“交叉”一个数字。有一点优化空间;也许通过使用bool数组可以获得小的速度提升,其中索引代表特定的数字。不管。
使用非常原始的技术(仔细放置Array[Option[Int]]
次调用...)我将功能版本的基准测试比命令式版本慢大约6倍。很明显,这两种方法具有相同的大时间复杂度,但是使用链表进行编程所涉及的常数因素确实会产生成本。
我还使用new Date()
代替Math.sqrt(max).ceil.toInt
对您的版本进行了基准测试:它比我在此处提供的功能版本慢了约15倍。有趣的是,据估计1 1到1000之间的每7个数字中约有1个(max / 2
)是素数(1 / ln(1000)),因此,很大一部分减速可归因于你在每个数字上执行循环这一事实,而我只为每个素数执行我的函数。当然,如果执行~1000次迭代需要15倍的时间,它执行500000次迭代需要大约7500倍,这就是为什么你原来的代码很慢的原因。
答案 3 :(得分:2)
这是一个快速的筛选,实现了合并冲突的提示以及Ken Wayne VanderL提到的论文中的一些提示:
def createPrimes (MAX: Int) : Array[Boolean] = {
val pri = (false :: false :: true :: List.range (3, MAX + 1).map (_ % 2 != 0)).toArray
for (i <- List.range (3, MAX)
if (pri (i))) {
var j = 2 * i;
while (j < MAX) {
if (pri (j))
pri (j) = false;
j += i;
}
}
pri
}
val MAX = 1000*1000
(1 to MAX).filter (createPrimes (MAX))
比较图表: 垂直轴显示秒,水平从100 000到1 000 000个素数。 deltaNovember算法已经改进,只能运行到math.sqrt(max)和过滤,由Alexey Romanov在评论中提出。来自Dan Burton我采用了第二种算法,最后一种算法进行了小修改,以适应我的接口(List,而不是Array)和bitSet Sieve,他只在评论中链接,但是速度最快。
答案 4 :(得分:0)
列表是不可变的,每次调用filterPrimes
都会创建一个新列表。你正在创建很多列表,顺便说一下,这是不必要的。
以您的第一直觉(您可能称之为“命令式等效”),我猜测它使用单个可变数组。
(编辑说明我明白不需要创建多个列表。)