Гоша: Тимофей, недавно ты объяснял, что такое простые числа. Это числа, которые делятся без остатка только на 1 или на себя. Я решил написать функцию, которая определяет, является ли число простым.
Ряд простых чисел начинается с 2, ведь 1 не считается простым. 3 — простое число. Эти случаи можно обработать отдельно. Для других чисел будем выполнять проверку: если число n делится без остатка на какое-то из чисел, меньших n, но больших 1, то n — составное.
Скопировать кодJSX
def is_prime(n):
if n < 2:
return False
if n < 4:
return True
i = 2while i < n:
if n % i == 0:
return False
i = i + 1return True
Тимофей: Алгоритм корректный. Но кажется, он будет долго работать.
Какая сложность предложенного алгоритма?
Тимофей: Можно решить эту задачу быстрее. Проверять числа, большие чем n не обязательно. Если у числа есть делитель, больший n, то есть и делитель, меньший n. А его мы проверим раньше.
Скопировать кодJSX
def is_prime(n):
if n < 2:
return False
if n < 4:
return True
i = 2while i * i <= n:
if n % i == 0:
return False
i = i + 1return True
Какая стала сложность алгоритма?
Гоша: Действительно. Все числа не нужно проверять. А почему ты написал i⋅i⩽n, а не i⩽n?
Тимофей: Операция вычисления корня очень дорогая. Вариант с умножением более эффективный.
Гоша: Спасибо. Нужно будет запомнить этот приём.
Кстати, с помощью функции is_prime(n) можно найти все простые числа, меньшие числа n.
Для этого заведём пустой массив smaller_primes. Будем проверять все числа, меньшие n, на простоту. Если число простое, добавим его в массив smaller_primes. В конце работы алгоритма в этом массиве будет содержаться ответ.
Скопировать кодJSX
def get_smaller_primes(n):
smaller_primes = []
for num in range(2, n):
if is_prime(num):
smaller_primes.append(num)
return smaller_primes
Тимофей: Да, так можно определить все простые числа, не превосходящие n. Но этот метод не оптимальный. Его не стоит применять на практике.
Гоша: А какой метод более оптимальный?
Тимофей: Например, решето Эратосфена. Оно помогает найти все простые числа, не превосходящие n.
Алгоритм такой:
Выписываем все целые числа от 2 до n.
Заводим переменную num, равную первому не рассмотренному простому числу. Изначально она равна 2.
Помечаем в списке числа от 2⋅num до n с шагом, равным num составными, то есть False. Например, для 2 пометим числа 4, 6, 8 и так далее.
Находим первое не рассмотренное число в списке, большее чем num, и присваиваем переменной num это число.
Повторяем два предыдущих шага, пока это возможно.
Код функции, реализующий решето Эратосфена:
Скопировать кодJSX
def eratosthenes(n):
numbers = list(range(2, n + 1))
for num in range(2, n):
if is_prime(num):
for j in range(2 * num, n + 1, num):
numbers[j - 2] = False
return numbers
Посмотрим, что вернёт функция, если вызвать её с n=9.
Скопировать кодJSX
[2, 3, False, 5, False, 7, False, False]
Простые числа остались на своём месте. Там, где были составные числа, стоит False.
Алгоритм можно оптимизировать. Для каждого числа num начнём рассмотрение с числа p2. Ведь все составные числа, меньшие него уже будут рассмотрены.
Получится такой код:
Скопировать кодJSX
def eratosthenes_effective(n):
numbers = list(range(2, n + 1))
for num in range(2, n):
if is_prime(num):
for j in range(num * num, n + 1, num):
numbers[j - 2] = False
return numbers
Рассмотрим подробно пример работы алгоритма для n=15.
Скопировать кодJSX
23456789101112131415 # Запишем числа от 2 до 15
num = 2 # Пометим все числа, кратные 2, начиная с 4 False
23 False 5 False 7 False 9 False 11 False 13 False 15
num = 3 # Пометим все числа, кратные 3, начиная с 9 False
23 False 5 False 7 False False False 11 False 13 False False False False
num = 3 Алгоритм можно завершить, так как num**2 больше 15.
Все числа, меньшие 15 кратные 5 уже рассмотрены.
Гоша: Интересный алгоритм, спасибо. Стало понятно, откуда название «решето». Мы как будто просеиваем числа, оставляя только простые. А есть ли еще более эффективный метод?
Тимофей: Решето Эратосфена работает за O(nlog(logn)). Чтобы это доказать, нужно знать некоторые более сложные факты из теории чисел, чем про которые я рассказывал. Глядя на код видно, что он работает не за линейное время. Существует метод решения задачи нахождения всех простых чисел, не превосходящих n, которому требуется O(n) операций. Метод называется линейное решето.
Алгоритм такой:
Заведём массив sp (от smaller_primes) длины n+1. А также массив primes, в который будем добавлять найденные простые числа.
Для каждого числа i будем хранить sp[i] — минимальный простой делитель числа i.
Перебираем i по возрастанию.
Если sp[i] = 0, можно сделать вывод, что число простое и добавить его в массив primes.
Рассматриваем все простые числа p, которые не больше sp[i]. Обновляем sp[p * sp[i]] = p.
Скопировать кодJSX
def get_smaller_primes_linear(n):
lp = [0] * (n + 1)
primes = []
for i in range(2, n):
if lp[i] == 0:
lp[i] = i
primes.append(i)
for p in primes:
if (p > lp[i]) or (p * i > n):
break
lp[p * i] = p
return primes, lp
Если вызвать функцию от 8, получим ([2, 3, 5, 7], [0, 0, 2, 3, 2, 5, 2, 7, 2]).
Первые два числа в массиве sp мы не затрагивали. Можно создать массив, длина которого на 2 меньше. В этом случае формула для индекса будет менее наглядной.
Основная идея в том, что к каждой ячейке массива sp мы обратимся не более одного раза. То есть сложность алгоритма линейная.