【问题标题】:Sieve of Eratosthenes in Javascript vs HaskellJavascript 与 Haskell 中的 Eratosthenes 筛选
【发布时间】:2021-12-19 21:28:31
【问题描述】:

我一直在玩 Haskell,发现它很吸引人,尤其是 Lazy Evaluation 功能,它允许我们使用(可能)无限列表。

由此推导出the Sieve of Eratosthenes 的漂亮实现,以获得无限的素数列表:

primes = sieve [2..]
  where sieve (x:xs) = x : sieve [i | i <- xs, i `mod` x /= 0]

仍然使用haskell,我可以有:

takeWhile (<1000) primes

这给了我直到 1000 (n) 的素数,或者

take 1000 primes

这给了我前 1000 个素数


我尝试在 Javascript 中实现这一点,忘记了“无限”的可能性,这就是我想出的:

const sieve = list => {
  if (list.length === 0) return []
  const first = list.shift()
  const filtered = list.filter(x => x % first !== 0)
  return [first, ...sieve(filtered)]
}

const getPrimes = n => {
  const list = new Array(n - 1).fill(null).map((x, i) => i + 2)
  return sieve(list)
}

它工作得很好(如果我没有达到最大调用堆栈大小),但我只能得到素数“直到”n。

我如何使用它来实现一个返回“前 n 个”素数的函数?

我尝试了很多方法,但无法让它发挥作用


奖金

有什么方法可以使用尾调用优化或其他方法来避免大型 N 的 StackOverflows?

【问题讨论】:

  • 可能值得将其转换为生成器。然后,您可以拥有一个非常通用的 taketakeUntil 可迭代助手,它们将与您的主要生成器一起使用。
  • 我会在明天试一试,除非在那之前有人尝试过。现在该睡觉了。但这里是步骤:保留以2 开头的素数列表。对于以下每个数字,检查它是否是素数的倍数,如果是(Erathostenes)则丢弃它,如果不是,则放弃并保留它。或者,您可以生成素数的所有倍数(直到某个限制)并将它们保存在一个集合中。然后检查nonPrimes.has(i) 来决定。有了 taketakeUntil 就很容易了 - 第一个采用可迭代和 n 并产生 n 值,另一个采用可迭代和谓词并检查并产生
  • 切记不要落入“运行递归代码而不缓存结果”的陷阱。无论是斐波那契数列还是埃拉托色尼筛,维护一个已找到值的全局列表,这样您就不会一遍又一遍地重新运行计算,并且 [...] - 请咨询您的 LUT 以查看“ up to where" 你已经知道结果,并直接使用它,而不是递归。
  • 啊,抱歉,您需要n 素数,而不是n 以下的所有素数。老实说...从更高的n =D 开始(感谢 Gauss,他给了我们π(x)
  • (请注意,从技术上讲,筛分方法,而且我们正在谈论数学,所以这很重要,不会计算“n 个素数”的 erestothenes 筛子。利用 π(x)=n 作为“预备步骤”筛分具有数学意义)

标签: javascript primes


【解决方案1】:

正如@VLAZ 建议的那样,我们可以使用生成器来做到这一点:

function* removeMultiplesOf(x, iterator) {
  for (const i of iterator)
    if (i % x != 0)
      yield i;
}
function* eratosthenes(iterator) {
  const x = iterator.next().value;
  yield x;
  yield* eratosthenes(removeMultiplesOf(x, iterator));
}
function* from(i) {
  while (true)
    yield i++;
}
function* take(n, iterator) {
  if (n <= 0) return;
  for (const x of iterator) {
    yield x;
    if (--n == 0) break;
  }
}

const primes = eratosthenes(from(2));
console.log(Array.from(take(1000, primes)));

顺便说一句,我认为可以通过不重复进行除法来优化这一点:

function* removeMultiplesOf(x, iterator) {
  let n = x;
  for (const i of iterator) {
    while (n < i)
      n += x;
    if (n != i)
      yield i;
  }
}

但一个快速的基准测试表明它实际上与简单函数一样快。

【讨论】:

  • 这太棒了!我从来没有以这种方式使用过生成器,它(几乎)表现得像 haskell
  • 抱歉,我删除了“正确答案”,因为我明天想试试。如果我的方法没有成功,我会认为它是正确的
  • 是的,只是几乎:迭代器不可重用,不像 Haskell 的惰性列表。
【解决方案2】:

好的,在整个周末工作之后,我想我找到了最好的实现。

我的解决方案对以前的结果使用了适当的缓存(使用闭包的力量),因此,您使用它的次数越多,性能就会越来越好

为了获得前 N 个素数,我遍历 getPrimesTill 直到达到足够的长度......这里有一个折衷方案,它会在第一次找到比预期更多的素数,但我认为不可能任何其他方式。也许getPrimesTill(n + ++count * n * 5) 可以进一步优化,但我认为这已经足够了。

为了能够在避免堆栈溢出的同时处理非常大的数字,我使用 for 循环而不是递归来实现筛算法。

代码如下:

function Primes() {
  let thePrimes = []

  const shortCircuitPrimes = until => {
    const primesUntil = []
    for (let i = 0; ; i++) {
      if (thePrimes[i] > until) {
        return primesUntil
      }
      primesUntil.push(thePrimes[i])
    }
  }

  const sieveLoop = n => {
    const list = buildListFromLastPrime(n)
    const result = []
    let copy = [...thePrimes, ...list]
    for (let i = 0; i < result.length; i++) {
      copy = copy.filter(x => x % result[i] !== 0)
    }
    for (let i = 0; ; i++) {
      const first = copy.shift()
      if (!first) return result
      result.push(first)
      copy = copy.filter(x => x % first !== 0)
    }
  }

  const buildListFromLastPrime = n => {
    const tpl = thePrimes.length
    const lastPrime = thePrimes[tpl - 1]
    const len = n - (lastPrime ? tpl + 1 : 1)
    return new Array(len).fill(null).map((x, i) => i + 2 + tpl)
  }

  const getPrimesTill = n => {
    const tpl = thePrimes.length
    const lastPrime = thePrimes[tpl - 1]
    if (lastPrime > n) {
      return shortCircuitPrimes(n)
    }

    const primes = sieveLoop(n)
    if (primes.length - thePrimes.length) {
      thePrimes = primes
    }
    return primes
  }

  const getFirstPrimes = n => {
    let count = 0
    do {
      if (thePrimes.length >= n) {
        return thePrimes.slice(0, n)
      }
      getPrimesTill(n + ++count * n * 5)
    } while (true)
  }

  return { getPrimesTill, getFirstPrimes, thePrimes }
}

const { getPrimesTill, getFirstPrimes, thePrimes } = Primes()

我为它创建了一个 repo,进行了详尽的测试,任何人都想尝试一下。

https://github.com/andrepadez/prime-numbers-sieve-eratosthenes-javascript

整个测试套件大约需要 85 秒才能运行,因为我正在测试许多可能的组合和非常大的数字。
此外,所有预期的结果都是从 Haskell 实现中获得的,以免污染测试。


另外,我发现了这个很棒的视频,他使用 TypeScript 实现了惰性求值和无限列表……最后,他用 Javascript 构建了 Sieve 算法,工作方式与 Haskell 中的预期完全一样

https://www.youtube.com/watch?v=E5yAoMaVCp0

【讨论】:

    猜你喜欢
    • 2019-07-03
    • 2017-01-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多