Нахождение простых чисел: полный разбор с программным кодом

Раздел: Python -> Учебные задачи

Основные алгоритмы поиска простых чисел

Как эффективно найти все простые числа до заданного числа N?

Наиболее производительное решение - решето Эратосфена. Алгоритм работает за O(N log log N) и потребляет O(N) памяти.


def sieve_of_eratosthenes(n):
    if n < 2:
        return []
    is_prime = [True] * (n + 1)
    is_prime[0] = is_prime[1] = False
    for i in range(2, int(n ** 0.5) + 1):
        if is_prime[i]:
            for j in range(i * i, n + 1, i):
                is_prime[j] = False
    return [i for i, prime in enumerate(is_prime) if prime]

primes = sieve_of_eratosthenes(50)
print(primes)

алгоритм решения задачи python (алгоритм решения задачи на python)

[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]

базовые задачи python (базовые задачи python)

Пояснение шагов:

  • Создание списка: [True] * (n+1) - все числа от 0 до N считаются простыми.
  • Исключение 0 и 1: они не являются простыми.
  • Внешний цикл: перебор от 2 до квадратного корня из N. Если число простое, то вычёркиваются его кратные.
  • Внутренний цикл: начинается с i*i, чтобы не повторять вычёркивание ранее обработанных множителей.
  • Формирование результата: генератор списка собирает индексы, где значение True.

Типичные ошибки:

  • Забыть исключить 0 и 1 - они будут ошибочно включены в результат.
  • Внутренний цикл начинать с i вместо i*i - это избыточно, но не ломает логику, только замедляет.
  • Использование range(n+1) без учёта границ - возможен выход за пределы.

Как проверить, является ли конкретное число простым?

Простой перебор делителей от 2 до n-1. Работает медленно для больших чисел, но подходит для обучения.


def is_prime_naive(n):
    if n < 2:
        return False
    for i in range(2, n):
        if n % i == 0:
            return False
    return True

print(is_prime_naive(17))
print(is_prime_naive(18))

задачи для обучения python (задачи для обучения python)

True
False

задачи на классы в python (задачи на классы в python)

Оптимизация: достаточно проверять делители до sqrt(n).


def is_prime_optimized(n):
    if n < 2:
        return False
    if n == 2:
        return True
    if n % 2 == 0:
        return False
    i = 3
    while i * i <= n:
        if n % i == 0:
            return False
        i += 2
    return True

print([i for i in range(20) if is_prime_optimized(i)])

множество python задачи (задачи на множества в python)

[2, 3, 5, 7, 11, 13, 17, 19]

задачи на модули python (задачи на модули в python)

Проблемы:

  • Число 2 - чётное, но простое. Проверка на чётность может его исключить, поэтому нужна отдельная обработка.
  • При n близком к 1e12 цикл до sqrt(n) всё равно может быть долог, если не используется решето.

Как получить простые числа с помощью List Comprehension и all?

Функциональный стиль, но неэффективен для больших диапазонов.


def primes_upto_lc(n):
    return [x for x in range(2, n+1) if all(x % y != 0 for y in range(2, int(x**0.5)+1))]

print(primes_upto_lc(30))

задачи на операторы в python (задачи на операторы в python)

[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

задачи на последовательности python (задачи на последовательности в python)

Недостаток: для каждого числа заново вычисляется квадратный корень и выполняется цикл. При N=100000 алгоритм работает заметно дольше решета.

Как сгенерировать простые числа с помощью генератора?

Экономит память, возвращая числа по одному.


def prime_generator():
    yield 2
    primes = [2]
    candidate = 3
    while True:
        is_prime = True
        sqrt_candidate = candidate ** 0.5
        for p in primes:
            if p > sqrt_candidate:
                break
            if candidate % p == 0:
                is_prime = False
                break
        if is_prime:
            primes.append(candidate)
            yield candidate
        candidate += 2

# Первые 10 простых чисел
gen = prime_generator()
first_ten = [next(gen) for _ in range(10)]
print(first_ten)

задачи на списки python (задачи на списки в python)

[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

Особенности: генератор запоминает уже найденные простые числа, что ускоряет проверку. Однако он не может работать с бесконечным списком в памяти, поэтому используется для поточного получения.

- задачи на работу с файлами python (задачи на работу с файлами в python)
- задачи на функции в python (задачи на функции в python)
- задача на языке python код (задача по python с кодом)

Расширенные примеры и нестандартные реализации

Решето Эратосфена с битовыми операциями (экономия памяти)

Вместо списка булевых значений используется целое число или массив байтов. Каждый бит соответствует одному числу.

Пример

def sieve_bitarray(n):
    if n < 2:
        return []
    # Используем bytearray для хранения флагов (1 байт на число)
    is_prime = bytearray(b'\x01') * (n + 1)
    is_prime[0] = is_prime[1] = 0
    for i in range(2, int(n ** 0.5) + 1):
        if is_prime[i]:
            step = i
            start = i * i
            is_prime[start:n+1:step] = b'\x00' * ((n - start)//step + 1)
    return [i for i, flag in enumerate(is_prime) if flag]

print(sieve_bitarray(40))
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]

Пояснение: bytearray занимает 1 байт на элемент вместо 28 байт (для bool в Python). Операция присваивания среза b'\x00' * количество устанавливает все кратные в ноль за один вызов.

Решето Эратосфена с оптимизацией чётных чисел

Можно хранить информацию только для нечётных чисел, сократив память вдвое.

Пример

def sieve_odd_only(n):
    if n < 2:
        return []
    if n == 2:
        return [2]
    # is_prime[i] соответствует числу 2*i+3
    size = (n - 1) // 2
    is_prime = [True] * size
    for i in range(int(n ** 0.5) // 2):
        if is_prime[i]:
            # число = 2*i+3
            p = 2 * i + 3
            # начинаем с p*p = (2*i+3)^2 = 4*(i^2) + 12*i + 9 = 2*(2*i^2+6*i+4)+1
            step = p
            start = (p * p - 3) // 2  # индекс для p^2
            is_prime[start::step] = [False] * ((size - start + step - 1) // step)
    result = [2] + [2*i+3 for i, val in enumerate(is_prime) if val]
    return result

print(sieve_odd_only(50))
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]

Здесь сложнее индексация, но выигрыш в памяти и скорости за счёт меньшего количества операций.

Параллельное вычисление простых чисел (multiprocessing)

Для больших N (например, до 10^7) можно разделить диапазон на части и обработать параллельно.

Пример

import multiprocessing as mp

def sieve_segment(start, size):
    # Решето на отрезке [start, start+size)
    limit = int((start + size) ** 0.5) + 1
    # сначала найдём все простые до limit
    is_prime_base = [True] * (limit + 1)
    is_prime_base[0] = is_prime_base[1] = False
    for i in range(2, int(limit ** 0.5) + 1):
        if is_prime_base[i]:
            for j in range(i*i, limit+1, i):
                is_prime_base[j] = False
    bases = [i for i, v in enumerate(is_prime_base) if v]
    
    # Теперь работаем с сегментом
    segment = [True] * size
    first = start
    for p in bases:
        start_val = max(p*p, (first + p - 1)//p * p)
        for j in range(start_val, first+size, p):
            segment[j - first] = False
    return [first + i for i, v in enumerate(segment) if v]

def parallel_primes(n, num_workers=4):
    if n < 2:
        return []
    chunk_size = (n - 1) // num_workers + 1
    ranges = []
    for i in range(num_workers):
        start = 2 + i * chunk_size
        end = min(start + chunk_size, n + 1)
        if start < end:
            ranges.append((start, end - start))
    with mp.Pool(num_workers) as pool:
        results = pool.starmap(sieve_segment, ranges)
    # объединение результатов (могут быть дубли? нет)
    primes = []
    for r in results:
        primes.extend(r)
    return sorted(primes)

# для демонстрации возьмём небольшое n
print(parallel_primes(50, 2))
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]

Внимание: при малых N параллелизм не даёт выигрыша из-за накладных расходов. Для N=10^7 может ускорить в 2-3 раза на многоядерном процессоре.

Использование библиотеки numpy для векторизованного решета

Numpy позволяет выполнять операции над массивами быстрее, чем чистые циклы Python.

Пример

import numpy as np

def sieve_numpy(n):
    if n < 2:
        return []
    is_prime = np.ones(n+1, dtype=bool)
    is_prime[0:2] = False
    for i in range(2, int(n**0.5)+1):
        if is_prime[i]:
            is_prime[i*i:n+1:i] = False
    return np.where(is_prime)[0].tolist()

print(sieve_numpy(50))
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]

Скорость может быть выше решета на чистых списках для больших N за счёт низкоуровневой реализации. Однако импорт numpy оправдан только если библиотека уже используется в проекте.

Решето Эратосфена с Wheel factorization (оптимизация путём пропуска чисел, кратных 2,3,5)

Сначала предварительно отсеиваются числа с малыми простыми делителями, затем решето применяется к остаткам. Пример для wheel 2*3=6.

Пример

import math

def wheel_sieve(n):
    if n < 2:
        return []
    # малые простые
    small_primes = [2, 3, 5]
    limit = int(math.isqrt(n))
    # отсеиваем маленькие
    marks = [False] * (n+1)
    for p in small_primes:
        for multiple in range(p, n+1, p):
            marks[multiple] = True
    # теперь решето среди чисел вида 6k±1 (кроме кратных 2,3)
    for i in range(7, limit+1, 2):
        if not marks[i]:
            step = i if i%6==1 else i*2  # упрощение
            # на самом деле нужно вычёркивать кратные
            start = i*i
            if start > n:
                break
            for j in range(start, n+1, i):
                marks[j] = True
    # собираем непомеченные
    return [i for i in range(2, n+1) if not marks[i]]

print(wheel_sieve(100))
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]

Реализация выше не совершенна, но демонстрирует идею. Полное Wheel factorization (2*3*5 = 30) даёт дополнительное ускорение.

Задача по Python с кодом - comments

En
задача на языке python код (python)