在JavaScript中运行Eratosthenes算法的Sieve对大量运行无穷无尽

时间:2013-03-18 07:00:58

标签: javascript arrays algorithm primes sieve-of-eratosthenes

我一直在尝试用JavaScript编写Sieve of Eratosthenes算法。基本上我只是遵循以下步骤:

  1. 创建从2到(n-1)
  2. 的连续整数列表
  3. 让第一个素数p等于2
  4. 从p开始,以p为增量计数并删除每个数字(p和p的倍数)
  5. 转到列表中的下一个号码并重复2,3,4
  6. 将无意删除的素数添加回列表
  7. 这就是我的想法:

    function eratosthenes(n){
    var array = [];
    var tmpArray = []; // for containing unintentionally deleted elements like 2,3,5,7,...
    var maxPrimeFactor = 0;
    var upperLimit = Math.sqrt(n);
    var output = [];
    
    // Eratosthenes algorithm to find all primes under n
    
    // Make an array from 2 to (n - 1)
    //used as a base array to delete composite number from
    for(var i = 2; i < n; i++){
        array.push(i);
    }
    
    // Remove multiples of primes starting from 2, 3, 5,...
    for(var i = array[0]; i < upperLimit; i = array[0]){
        removeMultiples: 
        for(var j = i, k = i; j < n; j += i){
            var index = array.indexOf(j);
            if(index === -1)
                continue removeMultiples;
            else
                array.splice(index,1);
        }
        tmpArray.push(k);
    }
    array.unshift(tmpArray);
    return array;
    }
    

    适用于小数字,但不适用于大于一百万的数字。我使用Node.js进行测试,这个过程似乎无穷无尽,并且没有出现内存错误。我已经阅读了解决方案here(也在javascript中),但仍然无法完全理解它。

    问题:如何使这项工作足够大,如一百万及以上?

7 个答案:

答案 0 :(得分:35)

通过使用在线性时间内运行的Array#indexOfArray#splice等数组操作函数,您使Eratosthenes的Sieve变得更慢。如果你有两个操作都可以有O(1)。

以下是传统编程实践中的Eratosthenes筛选:

var eratosthenes = function(n) {
    // Eratosthenes algorithm to find all primes under n
    var array = [], upperLimit = Math.sqrt(n), output = [];

    // Make an array from 2 to (n - 1)
    for (var i = 0; i < n; i++) {
        array.push(true);
    }

    // Remove multiples of primes starting from 2, 3, 5,...
    for (var i = 2; i <= upperLimit; i++) {
        if (array[i]) {
            for (var j = i * i; j < n; j += i) {
                array[j] = false;
            }
        }
    }

    // All array[i] set to true are primes
    for (var i = 2; i < n; i++) {
        if(array[i]) {
            output.push(i);
        }
    }

    return output;
};

You can see a live example for n = 1 000 000 here.

答案 1 :(得分:10)

这个问题在定义什么是#34;大数字&#34;时,有点吝啬。并且接受它只开始于大约一百万,current answer工作;但是,对于要筛分的每个元素,它使用一个8字节数(一个64位的双实数)和每个找到的素数的另一个8字节数,使用相当多的内存。这个答案不适用于大数字&#34;比如大约2.5亿以上,因为它会超过JavaScript执行机器可用的内存量。

以下JavaScript代码实现&#34;无限&#34; (无界)Eratosthenes的页面分段筛选克服了这个问题,因为它只使用一个比特打包的16千字节页面分段筛分缓冲区(一个位代表一个潜在的素数),并且只使用存储用于基本素数到达平方根。当前页面段中的当前最高编号,实际找到的素数按顺序枚举,无需任何存储;只通过筛选奇数复合材料来节省时间,因为唯一的素数是2:

var SoEPgClass = (function () {
  function SoEPgClass() {
    this.bi = -1; // constructor resets the enumeration to start...
  }
  SoEPgClass.prototype.next = function () {
    if (this.bi < 1) {
      if (this.bi < 0) {
        this.bi++;
        this.lowi = 0; // other initialization done here...
        this.bpa = [];
        return 2;
      } else { // bi must be zero:
        var nxt = 3 + 2 * this.lowi + 262144; //just beyond the current page
        this.buf = [];
        for (var i = 0; i < 2048; i++) this.buf.push(0); // faster initialization 16 KByte's:
        if (this.lowi <= 0) { // special culling for first page as no base primes yet:
          for (var i = 0, p = 3, sqr = 9; sqr < nxt; i++, p += 2, sqr = p * p)
            if ((this.buf[i >> 5] & (1 << (i & 31))) === 0)
              for (var j = (sqr - 3) >> 1; j < 131072; j += p)
                this.buf[j >> 5] |= 1 << (j & 31);
        } else { // other than the first "zeroth" page:
          if (!this.bpa.length) { // if this is the first page after the zero one:
            this.bps = new SoEPgClass(); // initialize separate base primes stream:
            this.bps.next(); // advance past the only even prime of 2
            this.bpa.push(this.bps.next()); // keep the next prime (3 in this case)
          }
          // get enough base primes for the page range...
          for (var p = this.bpa[this.bpa.length - 1], sqr = p * p; sqr < nxt;
            p = this.bps.next(), this.bpa.push(p), sqr = p * p);
          for (var i = 0; i < this.bpa.length; i++) { //for each base prime in the array
            var p = this.bpa[i];
            var s = (p * p - 3) >> 1; //compute the start index of the prime squared
            if (s >= this.lowi) // adjust start index based on page lower limit...
              s -= this.lowi;
            else { //for the case where this isn't the first prime squared instance
              var r = (this.lowi - s) % p;
              s = (r != 0) ? p - r : 0;
            }
            //inner tight composite culling loop for given prime number across page
            for (var j = s; j < 131072; j += p) this.buf[j >> 5] |= 1 << (j & 31);
          }
        }
      }
    }
    //find next marker still with prime status
    while (this.bi < 131072 && this.buf[this.bi >> 5] & (1 << (this.bi & 31))) this.bi++;
    if (this.bi < 131072) // within buffer: output computed prime
      return 3 + ((this.lowi + this.bi++) * 2);
    else { // beyond buffer range: advance buffer
      this.bi = 0;
      this.lowi += 131072;
      return this.next(); // and recursively loop just once to make a new page buffer
    }
  };
  return SoEPgClass;
})();

以上代码可用于通过以下JavaScript代码计算到达给定限制的素数:

window.onload = function () {
  var elpsd = -new Date().getTime();
  var top_num = 1000000000;
  var cnt = 0;
  var gen = new SoEPgClass();
  while (gen.next() <= top_num) cnt++;
  elpsd += (new Date()).getTime();
  document.getElementById('content')
    .innerText = 'Found ' + cnt + ' primes up to ' + top_num + ' in ' + elpsd + ' milliseconds.';
};

如果将以上两段JavaScript代码放入名为app.js的文件中,该文件位于与以下名为whatever.html的HTML代码相同的文件夹中,则可以通过打开HTML文件在浏览器中运行代码在其中:

<!DOCTYPE html>

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Page Segmented Sieve of Eratosthenes in JavaScript</title>
    <script src="app.js"></script>
  </head>
  <body>
    <h1>Page Segmented Sieve of Eratosthenes in JavaScript.</h1>

    <div id="content"></div>
  </body>
</html>

当使用即时(JIT)编译(如Google Chrome的V8引擎)在JavaScript执行引擎上运行时,此代码可以筛选到十亿秒范围内的几十秒。通过使用极限轮分解和预先剔除最低基本素数的页面缓冲区可以实现进一步的增益,在这种情况下,执行的工作量可以再减少四倍,这意味着可以计算素数的数量在几秒钟内高达十亿(计数不需要这里使用的枚举,而是可以直接在页面段缓冲区上使用位计数技术),但代价是代码复杂性增加。

<强> EDIT_ADD:

使用TypedArray和ECMAScript 2015中的asm.js优化(现在在所有常见浏览器中都支持),执行速度可以加快三倍或更多,代码修改如下:

"use strict";
var SoEPgClass = (function () {
  function SoEPgClass() {
    this.bi = -1; // constructor resets the enumeration to start...
    this.buf = new Uint8Array(16384);
  }
  SoEPgClass.prototype.next = function () {
    if (this.bi < 1) {
      if (this.bi < 0) {
        this.bi++;
        this.lowi = 0; // other initialization done here...
        this.bpa = [];
        return 2;
      } else { // bi must be zero:
        var nxt = 3 + 2 * this.lowi + 262144; // just beyond the current page
        for (var i = 0; i < 16384; ++i) this.buf[i] = 0 >>> 0; // zero buffer
        if (this.lowi <= 0) { // special culling for first page as no base primes yet:
          for (var i = 0, p = 3, sqr = 9; sqr < nxt; ++i, p += 2, sqr = p * p)
            if ((this.buf[i >> 3] & (1 << (i & 7))) === 0)
              for (var j = (sqr - 3) >> 1; j < 131072; j += p)
                this.buf[j >> 3] |= 1 << (j & 7);
        } else { // other than the first "zeroth" page:
          if (!this.bpa.length) { // if this is the first page after the zero one:
            this.bps = new SoEPgClass(); // initialize separate base primes stream:
            this.bps.next(); // advance past the only even prime of 2
            this.bpa.push(this.bps.next()); // keep the next prime (3 in this case)
          }
          // get enough base primes for the page range...
          for (var p = this.bpa[this.bpa.length - 1], sqr = p * p; sqr < nxt;
            p = this.bps.next(), this.bpa.push(p), sqr = p * p);
          for (var i = 0; i < this.bpa.length; ++i) { // for each base prime in the array
            var p = this.bpa[i] >>> 0;
            var s = (p * p - 3) >>> 1; // compute the start index of the prime squared
            if (s >= this.lowi) // adjust start index based on page lower limit...
              s -= this.lowi;
            else { // for the case where this isn't the first prime squared instance
              var r = (this.lowi - s) % p;
              s = (r != 0) ? p - r : 0;
            }
            if (p <= 8192) {
              var slmt = Math.min(131072, s + (p << 3));
              for (; s < slmt; s += p) {
                var msk = (1 >>> 0) << (s & 7);
                for (var j = s >>> 3; j < 16384; j += p) this.buf[j] |= msk;
              }
            }
            else
              // inner tight composite culling loop for given prime number across page
              for (var j = s; j < 131072; j += p) this.buf[j >> 3] |= (1 >>> 0) << (j & 7);
          }
        }
      }
    }
    //find next marker still with prime status
    while (this.bi < 131072 && this.buf[this.bi >> 3] & ((1 >>> 0) << (this.bi & 7)))
      this.bi++;
    if (this.bi < 131072) // within buffer: output computed prime
      return 3 + ((this.lowi + this.bi++) << 1);
    else { // beyond buffer range: advance buffer
      this.bi = 0;
      this.lowi += 131072;
      return this.next(); // and recursively loop just once to make a new page buffer
    }
  };
  return SoEPgClass;
})();

加速是有效的,因为它使用预先键入的ECMAScript原语数组,通过直接在数组中使用整数来避免开销(也避免通过使用浮点表示来浪费空间),并且还使用asm.js中可用的类型提示位操作使用无符号整数/字节。同样,为了节省数组分配的时间,它现在分配筛分数组一次,并为每个新的页面段分配它。它现在在低端1.92 Gigahertz CPU上大约16秒钟内筛分到10亿,而不是大约50秒。同样,该算法被修改以简化内部复合数表示(在比特打包位中)以获得较小素数的额外速度,这是剔除操作的大部分。

请注意,现在大约60%的消耗时间用于枚举找到的素数。对于通常使用这种筛子而言,这可以大大减少,仅通过将每个片段页面中的零位数加起来来计算所找到的素数。如果这样做了,那么在这个低端CPU上筛选到10亿的时间将接近7秒,并且可能还有一些优化可能(所有时间使用谷歌Chrome版本72 V8 JavaScript引擎,该引擎不断得到改进,以后的版本可能运行得更快。)

TBH,我个人不喜欢JavaScript的所有扩展和复杂性,这些都是使其成为现代化的#34;语言,特别是不喜欢动态类型,所以几年前它出现时就采用了微软的TypeScript。上面的代码实际上是对TypeScript输出的代码的修改,同时强调静态类型的面向对象编程(OOP)。在我看来,呼叫&#34; next&#34;实例方法通过标准方法将方法添加到&#34; prototype&#34;可能比调用一个函数慢得多,所以我测试它并发现确实如此,this runnable link通过将枚举更改为简单的输出闭包函数来枚举找到的素数大约快2.5倍

现在,我们可以通过计算this other runnable link with modified code中显示的找到素数的数量来完全消除素数枚举,表明即使有上述改进,找到的素数的枚举仍然花费的时间几乎与实际相同使用此算法进行筛选,并且能够将枚举时间确定为上述两个链接到可运行代码的运行时间之间的差异。

请注意,链接的运行时间将与我在此处提到的不同(可能更短),因为大多数当前的CPU将比我目前使用的平板电脑Windows CPU更快,更强大(英特尔) x5-Z3850在1.92 Gigahertz,JavaScript在你正在查看链接的机器上运行。

这使得JavaScript只比JVM或DotNet上实现的相同算法慢一点,当然,它仍然比从C / C ++,Rust,Nim,Haskell等语言编译的高度优化的本机代码慢得多。 ,Swift,FreePascal,Julia等,可以在这个低端CPU上运行这个算法大约两秒钟。根据浏览器的实现,WebAssembly可以比这里的JavaScript快两到三倍运行这个算法;同样,当WebAssembly规范完全实现并实施时,我们将通过所使用的有效内核数量的因素,为进一步增益提供多线程支持。

<强> END_EDIT_ADD

<强> EDIT_ADD_MORE:

一旦完成上述相当小的修改,只是有效地计算找到的素数而不是枚举它们,从而使计数时间与筛选它们相比是一个小开销,那么进行更广泛的更改以使用最大值是值得的车轮分解(不仅仅为2&#34;仅赔率&#34;,而且对于覆盖210个潜在质数的范围的车轮也为3,5和7)并且还预先剔除小筛分的初始化数组使得没有必要通过以下11,13,17和19的素数进行剔除。这使得使用页面分段筛子时的复合数量剔除操作的数量减少了大约4到10亿的范围由于每次剔除操作的操作减少了大约与上述代码相同的速度,因此可以编写它以使其运行速度快四倍。

有效地进行210跨度车轮分解的方法是遵循这种方法来实现&#34;仅赔率&#34;高效筛分:上面的当前算法可以被认为是从两个中筛出一个比特填充的平面,其中另一个平面可以被消除,因为它只包含两个以上的偶数;对于210跨度,我们可以定义48个这种大小的位填充阵列,代表11和11以上的可能质数,其中所有其他162个平面包含的数字是2,3或5或7的因子,因此不要需要考虑。通过这种方式,筛选具有更少的内存需求同样有效(与#34相比只有一半以上;仅有几率&#34;以及与此处一样多的效率,其中一个48平面&#34;页面&# 34;代表16千字节=每平面乘以131072位乘以210,这是每个筛网段的27,525,120个数字的范围,因此只有40个页面段可以筛选到十亿(而不是如上所述的近四千),因此启动开销更少每页基本素数的地址计算,以进一步提高效率。

虽然上面描述的扩展代码是几百行并且很长,但是在我的低端英特尔1.92 Gigahertz CPU上使用谷歌V8 JavaScript引擎,它可以在两秒内计算到10亿的素数。比在本机代码中运行的相同算法慢大约四到五倍。这是我们在JavaScript中可以做的极限,还有进一步的高级技术&#34; loop-unrolling&#34; (当然)多处理不可用。但是,在这个低端CPU上运行大约1.4秒时,几乎可以匹配Sieve of Atkin的手动优化参考C实现。

尽管上述代码在大约160亿的范围内非常有效,但其他改进可以帮助将效率保持在数万亿甚至更大的范围内,这样就可以计算出大约1e14的素数。几天后在更快的CPU上使用JavaScript。这很有意思,因为这个范围的质数数量直到1985年才知道,然后由数值分析技术确定,因为当时的计算机不够强大,无法足够快地运行Eratosthenes的Sieve。在合理的时间范围内。

使用我当前的#14;反JavaScript&#34;和职业编码风格偏见,我会用Fable编写这段代码,Fable是F#的实现(静态类型的ML&#34;功能&#34;语言也支持OOP,如果需要),它非常有效地转换为JavaScript,生成的代码可能与直接用JavaScript编写的代码一样快。

要显示代码在Chrome V8 JavaScript引擎中的运行速度几乎与使用Fable(使用Elmish React界面)编写纯JavaScript一样快,就像上面here is a link to a Fable online IDE containing the above algorithm上一个链接一样。它运行速度比纯JavaScript慢,JavaScript输出&#34; Code&#34; view显示了原因:为尾部调用优化(TCO)生成的代码与JavaScript的循环不是一个简单的循环 - 只需为紧密的内部剔除循环手动调整代码就可以获得相同的速度。代码是以函数式编写的,除了数组内容变异和序列生成器函数所必需的,它们与JavaScript的形式相同,以便于理解;如果代码的流生成器​​部分被编写为使用F#序列,没有可见的突变,那么它的工作速度会很快。

由于上面的Fable代码是纯F#,它也可以使用Fabulous库作为DotNet Core的JavaScript生成器运行,或者可以通过在DotNet Core下直接运行它来运行多平台和更快一点。

<强> END_EDIT_ADD_MORE

总之,有各种各样的算法可以在一秒钟内找到数百万的素数,但它需要一个基于Eratosthenes算法的有效页面分段数组来确定按顺序数十亿的质数执行时间。

答案 2 :(得分:5)

我会将此作为评论发布给亚历山大,但我没有这样做的声誉。他的回答很棒,而且只是调整它以使其更快。我通过测试n = 100,000,000来进行基准测试。

不是在'array'中使用true和false,而是使用1和0来提高速度。这使我在Chrome中的时间从5000毫秒减少到4250毫秒。 Firefox未受影响(无论如何都是5600毫秒)。

然后我们可以考虑到偶数永远不会是素数。把2放入'输出',你可以做i = 3;在筛子期间i + = 2,并且j + = i * 2(我们可以跳过偶数倍数,因为任意数字乘以偶数偶数),只要我们也i + = 2同时推动'输出'结束。这使我在Chrome中的时间从4250毫秒减少到3350毫秒。 Firefox受益较少,从5600毫秒降至4800毫秒。

无论如何,这两个调整的结合使我在Chrome中的速度提升了33%,在Firefox中提升了14%。这是亚历山大代码的改进版本。

var eratosthenes = function(n) {
    // Eratosthenes algorithm to find all primes under n
    var array = [], upperLimit = Math.sqrt(n), output = [2];

    // Make an array from 2 to (n - 1)
    for (var i = 0; i < n; i++)
        array.push(1);

    // Remove multiples of primes starting from 2, 3, 5,...
    for (var i = 3; i <= upperLimit; i += 2) {
        if (array[i]) {
            for (var j = i * i; j < n; j += i*2)
                array[j] = 0;
        }
    }

    // All array[i] set to 1 (true) are primes
    for (var i = 3; i < n; i += 2) {
        if(array[i]) {
            output.push(i);
        }
    }

    return output;
};

答案 3 :(得分:2)

为了好玩,我实施了严格遵循TDD规则的Erastoten筛选算法(使用Node运行)。这个版本应该足够用于采访,作为学校运动或者像我一样 - 为了搞乱一点。

让我说,我绝对认为接受的答案应该是GordonBGood提供的答案。

module.exports.compute = function( size )
{
    if ( !utils.isPositiveInteger( size ) )
    {
        throw new TypeError( "Input must be a positive integer" );
    }

    console.time('optimal');
    console.log();
    console.log( "Starting for optimal computation where size = " + size );
    let sieve = utils.generateArraySeq( 2, size );

    let prime = 2;
    while ( prime )
    {
        // mark multiples
        for ( let i = 0; i < sieve.length; i += prime )
        {
            if ( sieve[i] !== prime )
            {
                sieve[i] = -1;
            }
        }

        let old_prime = prime;
        // find next prime number
        for ( let i = 0; i < sieve.length; i++ )
        {
            if ( ( sieve[i] !== -1 ) && ( sieve[i] > prime ) )
            {
                prime = sieve[i];
                break;
            }
        }

        if ( old_prime === prime )
        {
            break;
        }
    }
    console.timeEnd('optimal');
    // remove marked elements from the array
    return sieve.filter( 
        function( element )
        {
            return element !== -1;
        } );
} // compute

我会感激任何有意义的批评。

The whole repository can be found on my github account.

答案 4 :(得分:0)

由于我参加聚会有点晚了。我想添加一些简单的技巧,以找出所有不超过100的质数:

<!DOCTYPE html>
<html>
<title>Primes</title>
<head>
<script>
function findPrimes() {
    var primes = []
    var search = []

    var maxNumber = 100
    for(var i=2; i<maxNumber; i++){
        if(search[i]==undefined){
            primes.push(i);
            for(var j=i+i; j<maxNumber; j+=i){
                search[j] = 0;
            }
        }
    }
   document.write(primes);
}
findPrimes();
</script>
</head>
<body>
</body>
</html>

答案 5 :(得分:0)

function sieveOfEratosthenes(num, fromSt = null) {
let boolArr = Array(num + 1).fill(true); // Taking num+1 for simplicity
boolArr[0] = false;
boolArr[1] = false;

for (
    let divisor = 2;
    divisor * divisor <= num;
    divisor = boolArr.indexOf(true, divisor + 1)
)
    for (let j = 2 * divisor; j <= num; j += divisor) boolArr[j] = false;

let primeArr = [];
for (
    let idx = fromSt || boolArr.indexOf(true);
    idx !== -1;
    idx = boolArr.indexOf(true, idx + 1)
)
    primeArr.push(idx);

return primeArr;
}

答案 6 :(得分:0)

此算法每100万个千毫秒(ok 2 - big test # time=498.815ms):

module.exports.fast = function eratosthenes (max) {
  let sqrt = Math.sqrt(max)
  let sieve = new Array(max).fill(0)

  for (let primeCandidate = 2; primeCandidate < sqrt; primeCandidate++) {
    if (sieve[primeCandidate] === true) {
      continue // already processed
    }
    for (let multiple = primeCandidate * primeCandidate; multiple < max; multiple += primeCandidate) {
      if (sieve[multiple] === 0) {
        sieve[multiple] = true
      }
    }
  }

  return sieve
    .map((isPrime, i) => ({ i, isPrime })) // find the number associated with the index
    .filter(({ i, isPrime }) => isPrime === 0 && i >= 2) // remove not prime numbers
    .map(({ i }) => i) // output only the values
}

eratosthenes(1000000)返回包含78498个质数的数组。