计算Scala中的素数:此代码如何工作?

时间:2013-03-24 01:41:22

标签: scala stream primes

所以我花了好几个小时试图弄清楚这段代码是如何生成素数的。

lazy val ps: Stream[Int] = 2 #:: Stream.from(3).filter(i =>
   ps.takeWhile{j => j * j <= i}.forall{ k => i % k > 0});

我使用了许多印刷品等,但没有让它更清晰。

这就是我认为代码的作用:

/**
 * [2,3] 
 * 
 * takeWhile 2*2 <= 3 
 * takeWhile 2*2 <= 4 found match
 *      (4 % [2,3] > 1) return false.
 * takeWhile 2*2 <= 5 found match
 *      (5 % [2,3] > 1) return true 
 *          Add 5 to the list
 * takeWhile 2*2 <= 6 found match
 *      (6 % [2,3,5] > 1) return false
 * takeWhile 2*2 <= 7
 *      (7 % [2,3,5] > 1) return true
 *          Add 7 to the list
 */

但如果我将列表中的j*j更改为2 * 2,我假设将完全相同,则会导致堆栈溢出错误。

我显然在这里缺少一些基本的东西,并且真的可以使用某人向我解释这个,就像我五岁。

非常感谢任何帮助。

3 个答案:

答案 0 :(得分:24)

我不确定寻求程序/命令性解释是获得理解的最佳方式。流来自函数式编程,从这个角度来看它们是最好的理解。您给出的定义的关键方面是:

  1. lazy 。除了流中的第一个元素之外,在您要求之前不会计算任何内容。如果你从不要求第五个素数,它将永远无法计算。

  2. 这是递归。素数列表是根据其自身定义的。

  3. 无限。 Streams具有有趣的属性(因为它们很懒惰),它们可以表示具有无限数量元素的序列。 Stream.from(3)就是一个例子:它代表了列表[3,4,5,...]。

  4. 让我们看看我们是否能理解你的定义为何计算素数序列。

    该定义以2 #:: ...开头。这只是说序列中的第一个数字是2 - 到目前为止足够简单。

    下一部分定义其余的素数。我们可以从3(Stream.from(3))开始的所有计数数字开始,但我们显然需要过滤掉一堆这些数字(即所有复合数据)。那么让我们考虑每个数字i。如果i不是较低素数的倍数,则i为素数。也就是说,对于所有素数i小于kii % k > 0为素数。在Scala中,我们可以将其表达为

    nums.filter(i => ps.takeWhile(k => k < i).forall(k => i % k > 0))
    

    然而,实际上并不需要检查所有较小的素数 - 我们实际上只需要检查其平方小于或等于i的素数(这是数论的一个事实{ {1}})。所以我们可以改写

    *

    所以我们推导出你的定义。

    现在,如果您碰巧尝试了第一个定义(使用nums.filter(i => ps.takeWhile(k => k * k <= i).forall(k => i % k > 0)) ),您会发现它不起作用。为什么不?它与这是一个递归定义的事实有关。

    假设我们正在尝试决定序列中2之后的内容。该定义告诉我们首先确定3是否属于。为此,我们认为第一个素数列表大于或等于3(k < i)。第一个素数是2,小于3 - 到目前为止一直很好。但是我们还不知道第二个素数,所以我们需要计算它。很好,所以我们首先要看看3是否属于...... BOOM!

    takeWhile(k => k < i) 很容易看出,如果数字*是复合数,那么其中一个因子的平方必须小于或等于n。如果n是复合的,那么按照定义n,其中n == a * b(我们可以通过恰当地标记这两个因素来保证1 < a <= b < n)。从a <= b开始,它跟随a <= b,因此它跟随a^2 <= a * b

答案 1 :(得分:3)

你的解释大多是正确的,你只犯了两个错误:

takeWhile不包含最后一个选中的元素:

scala> List(1,2,3).takeWhile(_<2)
res1: List[Int] = List(1)

您认为ps始终只包含两个和三个,但由于Stream是惰性的,因此可以向其中添加新元素。实际上,每次找到新的素数时,它都会添加到ps,在下一步takeWhile中会考虑这个新添加的元素。在这里,重要的是要记住Stream的尾部仅在需要时计算,因此takeWhileforall被评估为真之前无法看到它。

记住这两件事,你应该想出这个:

ps = [2]
i = 3
  takeWhile
    2*2 <= 3 -> false
  forall on []
    -> true
ps = [2,3]
i = 4
  takeWhile
    2*2 <= 4 -> true
    3*3 <= 4 -> false
  forall on [2]
    4%2 > 0 -> false
ps = [2,3]
i = 5
  takeWhile
    2*2 <= 5 -> true
    3*3 <= 5 -> false
  forall on [2]
    5%2 > 0 -> true
ps = [2,3,5]
i = 6
...

虽然这些步骤描述了代码的行为,但它并不完全正确,因为不仅向Stream添加元素是惰性的,而且每个操作都在其上。这意味着当您调用xs.takeWhile(f)时,并非所有值都会立即计算f为false时的值 - 它们是在forall想要查看它们时计算的(因为它是唯一的函数)在这里需要查看所有元素才能确定结果为true,因为false它可以先中止)。这里考虑到懒惰的计算顺序(例子只看9):

ps = [2,3,5,7]
i = 9
  takeWhile on 2
    2*2 <= 9 -> true
  forall on 2
    9%2 > 0 -> true
  takeWhile on 3
    3*3 <= 9 -> true
  forall on 3
    9%3 > 0 -> false
ps = [2,3,5,7]
i = 10
...

由于forall在评估为false时被中止,takeWhile不会计算剩余的可能元素。

答案 2 :(得分:1)

该代码更容易(对我来说,至少)阅读时将某些变量重命名为暗示,如

lazy val ps: Stream[Int] = 2 #:: Stream.from(3).filter(i =>
   ps.takeWhile{p => p * p <= i}.forall{ p => i % p > 0});

这很自然地读取从左到右

  

primes 2 3 中的那些数字i all primes p,其正方形不超过i均匀划分i(即没有一些非零余数) )。

以一种真正的递归方式,要理解这个定义是为了定义不断增加的素数流,我们假设 这样,从这个假设我们看到没有出现矛盾,即定义的真实性成立。

此后唯一可能的问题是,在定义时访问流ps 的时间。作为第一步,假设我们只是从某个地方为我们提供了另一个素数流,神奇地。然后,在看到定义的真实性之后,检查访问的时间是否正常,即在定义之前我们从不尝试访问ps的区域;这会使定义停滞不前,非生产性

我记得在某个地方(不记得在哪里)阅读以下内容 - 学生和巫师之间的对话,

  • 学生:哪些数字是素数?
  • 向导:嗯,你知道第一个素数是多少?
  • s:是的,它是 2
  • w:好的(快速在一张纸上写下 2 )。那下一个怎么样?
  • s:好,下一位候选人 3 。我们需要检查它是否被正方形不超过它的任何素数除,但我还不知道素数是什么!
  • w:别担心,我会把它们给你。这是我知道的魔法;毕竟我是个巫师。
  • s:好的,那么第一个素数是多少?
  • w:(浏览一张纸) 2
  • s:很棒,所以它的方块已经大于3 ... 嘿,你被骗了! .....

这是代码的伪代码 1 ,部分读取从右到左,为了清晰起见,再次重命名了一些变量(使用p代表“素“):

ps = 2 : filter (\i-> all (\p->rem i p > 0) (takeWhile (\p->p^2 <= i) ps)) [3..]

也是

ps = 2 : [i | i <- [3..], and [rem i p > 0 | p <- takeWhile (\p->p^2 <= i) ps]]

使用list comprehensions更明显一点。 and检查布尔列表中的所有条目是True(读取|为“for”,<-为“from”,,为“这样“(\p-> ...)为”p“的 lambda

所以你ps 是2的懒惰列表 ,然后是数字i从流[3,4,5,...]开始,对于从p中抽取ps的所有p^2 <= ii % p > 0就是ps。这实际上是最佳 trial division算法。 :)

当然,这里有一个微妙之处:列表p是开放式的。我们使用它,因为它正在“充实”(当然,因为它是懒惰的)。当ps取自ps时,可能会出现这种情况,即我们已经超过了它的结尾,在这种情况下,我们手上会有一个非终止计算(“黑洞“)。它恰好发生了:)(并且需要/可以通过数学方式证明)这对于上述定义是不可能的。所以2无条件地放入bad = 2 : [i | i <- [3..], and [rem i p > 0 | p <- takeWhile (\p->p < i) bad]] ,所以开始时会有一些东西。

但如果我们试图“简化”,

takeWhile (\p->p < 3) bad

它只产生一个数字后停止工作,2:当考虑3作为候选人时,bad要求{2}之后的bad = 2 : [i | i <- [3..], and [rem i p > 0 | p <- [2..(i-1)] ]] 中的下一个数字,但那里还没有更多的数字。它“超越自己”。

这是“固定”的

{{1}}

但这是一个慢得多的试验分割算法,距离最佳非常远。

-

1 Haskell实际上,这对我来说更容易:))