简体   繁体   English

Javascript 与 Haskell 中的 Eratosthenes 筛选

[英]Sieve of Eratosthenes in Javascript vs Haskell

I've been playing around with Haskell and find it fascinating, especially the Lazy Evaluation feature which allows us to work with (potencially) infinite lists.我一直在玩 Haskell 并发现它很有趣,尤其是 Lazy Evaluation 功能,它允许我们使用(潜在地)无限列表。

From this, derives the beautiful implementation of the Sieve of Eratosthenes to get an infinite list of primes:由此,推导出Eratosthenes 筛的美丽实现,以获得无限的素数列表:

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

Still using haskell i can have either:仍在使用 haskell 我可以有:

takeWhile (<1000) primes

which gives me the primes until 1000 (n), or这给了我直到 1000 (n) 的素数,或者

take 1000 primes

which gives me the first 1000 primes这给了我前 1000 个素数


I tried to implement this in Javascript, forgetting the 'infinite' possibility and this is what i came up with:我试图在 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)
}

It works perfectly (if i don't reach maximum call stack size), but i can only get the prime numbers "up until" n.它工作得很好(如果我没有达到最大调用堆栈大小),但我只能得到“直到”n 的素数。

How could i use this to implement a function that would instead return "the first n" primes?我如何使用它来实现一个函数,而不是返回“前 n”个素数?

I've tried many approaches and couldn't get it to work我尝试了很多方法,但无法让它发挥作用


Bonus奖金

is there any way i can use tail call optimization or something else to avoid StackOverflows for large Ns?有什么方法可以使用尾调用优化或其他方法来避免大型 Ns 的 StackOverflows?

As @VLAZ suggested, we can do this using generators:正如@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* 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(20, primes)));

Alright, after working all weekend on this, i think i found my best implementation.好吧,经过整个周末的工作,我想我找到了我最好的实现。

My solution uses proper caching (using the power of closures) of previous results so, the performance keeps getting better the more you use it我的解决方案使用了先前结果的适当缓存(使用闭包的力量),因此,使用得越多,性能就会越来越好

To get the first N primes, I iterate through the getPrimesTill until i reach a sufficient length... there is a compromise here, which will find more primes than intended on the first time but i don't think it can be any other way.为了获得前 N 个素数,我遍历 getPrimesTill 直到达到足够的长度......这里有一个妥协,它会在第一次找到比预期更多的素数,但我认为它不能是任何其他方式. Maybe getPrimesTill(n + ++count * n) can be further optimized but i think this is more than good enough.也许getPrimesTill(n + ++count * n)可以进一步优化,但我认为这已经足够了。

To be able to handle very large numbers while avoiding stack overflows, i implemented the sieve algorithm using a for loop, instead of recursion.为了能够在避免堆栈溢出的同时处理非常大的数字,我使用 for 循环而不是递归实现了筛选算法。

Here's the code:这是代码:

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)
    } while (true)
  }

  return { getPrimesTill, getFirstPrimes, thePrimes }
}

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

I created a repo for it, with exhaustive testing anyone wants to give it a go.我为它创建了一个 repo,进行了详尽的测试,任何人都想试一试。

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

The entire test suite takes about 85s to run, as i'm testing with many possible combinations and very large numbers.整个测试套件大约需要 85 秒才能运行,因为我正在测试许多可能的组合和非常大的数字。
Also, all the expected results were obtained from the Haskell implementation, so to not to pollute the tests.此外,所有预期结果都是从 Haskell 实现中获得的,以免污染测试。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM