Задался я тут задачей по-быстрому найти все простые числа, которые вмещаются в тип данных
int. Оглядываясь назад, глупая, наверное, затея, учитывая что этих чисел на 800 МБ и поиск совсем не быстрый. Но тем не менее.
Так как я хотел сделать это по-быстрому, то начал с самого простого: перебор в лоб с проверкой на делимость. Сразу же можно заметить, что перебирать можно только нечётные кандидаты, а на делимость проверять только найденными простыми. Очевидно кроме того (особенно, если проделывать эти операции в ручную на калькуляторе какое-то время), что проверять делимость кандидата на числа большее, чем его корень смысла нет: результат всегда будет меньше корня, и, если бы делимость была на цело, то она была бы уже найдена для меньшего сомножителя.
Единственный вопрос в том, как определять окончание проверки кандидата: 1) можно на каждом шаге цикла проверки находить квадрат делителя и сравнивать его с кандидатом:
if (primeNominee < primeTester * primeTester)...; 2) а можно заранее посчитать корень
primeLimit = (long) Math.sqrt(primeNominee); и на каждом шаге делители сравнивать с найденным порогом:
if (primeLimit < primeTester)... На практике второй вариант оказался чуть-чуть быстрее. В результате этих соображений получается такая программа:
class Primes {
private static final int primesNumber = 1000000;
private static final long[] primesList = new long[primesNumber];
static {
int primeIndex, testerIndex;
long primeNominee, primeTester, primeLimit;
primesList[0] = 2;
primesList[1] = 3;
primeIndex = 2;
primeNominee = 5;
outerLoop: while (true) {
testerIndex = 1;
primeLimit = (long) Math.sqrt(primeNominee);
while (true) {
primeTester = primesList[testerIndex];
if (primeLimit < primeTester) {
primesList[primeIndex] = primeNominee;
++primeIndex;
if (primesNumber == primeIndex) {
break outerLoop;
}
break;
}
if (0 == primeNominee % primeTester) {
break;
}
++testerIndex;
}
primeNominee += 2;
}
}
public static long getPrime(int index) {
return primesList[index];
}
public static long getLastPrime() {
return primesList[primesNumber - 1];
}
}
public class Primes_Search {
public static void main(String[] args) {
long startTime, stopTime;
System.out.println(String.format("%13d", Integer.MAX_VALUE));
startTime = System.nanoTime();
System.out.println(String.format("%13d", Primes.getLastPrime()));
stopTime = System.nanoTime();
System.out.println(String.format("\n%8.4f", 1e-9 * (stopTime - startTime)));
}
}
Однако, я был разочарован, так как работает этот алгоритм чрезвычайно медленно (не то, чтобы у меня было с чем сравнивать). Единственные решения для улучшения алгоритма, которые я смог придумать не меняя его сути, заключаются в следующем. Можно перебирать не просто нечётные числа с шагом 2 (то есть не делящиеся нацело на 2), а нечётные с переменным шагом
2, 4, которые не будут делиться не только на 2, но и на 3. Этот подход можно улучшить, если так же исключить числа, делящиеся на 5, взяв переменный шаг
2, 4, 2, 4, 6, 2, 6, 4. Связанный с этим подходом в английском есть термин
Wheel factorization.
Кроме того, от медленного вычисления корня можно избавиться, если заметить, что он плавно растёт вместе сплавным ростом кандидата в простое число, а следующий простой делитель в список делителей для проверки добавляется, когда кандидат пересекает очередной квадрат простого числа. Все эти соображения дают такой улучшенный алгоритм:
class Primes {
private static final int primesNumber = 1000000;
private static final long[] primesList = new long[primesNumber];
static {
int primeIndex, testerIndex, stepIndex, limitIndex;
long primeNominee, limitSquare;
primesList[0] = 2;
primesList[1] = 3;
primesList[2] = 5;
primesList[3] = 7;
primeIndex = 4;
primeNominee = 11;
int[] stepsList = {2, 4, 2, 4, 6, 2, 6, 4};
stepIndex = 0;
limitIndex = 3;
limitSquare = 49;
outerLoop: while (true) {
if (limitSquare == primeNominee) {
++limitIndex;
limitSquare = primesList[limitIndex];
limitSquare *= limitSquare;
} else {
testerIndex = 3;
while (true) {
if (limitIndex == testerIndex) {
primesList[primeIndex] = primeNominee;
++primeIndex;
if (primesNumber == primeIndex) {
break outerLoop;
}
break;
}
if (0 == primeNominee % primesList[testerIndex]) {
break;
}
++testerIndex;
}
}
primeNominee += stepsList[stepIndex];
++stepIndex;
if (8 == stepIndex) {
stepIndex = 0;
}
}
}
public static long getPrime(int index) {
return primesList[index];
}
public static long getLastPrime() {
return primesList[primesNumber - 1];
}
}
Однако, последние два улучшения являются по сути украшательством, так как прибавка в скорости составляет чуть больше 4%. Очевидно, что основное время выполнения программы набирается на операции деления с остатком ближе к концу работы при проверке больших простых чисел или же чисел с большими простыми множителями. Откидывание чисел, кратных 2, 3 и 5, практически ничего не даёт, так как такие числа в первом примере отсеиваются сразу на первых итерациях внутреннего цикла.
В связи с этим я начинаю задумываться о принципиально других алгоритмах поиска простых чисел. Первый в очереди — решето Эратосфена. Однако, прежде чем переходить к новому алгоритму со всеми его особенностями, хотелось бы оценить сложность наивного алгоритма. С этим у меня проблемы. Внешний цикл имеет количество итераций
, где
N — это граница сверху на искомые простые числа, а не количество к поиску, как в моих примерах. С внутренним же всё сложнее: количество его итераций скачет туда-сюда и в худшем случае составляет где-то
. Хотелось бы теперь просто перемножить эти две величины и получить что-то типа
, но на самом деле вторую величину надо интегрировать (читай: суммировать по всем нечётным
n от 3 до
N), учитывая при этом её случайным образом пляшущий характер. Как быть?
П.С. По решету Эратосфена рекомендации тоже с радостью принимаются.