TL;DR: проблема не в памяти, а в скорости.
Вы были очень изобретательны в преодолении недостатка знаний. :) На самом деле, у нас есть progn
в Common Lisp, который группирует несколько вычисляемых выражений по порядку:
(defun myprimes-to (n &aux ;; &aux vars may have init values
(current 3)
(primecount 1)
(isprime t) ; "yes" ;; t is better
(primes (list 2))
(loop ;for x from current do ;; for a bare loop, just `loop` is enough
(progn ;list ;; `progn` groups expressions together to be
(setq isprime t) ; "yes" ;; evaluated in order, one after another
(loop ;; (not even needed here, see comments below)
for prime in primes do
(if (= (mod current prime) 0)
(progn ;list
(setq isprime nil)
(return)) ;; exit the loop
nil)) ;; don't have to have alternative clause
(if isprime
(progn ;list
(incf primecount) ; (setq primecount (+ primecount 1)) ;; same but shorter
(setq primes (cons current primes))
(if (= primecount n) ;; abstract the n as function's argument
(return) ;; exit the loop
nil))
;; What?? increment your loop var _only_ if it's not prime?
(setq current (+ current 1)))))
;; (write-line (write-to-string primes))
primes) ;; just return them
Поэтому, если current
является простым, попытка повторяется. Поскольку в последний раз он был добавлен в primes
, теперь он будет считать себя не простым, и итерация продолжится. Результаты правильные, но логика искажена.
Что еще более важно, здесь есть две основные неэффективности с точки зрения алгоритма: вы тестируете все primes
до сих пор и обратно. Но у любого заданного числа гораздо больше шансов иметь меньший делитель, чем больший; поэтому мы должны проверять по простым числам в возрастающем порядке. И мы можем остановиться, когда простое число p
равно p*p > current
, потому что если current == a*b
и a <= b
, то a*a <= b*a == current
. Ранняя остановка дает огромное ускорение, она изменяет сложность времени выполнения с ~ n^2 до, грубо говоря, ~ n^1,5 (в произведенных n простых числах).
Обратите внимание: ваш код хуже, чем ~ n^2, алгоритмически. Было бы ~ n^2, если бы проверялись простые числа в порядке возрастания. -- Почему это важно? Это дает частичный ответ на ваш вопрос: у вас проблемы не с памятью, а с скоростью. Сколько бы времени ни потребовалось вашему коду для завершения n=100
, для перехода к n=1000
потребуется более чем в 100 раз (поскольку 1000/100 == 10, а 10^2 = 100). И чтобы добраться до n=10000
, потребуется в 100 раз больше времени. Таким образом, если задача n=100
заняла секунду времени выполнения, n=1000
займет 100 секунд (~ 2 минуты), а n=10001
200 минут (более чем через 3 часа).
Мы всегда можем получить оценки порядков роста во время выполнения эмпирически; см. эту статью о WP.
Таким образом, вам нужно поддерживать primes
в порядке возрастания и append
каждого нового простого числа до конца за время O(1). Вам нужно добиться тех же результатов, что и строка кода (setq primes (append primes (list current)))
; но вы не можете использовать эту строку кода, потому что это O (n) по времени и резко замедлит работу всей программы. То же самое с (setq primes (reverse (cons current (reverse primes))))
. И мы не можем поддерживать primes
в обратном порядке, потому что мы должны тестировать в порядке возрастания, и простой вызов (reverse primes)
для каждого нового current
также является операцией времени O(n).
Решение состоит в том, чтобы использовать rplacd
: (rplacd (last primes) (list current))
. Даже это все равно неправильно, потому что last
тоже операция O(n). Вместо этого вам придется поддерживать дополнительную переменную, указывающую на последнюю cons-ячейку списка простых чисел, и продвигать ее каждый раз, когда вы добавляете туда новое простое число: (setq last_cell (cdr last_cell))
.
Когда вы запустите фиксированный код, вы обнаружите, что он завершает задание 10 000 примерно за 0,1 секунды и работает примерно за ~ n^1,4 порядки роста во время выполнения в этом диапазоне.
person
Will Ness
schedule
26.12.2013