The Algorithms - Python Python TheAlgorithms/Python

Maths Algorithms in TheAlgorithms/Python

Seven mathematical algorithms from prime sieves to the Chinese Remainder Theorem.

7 stops ~14 min Verified 2026-05-05
What you will learn
  • How the sieve eliminates composites by marking multiples only from each prime's square onward
  • How the 6k plus or minus 1 rule reduces prime checking to O(sqrt(n)) iterations
  • How GCD of n numbers is computed via shared prime factor intersection using a Counter
  • How iterative Fibonacci builds the sequence by always appending the sum of the last two elements
  • How Euclidean distance generalizes to arbitrary dimensions with one numpy expression
  • How binary exponentiation halves the number of multiplications by squaring the base on even exponents
  • How the Chinese Remainder Theorem reconstructs a unique solution from two modular residues via the extended Euclidean algorithm
Prerequisites
  • Basic number theory: primes, divisibility, modular arithmetic
  • Familiarity with Python type hints and doctest format
  • Comfort with recursion and bitwise operators
1 / 7

Sieve of Eratosthenes: mark composites from each prime's square

maths/sieve_of_eratosthenes.py:37

The sieve starts marking composites from each prime's square because all smaller multiples were already eliminated by smaller primes.

Trial division checks each candidate for divisibility, making it slow for large inputs. The Sieve of Eratosthenes avoids this by marking entire sets of composites at once. The sieve array starts as all True. For each number start from 2 to the square root of num, if its flag is still True it is prime, and the inner loop eliminates all multiples beginning at the square of start. Beginning at the square, rather than at twice start, is the key optimization: every smaller multiple was already eliminated by a prior prime factor. After the main loop ends at sqrt(num), a second pass appends any remaining unmarked numbers above that boundary, since those are guaranteed primes. The doctest confirms that prime_sieve(50) returns all 15 primes up to 50. For the full walkthrough see the dedicated tour.

Key takeaway

Beginning composite elimination at the square of each prime is the optimization that separates the sieve from naive trial division for large inputs.

    if num <= 0:
        msg = f"{num}: Invalid input, please enter a positive integer."
        raise ValueError(msg)

    sieve = [True] * (num + 1)
    prime = []
    start = 2
    end = int(math.sqrt(num))

    while start <= end:
        # If start is a prime
        if sieve[start] is True:
            prime.append(start)

            # Set multiples of start be False
            for i in range(start * start, num + 1, start):
                if sieve[i] is True:
                    sieve[i] = False

        start += 1

    for j in range(end + 1, num + 1):
        if sieve[j] is True:
            prime.append(j)

    return prime
2 / 7

Prime check: the 6k plus-or-minus-1 rule halves the loop iterations

maths/prime_check.py:42

Every prime greater than 3 is of the form 6k plus or minus 1, so a primality loop need only check divisors at those positions.

Trial division up to the square root is the standard primality test. This implementation sharpens it with the 6k observation. After handling 0, 1, 2, and 3 as special cases, the code eliminates all even numbers and all multiples of 3 in a single guard. These two checks remove two-thirds of all integers from consideration. Every integer falls into one of six residue classes modulo 6; residues 0, 2, 3, and 4 are always composite, leaving only residues 1 and 5 as candidates. The loop steps by 6 and checks both i (the 6k minus 1 position, starting at 5) and i + 2 (the 6k plus 1 position) per iteration. This cuts loop iterations to roughly one third of naive trial division. The doctest confirms that 563 and 2999 are prime while 27, 87, and 67483 are not.

Key takeaway

The 6k rule cuts trial division iterations by two-thirds by checking only residues 1 and 5 in each block of 6 consecutive integers.

    # precondition
    if not isinstance(number, int) or not number >= 0:
        raise ValueError("is_prime() only accepts positive integers")

    if 1 < number < 4:
        # 2 and 3 are primes
        return True
    elif number < 2 or number % 2 == 0 or number % 3 == 0:
        # Negatives, 0, 1, all even numbers, all multiples of 3 are not primes
        return False

    # All primes number are in format of 6k +/- 1
    for i in range(5, int(math.sqrt(number) + 1), 6):
        if number % i == 0 or number % (i + 2) == 0:
            return False
    return True
3 / 7

GCD of N numbers: Counter intersection finds shared prime factors

maths/gcd_of_n_numbers.py:63

GCD of n numbers is the product of all prime factors shared by every number, each raised to the minimum exponent across all inputs.

The standard Euclidean algorithm computes GCD for exactly two numbers. This implementation extends GCD to an arbitrary number of inputs by factoring each number into its prime components using a Counter object, then intersecting all Counters. Python's Counter & operator returns common elements with the minimum count, which corresponds exactly to the lowest exponent of each shared prime factor across all inputs. For example, GCD(18, 45) becomes Counter({2:1, 3:2}) intersected with Counter({3:2, 5:1}), yielding Counter({3:2}), whose product is 9. The variadic *numbers parameter allows any number of arguments, making this a genuine n-way GCD. The doctest shows that GCD of all integers from 1 to 10 is 1, as expected when 1 is in the set. Non-integer or negative inputs raise an exception through the get_factors helper.

Key takeaway

Counter intersection returns the minimum exponent for each shared prime factor, which is the definition of GCD generalized from two numbers to n numbers.

def get_greatest_common_divisor(*numbers: int) -> int:
    """
    get gcd of n numbers:
    >>> get_greatest_common_divisor(18, 45)
    9
    >>> get_greatest_common_divisor(23, 37)
    1
    >>> get_greatest_common_divisor(2520, 8350)
    10
    >>> get_greatest_common_divisor(-10, 20)
    Traceback (most recent call last):
        ...
    Exception: numbers must be integer and greater than zero
    >>> get_greatest_common_divisor(1.5, 2)
    Traceback (most recent call last):
        ...
    Exception: numbers must be integer and greater than zero
    >>> get_greatest_common_divisor(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    1
    >>> get_greatest_common_divisor("1", 2, 3, 4, 5, 6, 7, 8, 9, 10)
    Traceback (most recent call last):
        ...
    Exception: numbers must be integer and greater than zero
    """
4 / 7

Fibonacci: an iterative list-grow from seed values

maths/fibonacci.py:65

Iterative Fibonacci appends each new value as the sum of the last two, building the sequence in O(n) time and O(n) space.

The maths/fibonacci.py file contains seven different Fibonacci implementations for benchmarking: iterative with yield, iterative with a list, recursive, memoized recursive, and a matrix exponentiation method. This iterative version demonstrates the canonical approach. It starts the sequence at [0, 1] and appends fib[-1] + fib[-2] exactly n - 1 times. The two boundary conditions handle n = 0 (return a single-element list) and negative inputs (raise ValueError). The loop has no branching: each iteration is a single list append. The result is O(n) time and O(n) space. The matrix exponentiation variant in the same file computes the nth Fibonacci number in O(log n) time, which is the preferred approach for very large indices. The doctest file also covers the generator variant in fib_iterative_yield, which yields values lazily rather than accumulating a list.

Key takeaway

The iterative Fibonacci loop avoids branching: each step appends fib[-1] + fib[-2], making the list grow in O(n) with constant work per element.

def fib_iterative(n: int) -> list[int]:
    """
    Calculates the first n (0-indexed) Fibonacci numbers using iteration
    >>> fib_iterative(0)
    [0]
    >>> fib_iterative(1)
    [0, 1]
    >>> fib_iterative(5)
    [0, 1, 1, 2, 3, 5]
    >>> fib_iterative(10)
    [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
    >>> fib_iterative(-1)
    Traceback (most recent call last):
        ...
    ValueError: n is negative
    """
    if n < 0:
        raise ValueError("n is negative")
    if n == 0:
        return [0]
    fib = [0, 1]
    for _ in range(n - 1):
        fib.append(fib[-1] + fib[-2])
    return fib
5 / 7

Euclidean distance: one numpy expression generalizes to any dimension

maths/euclidean_distance.py:12

Euclidean distance in n dimensions is the square root of the sum of squared elementwise differences, implemented as a single numpy expression.

Euclidean distance in two dimensions is the hypotenuse of a right triangle: the square root of the sum of squared differences in each coordinate. The formula extends directly to any number of dimensions by summing squared differences across all dimensions before taking the square root. The numpy implementation collapses all of this into one expression. np.asarray converts lists and tuples to arrays without copying when the input is already an array. The subtraction, squaring, and np.sum all broadcast across dimensions automatically. The no-numpy variant euclidean_distance_no_np directly below uses a generator expression with zip to pair and square corresponding coordinates, then raises the sum to the power of one half. The file's benchmark function in __main__ shows that the numpy version is faster for large vectors despite the conversion overhead. The doctests confirm consistent results across 2D, 3D, and 4D inputs.

Key takeaway

The numpy Euclidean distance function handles any dimensionality because array subtraction and summation broadcast across all axes automatically.

def euclidean_distance(vector_1: Vector, vector_2: Vector) -> VectorOut:
    """
    Calculate the distance between the two endpoints of two vectors.
    A vector is defined as a list, tuple, or numpy 1D array.
    >>> float(euclidean_distance((0, 0), (2, 2)))
    2.8284271247461903
    >>> float(euclidean_distance(np.array([0, 0, 0]), np.array([2, 2, 2])))
    3.4641016151377544
    >>> float(euclidean_distance(np.array([1, 2, 3, 4]), np.array([5, 6, 7, 8])))
    8.0
    >>> float(euclidean_distance([1, 2, 3, 4], [5, 6, 7, 8]))
    8.0
    """
    return np.sqrt(np.sum((np.asarray(vector_1) - np.asarray(vector_2)) ** 2))
6 / 7

Binary exponentiation: squaring the base halves the remaining exponent

maths/binary_exponentiation.py:53

Binary exponentiation computes the result in O(log b) multiplications by squaring the base and right-shifting the exponent each iteration.

Computing 3 to the power of 13 by repeated multiplication requires 12 multiplications. Binary exponentiation does it in 5. The algorithm reads the exponent in binary from least-significant to most-significant bit. The bitwise AND on line 81 checks whether the current bit is 1: if it is, the accumulated result is multiplied by the current power of the base. The base is squared unconditionally each iteration, and the exponent is right-shifted by one, which is equivalent to dividing by 2. After processing all bits, res holds the answer. For 13 in binary (1101), the algorithm multiplies res by the base at positions corresponding to the three set bits, squaring the base between each. The technique extends to modular exponentiation: binary_exp_mod_iterative in the same file applies the modulus at each step to prevent overflow when computing large powers used in RSA and Diffie-Hellman key exchange.

Key takeaway

Binary exponentiation reduces O(b) multiplications to O(log b) by doubling the base each step and only accumulating into the result when the current exponent bit is 1.

def binary_exp_iterative(base: float, exponent: int) -> float:
    """
    Computes a^b iteratively, where a is the base and b is the exponent

    >>> binary_exp_iterative(3, 5)
    243
    >>> binary_exp_iterative(11, 13)
    34522712143931
    >>> binary_exp_iterative(-1, 3)
    -1
    >>> binary_exp_iterative(0, 5)
    0
    >>> binary_exp_iterative(3, 1)
    3
    >>> binary_exp_iterative(3, 0)
    1
    >>> binary_exp_iterative(1.5, 4)
    5.0625
    >>> binary_exp_iterative(3, -1)
    Traceback (most recent call last):
        ...
    ValueError: Exponent must be a non-negative integer
    """
    if exponent < 0:
        raise ValueError("Exponent must be a non-negative integer")

    res: int | float = 1
7 / 7

Chinese Remainder Theorem: extended Euclid finds the modular inverse

maths/chinese_remainder_theorem.py:19

The Chinese Remainder Theorem reconstructs a unique integer from two residues by using extended Euclid to find modular inverses of each modulus.

The Chinese Remainder Theorem guarantees that a system of congruences with coprime moduli has a unique solution modulo the product of the moduli. Finding that solution requires modular inverses, which the extended Euclidean algorithm provides. The standard Euclidean algorithm returns only the GCD; the extended version also returns Bezout coefficients. For coprime inputs the GCD is 1, which means one coefficient is the modular inverse of the first input modulo the second. The recursive structure follows standard Euclidean reduction and reconstructs coefficients on the way back up from the base case. The chinese_remainder_theorem function uses these coefficients to build the combined solution and takes it modulo the product of both moduli to land in the canonical range. The doctest confirms that 31 is the smallest positive integer leaving remainder 1 when divided by 5 and remainder 3 when divided by 7.

Key takeaway

Extended Euclid is the engine: it returns coefficients that are modular inverses when the inputs are coprime, making the CRT reconstruction a single linear combination.

def extended_euclid(a: int, b: int) -> tuple[int, int]:
    """
    >>> extended_euclid(10, 6)
    (-1, 2)

    >>> extended_euclid(7, 5)
    (-2, 3)

    """
    if b == 0:
        return (1, 0)
    (x, y) = extended_euclid(b, a % b)
    k = a // b
    return (y, x - k * y)


# Uses ExtendedEuclid to find inverses
def chinese_remainder_theorem(n1: int, r1: int, n2: int, r2: int) -> int:
    """
    >>> chinese_remainder_theorem(5,1,7,3)
    31

    Explanation : 31 is the smallest number such that
                (i)  When we divide it by 5, we get remainder 1
                (ii) When we divide it by 7, we get remainder 3

    >>> chinese_remainder_theorem(6,1,4,3)
    14

    """
    (x, y) = extended_euclid(n1, n2)
    m = n1 * n2
    n = r2 * x * n1 + r1 * y * n2
    return (n % m + m) % m
Your codebase next

Create code tours for your project

Intraview lets AI create interactive walkthroughs of any codebase. Install the free VS Code extension and generate your first tour in minutes.

Install Intraview Free