Сито алгоритма Эратосфена в JavaScript работает бесконечно для большого числа

Я пытаюсь написать алгоритм Sieve of Eratosthenes в JavaScript. В основном я просто буквально выполнил следующие шаги:

  • Создайте список последовательных целых чисел от 2 до (n-1)
  • Пусть первое простое число p равно 2
  • Начиная с p, подсчитывайте с шагом p и удаляйте каждое из этих чисел (p и кратность p)
  • Перейдите к следующему номеру в списке и повторите 2,3,4
  • Добавить непреднамеренно удаленные простые числа обратно в список

и это то, что я придумал:

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 для тестирования, и процесс просто кажется бесконечным и ошибки памяти не было. Я прочитал решение здесь (также в javascript), но до сих пор не могу его полностью понять.

Вопрос: Как сделать эту работу для достаточно больших чисел, таких как миллион и выше?

Ответ 1

Вы делаете Sieve of Eratosthenes намного медленнее, используя функции манипулирования массивами, такие как Array#indexOf и Array#splice которые выполняются за линейное время. Когда вы можете иметь O (1) для обеих задействованных операций.

Ниже приводится Сито Эратосфена в соответствии с традиционными практиками программирования:

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;
};

Вы можете увидеть живой пример для n = 1 000 000 здесь.

Ответ 2

Этот вопрос немного скуп на низкую сторону в определении того, что такое "большое число", и допускает, что оно начинается только с миллиона, для которого работает текущий ответ; тем не менее, он использует довольно много памяти, как в одном 8-байтовом числе (двойное действительное число в 64 бита) для каждого просеиваемого элемента и еще одно 8-байтовое число для каждого найденного простого числа. Этот ответ не сработает для "больших чисел", скажем, около 250 миллионов и выше, так как он превысит объем памяти, доступный для исполняющей машины JavaScript.

Следующий код JavaScript, реализующий "бесконечное" (неограниченное) Page Segmented Sieve of 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 в той же папке, что и следующий HTML-код с именем what.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>

Этот код может просеять до миллиардного диапазона в несколько десятков секунд при запуске на обработчике JavaScript с использованием компиляции Just-In-Time (JIT), такой как Google Chrome V8. Дальнейшее увеличение может быть достигнуто с помощью экстремального факторинга колес и предварительного отбора буферов страниц с наименьшими базовыми простыми числами, и в этом случае объем выполненной работы можно сократить еще на четыре раза, что означает, что число простых чисел может быть подсчитано до миллиарда за несколько секунд (для подсчета не требуется перечисление, как здесь используется, скорее, можно напрямую использовать методы подсчета битов в буферах сегмента страницы), хотя за счет увеличения сложности кода.

EDIT_ADD:

Скорость выполнения может быть увеличена в три или более раз с помощью TypedArray и оптимизаций asm.js из ECMAScript 2015 (теперь поддерживается во всех распространенных браузерах) с изменениями кода следующим образом:

"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, для вызова битовых манипуляций. используйте целые числа без знака/байты. кроме того, чтобы сэкономить время на выделение массивов, теперь он выделяет массив просеивания один раз и просто обнуляет его для каждого нового сегмента страницы. Теперь он экономит до миллиарда примерно за 16 секунд на младшем 1,92 гигагерцовом процессоре, а не примерно за 50 секунд. Кроме того, алгоритм модифицирован, чтобы упростить представление внутреннего составного числа (в битах, упакованных в биты) для дополнительной скорости для меньших простых чисел, что является большинством операций отбраковки.

Обратите внимание, что теперь около 60% затраченного времени тратится только на перечисление найденных простых чисел. Это может быть значительно уменьшено для обычного использования такого сита, чтобы просто подсчитать найденные простые числа, просто суммируя число нулевых битов в массиве для каждой страницы сегмента. Если бы это было сделано, то время для уменьшения до миллиарда было бы примерно 7 секунд на этом младшем процессоре, и возможны еще несколько возможных оптимизаций (все время с использованием механизма JavaScript Google Chrome версии 72 V8, который постоянно совершенствуется и более поздние версии могут работать быстрее).

TBH, мне лично не нравится JavaScript со всеми его расширениями и сложностями, которые были необходимы, чтобы сделать его "современным" языком и, в частности, я не люблю динамическую типизацию, поэтому принял Microsoft TypeScript, когда он появился несколько лет назад. Приведенный выше код на самом деле является модификацией кода, выводимого из TypeScript, с акцентом на статически типизированное объектно-ориентированное программирование (ООП). Мне пришло в голову, что вызов метода "следующего" экземпляра с помощью стандартного способа добавления методов к "прототипу" может быть намного медленнее, чем просто вызов функции, поэтому я протестировал его и обнаружил, что это именно тот случай, с этим runnable связать перечисление найденных простых чисел примерно в два с половиной раза быстрее, просто изменив перечисление на простую функцию закрытия вывода.

Теперь мы можем полностью исключить перечисление простых чисел, просто посчитав количество найденных простых чисел, как показано в этой другой работающей ссылке с измененным кодом, показывая, что даже с указанным улучшением перечисление найденных простых чисел по-прежнему стоит почти столько же времени, сколько фактически выполняется. просеивание по этому алгоритму, позволяющему определить время перечисления как разницу между временем выполнения для двух вышеупомянутых ссылок на исполняемый код.

Обратите внимание, что время выполнения для ссылок будет отличаться (и, вероятно, будет короче), чем, как я упоминаю здесь, так как большинство современных процессоров будет быстрее и мощнее, чем процессор Windows для планшетов, который я сейчас использую (Intel x5-Z3850 с тактовой частотой 1,92 ГГц и JavaScript запускается на машине, на которой вы просматриваете ссылку.

Это делает JavaScript чуть медленнее, чем тот же алгоритм, реализованный в JVM или DotNet, который, конечно, все еще намного медленнее, чем высокооптимизированный нативный код, скомпилированный из таких языков, как C/C++, Rust, Nim, Haskell, Swift, FreePascal, Julia и т.д., Которые могут запустить этот алгоритм за две секунды на этом младшем процессоре. WebAssembly может запускать этот алгоритм примерно в два-три раза быстрее, чем JavaScript, в зависимости от реализации браузера; Кроме того, когда спецификация WebAssembly будет полностью завершена и реализована, мы получим поддержку многопоточности для дальнейшего увеличения в зависимости от количества используемых используемых ядер.

END_EDIT_ADD

EDIT_ADD_MORE:

После того, как вышеупомянутые довольно небольшие модификации сделаны, чтобы просто подсчитать найденные простые числа, а не перечислить их, таким образом, делая время подсчета небольшим накладным расходом по сравнению с просеиванием, было бы целесообразно внести более обширные изменения, чтобы использовать максимальную факторизацию колес ( не только на 2 для "только шансов", но также на 3, 5 и 7 для колеса, охватывающего диапазон из 210 потенциальных простых чисел), а также на предварительный отбор при инициализации небольших решетчатых матриц, так что нет необходимости Отбор на следующие простые числа из 11, 13, 17 и 19. Это уменьшает количество операций отбора составного числа при использовании сегментированного сита страницы примерно в четыре раза до диапазона миллиардов и может быть записано так, чтобы оно выполнялось примерно в четыре раза быстрее из-за сокращения операций с каждой операцией отбраковки примерно с той же скоростью, что и для приведенного выше кода.

Эффективный способ факторизации колеса с 210 пролетами состоит в том, чтобы следовать этому методу эффективного просеивания "только шансы": текущий алгоритм, описанный выше, можно рассматривать как просеивание одной плоскости с битами из двух, где другая плоскость может быть исключено, так как содержит только четные числа над двумя; для диапазона 210 мы можем определить 48 битовых массивов этого размера, представляющих возможные простые числа от 11 и выше, где все остальные 162 плоскости содержат числа, которые являются множителями двух, трех, пяти или семи, и, следовательно, не нуждаются в быть принятым во внимание. Таким образом, столь же эффективно просеивать с меньшими требованиями к памяти (более чем вдвое по сравнению с "только коэффициентами") и такой же эффективностью, как здесь, где одна "страница" с 48 плоскостями представляет 16 килобайт = 131072 бит на плоскость умножить на 210, что составляет диапазон 27 525 120 номеров в сегменте просеивающей страницы, таким образом, только 40 сегментов страницы просеивают до миллиарда (вместо почти четырех тысяч, как указано выше), и, следовательно, меньше накладных расходов при расчете начального адреса на базовое простое число на сегмент страницы для дальнейшее повышение эффективности.

Несмотря на то, что описанный выше расширенный код занимает несколько сотен строк и может быть размещен здесь, он может подсчитать число простых чисел до миллиарда менее чем за две секунды на моем младшем процессоре Intel 1.92 Гигагерца с использованием JavaScript-движка Google V8, что составляет около четырех в пять раз медленнее, чем тот же алгоритм, выполняемый в нативном коде. Это почти предел того, что мы можем сделать в JavaScript, с недоступными дополнительными продвинутыми методами "развертывания цикла" и (конечно) многопроцессорной обработки. Тем не менее, этого почти достаточно, чтобы соответствовать оптимизированной вручную эталонной реализации C Sieve of Atkin на этом младшем процессоре, который работает примерно за 1,4 секунды.

Хотя приведенный выше код достаточно эффективен, вплоть до диапазона около 16 миллиардов, другие улучшения могут помочь сохранить эффективность на еще больших диапазонах, составляющих несколько десятков тысяч миллиардов, так что можно посчитать число простых чисел до 1e14 в нескольких дни с использованием JavaScript на более быстром процессоре. Это интересно, поскольку число простых чисел в этом диапазоне не было известно до 1985 года, а затем было определено методом численного анализа, поскольку компьютеры того времени не были достаточно мощными, чтобы запустить Сито Эратосфена достаточно быстро для этого диапазона в разумный срок.

С моим текущим "анти-JavaScript" и профессиональным смещением стиля кодирования я написал бы этот код, используя Fable, который является реализацией F # (статически типизированный ML "функциональный" язык, который также поддерживает ООП, если это необходимо), который переносится в JavaScript очень эффективно, так что сгенерированный код, вероятно, будет работать примерно так же быстро, как если бы он был написан непосредственно на JavaScript.

Чтобы показать, что код в JavaScript-движке Chrome V8 с использованием Fable (с интерфейсом Elmish React) может работать почти так же быстро, как и при написании чистого JavaScript, как в последней ссылке выше, здесь приведена ссылка на онлайн-среду разработки Fable, содержащую приведенный выше алгоритм. Он работает немного медленнее, чем чистый JavaScript, и представление "Код" в выводе JavaScript показывает, почему: код, сгенерированный для Tail Call Optimizations (TCO), не совсем простой цикл для JavaScript - было бы легко настроить вручную код только для тесных внутренних циклов отбора, чтобы получить ту же скорость. Код написан в функциональном стиле, за исключением мутации содержимого массива и по мере необходимости для функций генератора последовательностей, которые находятся в той же форме, что и JavaScript для простоты понимания; было бы работать так же быстро, если бы эта часть кода потокового генератора была написана для использования последовательностей F # без видимой мутации.

Поскольку вышеприведенный код Fable является чистым F #, он также может работать с библиотекой Fabulous в качестве генератора JavaScript от DotNet Core или может работать на нескольких платформах и немного быстрее, напрямую запуская его под DotNet Core.

END_EDIT_ADD_MORE

Таким образом, существуют всевозможные алгоритмы, которые могут находить простые числа до нескольких миллионов с точностью до секунды, но для эффективного определения простых чисел до миллиардов в таком порядке времени выполнения требуется эффективный алгоритм Sieve of Eratosthenes на основе массива страниц.,

Ответ 3

Я бы разместил это в качестве комментария к Александру, но у меня нет такой репутации. Его ответ потрясающий, и это просто улучшает его, чтобы сделать его быстрее. Я сравнивал результаты тестирования n = 100 000 000.

Вместо использования true и false в 'array', я получаю большое ускорение скорости, используя 1 и 0. Это сократило мое время в Chrome с 5000 мс до 4250 мс. Firefox не был затронут (5600 мс в любом случае).

Тогда мы можем учесть, что четные числа никогда не будут первыми. Поместите 2 в "выход" с бита, и вы можете сделать я = 3; я + = 2 и j + = я * 2 во время сита (мы можем пропустить четные кратные числа, так как любое число раз четное число четное), пока мы также я + = 2 при нажатии на 'output' на конец. Это сократило мое время в Chrome с 4250 мс до 3350 мс. Firefox выиграл чуть меньше, снизившись с 5600 мс до 4800 мс.

В любом случае комбинация этих двух настроек дала мне 33% -ный прирост скорости в Chrome и 14% -ный рост в Firefox. Здесь улучшена версия кода Александра.

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;
};

Ответ 4

Просто для удовольствия, я реализовал алгоритм решета Erastoten (бег с Node), строго следуя правилам TDD. Эта версия должна быть достаточной для интервью, как школьное упражнение или так же, как и я, - для битвы немного.

Позвольте мне сказать, что я определенно считаю, что принятый ответ должен быть предоставлен 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

Я по достоинству оценю всякую чувствительную критику.

Весь репозиторий можно найти в моей учетной записи github.

Ответ 5

Так как я немного опоздал на вечеринку. Я хотел бы добавить свой простой и немного хакерский вклад, который находит все простые числа до 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>