在 Scala 中计算素数:这段代码是如何工作的?

Posted

技术标签:

【中文标题】在 Scala 中计算素数:这段代码是如何工作的?【英文标题】:Calculating prime numbers in Scala: how does this code work? 【发布时间】:2013-03-13 17:16:06 【问题描述】:

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

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

我使用了许多 printlns 等,但没有什么能让它更清晰。

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

/**
 * [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,我假设会完全一样,它会导致 *** 错误。

我显然在这里遗漏了一些基本的东西,并且真的可以像我五岁一样向我解释这一点。

任何帮助将不胜感激。

【问题讨论】:

为什么要使用“懒惰”?初始化 Stream 不需要太多时间或内存,而且在 ps 不偷懒的情况下也能正常工作,对吧? @AmigoNico mmlac 在对我的回答的评论中引起了我的注意——lazy 实际上可能是必要的,因为val 具有递归定义。奇怪的是,在我自己在 Scala 2.10.4 中进行的测试中,lazy 在 REPL 中不需要,但在普通代码中却是必需的——可能是 REPL 错误。 啊,当然可以。我应该仔细看看。 【参考方案1】:

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

takeWhile 不包括最后检查的元素:

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

您假设ps 总是只包含一个二和一个三,但是因为Stream 是惰性的,所以可以向它添加新元素。事实上,每次发现一个新的素数时,它都会被添加到ps 中,下一步takeWhile 将考虑这个新添加的元素。在这里,重要的是要记住 Stream 的尾部仅在需要时计算,因此 takeWhileforall 被评估为 true 之前无法看到它。

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

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 想要查看它们时计算它们(因为它是这里唯一需要查看的函数它之前的所有元素肯定可以导致为真,如果为假,它可以更早地中止)。这里是到处考虑惰性时的计算顺序(仅查看 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 不会计算剩余的可能元素。

【讨论】:

非常感谢您抽出宝贵时间回复此问题。现在这对我来说很有意义,对惰性关键字如何工作的澄清也确实有助于解释为什么我得到了我得到的 println 输出。 @AlanHollis:在这种情况下不需要lazy 关键字,没有它代码也可以工作。这是因为Stream 内部已经很懒惰了。关键字只是使对引用的调用变得惰性,而不是数据结构本身。【参考方案2】:

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

    这是 懒惰。除了流中的第一个元素之外,在您请求之前不会计算任何内容。如果你从不要求第五个素数,它永远不会被计算出来。

    它是递归的。素数列表是根据自身定义的。

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

让我们看看我们是否能理解为什么你的定义会计算素数序列。

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

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

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

然而,实际上并不需要检查所有较小的素数——我们实际上只需要检查平方小于或等于i 的素数(这是来自数论* 的事实)。所以我们可以改为写

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

所以我们得出了您的定义。

现在,如果您碰巧尝试了第一个定义(使用k &lt; i),您会发现它不起作用。为什么不?这与这是一个递归定义这一事实有关。

假设我们试图确定序列中 2 之后的内容。定义告诉我们首先确定 3 是否属于。为此,我们考虑到第一个大于或等于 3 的素数列表 (takeWhile(k =&gt; k &lt; i))。第一个素数是 2,小于 3——到目前为止还不错。但是我们还不知道第二个素数,所以我们需要计算它。好吧,所以我们需要先看看 3 是否属于... BOOM!

* 很容易看出,如果一个数 n 是合数,那么它的一个因数的平方必须小于或等于 n。如果n 是复合的,那么根据定义n == a * b,其中1 &lt; a &lt;= b &lt; n(我们可以通过适当地标记这两个因素来保证a &lt;= b)。从a &lt;= b 后面是a^2 &lt;= a * b,所以后面是a^2 &lt;= n

【讨论】:

谢谢亚伦。非常感谢。事实上,我确实尝试了 k 最好的解释。谢谢:) 将中断当前 scalac 并出现“前向引用扩展值 ps 的定义”错误。已测试 2.11.4、2.10.4 和 2.9.0 @mmlac 什么中断? OP的原始代码?仅在 2.11 中? @AaronNovstrup 如果我正确理解您的解决方案,这应该可以。内联错误。 gist.github.com/mmlac/fb5e850b17ebdd2d94bc【参考方案3】:

该代码更容易阅读(至少对我而言),将一些变量重命名为建议,如

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

这很自然地读作从左到右,就像

素数2,那些数字i3起,所有的素数 p 的平方不超过ii 均分(即没有一些非零余数)。

以真正的递归方式,为了将这个定义理解为定义不断增加的素数流,我们假设如此,并且从这个假设中我们看到没有矛盾出现,即定义的真实性成立。

在那之后唯一的潜在问题是访问流ps 的时间正如它被定义的那样。作为第一步,想象我们只是从某个地方神奇地提供了另一个素数流。然后,在看到定义的真实性之后,检查访问的时机是否正确,即我们从未尝试在定义之前访问ps 的区域;这会使定义卡住,没有效率

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

学生:哪些数是素数? 向导:好吧,你知道第一个素数是多少吗? s:是的,它是2w: 好的(快速在一张纸上写下2)。那么下一个呢? s: 好吧,下一个候选人是 3。我们需要检查它是否被任何平方不超过它的素数除,但是我还不知道素数是什么! w: 别担心,我会给你的。这是我知道的魔法;毕竟我是个巫师。 s:好的,那么第一个质数是多少? w:(瞥了一眼纸)2s: 太好了,所以它的平方已经大于 3...嘿,你作弊了! .....

这是您的代码的伪代码1 翻译,部分阅读从右到左,为清楚起见再次重命名了一些变量(使用p 表示“prime” ):

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”,&lt;- 读作“drawn from”,, 读作“such that”,(\p-&gt; ...) 读作“lambda of p")。

所以你看到ps是一个惰性列表,包含 2,然后是从流 @ 中提取的数字 i 987654343@ 使得对于从ps 得出的所有p 使得p^2 &lt;= ii % p &gt; 0 是真的。这实际上是一个最佳 trial division 算法。 :)

这里当然有一个微妙之处:ps 列表是开放式的。我们使用它是因为它正在“充实”(当然,因为它是懒惰的)。当ps 取自ps 时,我们可能会跑到它的末尾,在这种情况下,我们手头上会有一个非终止计算(“黑洞 em>”)。就这样发生了:)(并且需要 ⁄ 可以在数学上证明)上述定义是不可能的。所以 2 被无条件地放入ps,所以里面有东西。

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

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

它在只产生一个数字 2 后停止工作:当考虑 3 作为候选时,takeWhile (\p-&gt;p &lt; 3) bad 要求在 2 之后的 bad 中的下一个数字,但那里还没有更多的数字。它“超越自己”。

这是“固定的”

bad = 2 : [i | i <- [3..], and [rem i p > 0 | p <- [2..(i-1)] ]]

但这是一种慢得多的试验划分算法,与最佳算法相去甚远。

--

1(实际上是Haskell,那样对我来说更容易:))

【讨论】:

谢谢 Will Ness,伪代码帮了大忙。我认为是因为 all 和 takeWhile 函数的顺序是左->右,这使得它更易于阅读。修复后的版本帮助我了解了在不太复杂的庄园中尝试实现相同目标的错误所在!再次感谢,艾伦 @AlanHollis 很高兴这有帮助。 :) 根本不确定是否会。 :) 使用 2 而不是 j 我们得到 ps = 2 : [i | i &lt;- [3..], and [rem i p &gt; 0 | p &lt;- takeWhile (\p-&gt;4 &lt;= i) ps]] 所以对于 i==3 它是 takeWhile (\p-&gt;False) ps == [] 并且 3 通过 OK(因为 and [] == True),但是对于 i==4 它是 takeWhile(p-&gt;True) ps 它永远不会停止询问新的来自ps的号码。

以上是关于在 Scala 中计算素数:这段代码是如何工作的?的主要内容,如果未能解决你的问题,请参考以下文章

C 中的这段代码决定一个数字是不是是素数,它会因大数而崩溃。为啥?

这段代码是如何工作的?

C语言之素数判断及输出

有人能解释一下这段代码中的法线是如何计算的吗?

计算素数;程序工作/崩溃超过 6657;

如何在背景减法中计算前景区域的像素数