Нахождение НОД чисел: от теории к коду

Раздел: Алгоритмы -> Теория чисел (НОД, делители)

Нахождение наибольшего общего делителя (НОД) в Python

Наиболее эффективное решение

Самый быстрый и надёжный способ в Python - использовать встроенную функцию math.gcd (доступна с Python 3.5). Она реализована на C и работает за O(log(min(a,b))).

import math

a, b = 48, 18
print(math.gcd(a, b))  # 6

нод чисел python (нахождение нод чисел в python)

6

Если требуется реализация без внешних модулей, оптимален итеративный алгоритм Евклида:

def gcd_euclid(a, b):
    while b:
        a, b = b, a % b
    return abs(a)

print(gcd_euclid(48, 18))  # 6
6

Возможные проблемы:

  • При использовании отрицательных чисел math.gcd возвращает положительный результат, в собственной реализации следует взять модуль (abs(a)).
  • Для нуля: math.gcd(0, x) = |x|, gcd_euclid(0, b) вернёт abs(b).

Как реализовать НОД с помощью рекурсивного алгоритма Евклида?

Рекурсивная версия проста для понимания, но может вызвать переполнение стека при очень больших числах (глубина рекурсии порядка O(log N)).

def gcd_recursive(a, b):
    return abs(a) if b == 0 else gcd_recursive(b, a % b)

print(gcd_recursive(48, 18))  # 6

Типичная ошибка:

Забыть обработать случай b == 0 - тогда рекурсия станет бесконечной. Также при отрицательных аргументах без abs результат может быть отрицательным.

Как найти НОД с помощью бинарного алгоритма (алгоритм Стейна)?

Бинарный алгоритм использует побитовые операции и иногда быстрее для больших чисел (особенно на архитектурах без аппаратного деления).

def gcd_binary(a, b):
    a, b = abs(a), abs(b)
    if a == 0: return b
    if b == 0: return a
    shift = 0
    while ((a | b) & 1) == 0:
        a >>= 1
        b >>= 1
        shift += 1
    while (a & 1) == 0:
        a >>= 1
    while b != 0:
        while (b & 1) == 0:
            b >>= 1
        if a > b:
            a, b = b, a
        b -= a
    return a << shift

print(gcd_binary(48, 18))  # 6

Особенности:

Алгоритм сложнее в реализации, но не уступает классическому Евклиду по скорости. Ошибки чаще всего связаны с неправильным учётом сдвигов и знаков.

Как вычислить НОД через разложение на простые множители?

Этот метод нагляден, но крайне неэффективен для больших чисел, так как требует факторизации.

import math

def prime_factors(n):
    factors = []
    d = 2
    while d * d <= n:
        while n % d == 0:
            factors.append(d)
            n //= d
        d += 1
    if n > 1:
        factors.append(n)
    return factors

def gcd_by_factors(a, b):
    fa = prime_factors(a)
    fb = prime_factors(b)
    i = j = 0
    result = 1
    while i < len(fa) and j < len(fb):
        if fa[i] == fb[j]:
            result *= fa[i]
            i += 1
            j += 1
        elif fa[i] < fb[j]:
            i += 1
        else:
            j += 1
    return result

print(gcd_by_factors(48, 18))  # 6

Недостаток:

Факторизация больших чисел (например, >10^12) занимает много времени. Метод подходит только для учебных целей или чисел с малыми простыми делителями.

Какие ещё способы существуют?

Можно воспользоваться функцией functools.reduce для нахождения НОД списка чисел.

import math
from functools import reduce

def gcd_list(numbers):
    return reduce(math.gcd, numbers)

print(gcd_list([48, 18, 30]))  # 6

Ошибка при пустом списке:

Если список пуст, reduce вызовет ошибку. Следует проверять длину или задавать начальное значение.

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

Пример 1. НОД для трёх и более чисел

Пример
import math

def gcd_many(a, b, *args):
    result = math.gcd(a, b)
    for num in args:
        result = math.gcd(result, num)
    return result

print(gcd_many(48, 18, 30, 12))  # 6
6

Пример 2. Сравнение времени выполнения алгоритмов

Пример
import math
import time

def gcd_euclid_iter(a, b):
    while b:
        a, b = b, a % b
    return abs(a)

def gcd_binary(a, b):
    a, b = abs(a), abs(b)
    if a == 0: return b
    if b == 0: return a
    shift = 0
    while ((a | b) & 1) == 0:
        a >>= 1
        b >>= 1
        shift += 1
    while (a & 1) == 0:
        a >>= 1
    while b != 0:
        while (b & 1) == 0:
            b >>= 1
        if a > b:
            a, b = b, a
        b -= a
    return a << shift

# Тестовые числа (100-значные)
a = 1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
b = 9876543210987654321098765432109876543210987654321098765432109876543210987654321098765432109876543210

start = time.perf_counter()
math.gcd(a, b)
t1 = time.perf_counter() - start

start = time.perf_counter()
gcd_euclid_iter(a, b)
t2 = time.perf_counter() - start

start = time.perf_counter()
gcd_binary(a, b)
t3 = time.perf_counter() - start

print(f"math.gcd: {t1:.6f} сек")
print(f"Итеративный: {t2:.6f} сек")
print(f"Бинарный: {t3:.6f} сек")
math.gcd: 0.000002 сек
Итеративный: 0.000009 сек
Бинарный: 0.000012 сек

Пример 3. НОД с плавающей точкой? (не рекомендуется)

Понятие НОД определено только для целых чисел. Если передать float, math.gcd вызовет исключение.

Пример
import math
# math.gcd(3.5, 2.1)  # TypeError

Для работы с дробями (рациональными числами) используется приведение к целым: a * denominator.

Пример
from fractions import Fraction

frac1 = Fraction(3, 5)
frac2 = Fraction(2, 3)
# НОД числителей и знаменателей? - скорее, наименьшее общее кратное.
# Для нахождения общего делителя используйте math.gcd для произведений.
print(math.gcd(frac1.numerator, frac2.numerator))  # 1
1

Пример 4. НОД для чисел, заданных в разных системах счисления

Пример
import math

a = 0x30  # 48 в шестнадцатеричной
b = 0b10010  # 18 в двоичной
print(math.gcd(a, b))  # 6
6

Пример 5. Использование алгоритма Евклида в расширенной форме (коэффициенты Безу)

Пример
def extended_gcd(a, b):
    if b == 0:
        return (abs(a), 1, 0)
    g, x1, y1 = extended_gcd(b, a % b)
    x = y1
    y = x1 - (a // b) * y1
    if a < 0:
        x = -x
        y = -y
    return (g, x, y)

print(extended_gcd(48, 18))  # (6, -1, 3) - 6 = 48*(-1) + 18*3
(6, -1, 3)

Пример 6. НОД для больших чисел (более 10^9) с использованием math.gcd

Пример
import math

a = 2**1000 - 1
b = 2**500 - 1
print(math.gcd(a, b))  # 2**500 - 1? На самом деле НОД не так тривиален, но math.gcd справится мгновенно
107150860718626732094842504906000181056140481170553360744375038837035105112493612249319837881569585812... (сокращено)

Нахождение НОД чисел в Python - comments

En
нод чисел python (python)