18.7 Primality Testing

Prime numbers occupy a central position in number theory.

18.7 Primality Testing

Prime numbers occupy a central position in number theory. They are the building blocks of the integers, appear throughout algebraic structures, and form the foundation of modern public-key cryptography. Many algorithms depend on quickly determining whether a given integer is prime.

At first glance, primality testing seems simple. A number is prime if it has no divisors other than 1 and itself. However, when the input contains hundreds or thousands of digits, naive approaches become impractical. Efficient primality testing therefore focuses on eliminating unnecessary work and exploiting number-theoretic structure.

This section develops increasingly powerful primality tests, beginning with straightforward divisor checking and ending with probabilistic algorithms suitable for very large integers.

Problem

Given an integer:

n

determine whether:

n

is prime.

Examples:

2     → prime
3     → prime
17    → prime
21    → composite
91    → composite

The challenge is to perform this test efficiently for large values of n.

Definition of a Prime Number

A prime number is a positive integer greater than one whose only positive divisors are:

1
n

Examples:

2
3
5
7
11
13
17
19

Composite numbers possess additional divisors.

Examples:

4 = 2 × 2
6 = 2 × 3
8 = 2 × 4
9 = 3 × 3

The distinction appears simple, but identifying it efficiently is the real challenge.

Naive Primality Test

The most direct approach checks every possible divisor.

def is_prime_naive(n):
    if n < 2:
        return False

    for d in range(2, n):
        if n % d == 0:
            return False

    return True

Example:

print(is_prime_naive(17))
print(is_prime_naive(21))

Output:

True
False

While correct, this approach requires:

O(n)

divisions.

For large inputs, it is far too slow.

Divisor Pair Observation

Suppose:

n = a × b

If both factors exceed:

√n

then:

a × b > n

which is impossible.

Therefore, every composite number must have at least one factor less than or equal to:

√n

This leads immediately to a much better algorithm.

Square Root Test

Instead of checking every possible divisor, check only divisors up to the square root.

from math import isqrt

def is_prime(n):
    if n < 2:
        return False

    for d in range(2, isqrt(n) + 1):
        if n % d == 0:
            return False

    return True

Example:

print(is_prime(97))

Output:

True

Complexity:

O(√n)

This improvement is dramatic.

For:

n = 1,000,000,007

the algorithm checks about:

31,623

divisors instead of a billion.

Eliminating Even Divisors

Every even number greater than two is composite.

Therefore:

from math import isqrt

def is_prime(n):
    if n < 2:
        return False

    if n == 2:
        return True

    if n % 2 == 0:
        return False

    limit = isqrt(n)

    for d in range(3, limit + 1, 2):
        if n % d == 0:
            return False

    return True

Only odd candidates are tested.

This roughly halves the work.

The 6k ± 1 Observation

Every integer belongs to one of the forms:

6k
6k+1
6k+2
6k+3
6k+4
6k+5

Numbers of the forms:

6k
6k+2
6k+4

are even.

Numbers of the form:

6k+3

are divisible by 3.

Thus every prime greater than 3 must be:

6k−1

or:

6k+1

This further reduces the number of candidates.

from math import isqrt

def is_prime(n):
    if n <= 1:
        return False

    if n <= 3:
        return True

    if n % 2 == 0 or n % 3 == 0:
        return False

    i = 5

    while i <= isqrt(n):
        if n % i == 0 or n % (i + 2) == 0:
            return False

        i += 6

    return True

This version is often sufficient for ordinary applications.

Trial Division Example

Determine whether:

221

is prime.

Compute:

√221 ≈ 14.8

Check primes:

2
3
5
7
11
13

When testing:

13

we find:

221 = 13 × 17

Therefore:

221

is composite.

The search terminates immediately.

Fermat's Little Theorem

The next major improvement comes from number theory.

If:

p

is prime and:

a

is not divisible by p, then:

a^(p−1) ≡ 1 (mod p)

This is a consequence of entity["scientific_concept","Fermat's Little Theorem","number theory theorem"].

For example:

2^6 mod 7 = 1

and:

3^10 mod 11 = 1

The theorem suggests a primality test.

Given:

n

choose:

a

and compute:

a^(n−1) mod n

If the result is not:

1

then:

n

is definitely composite.

Fermat Primality Test

def fermat_test(n, a):
    return pow(a, n - 1, n) == 1

Example:

print(fermat_test(17, 2))

Output:

True

However, Fermat's test is only a one-way implication.

Failure proves compositeness.

Success does not guarantee primality.

Carmichael Numbers

Some composite numbers satisfy Fermat's congruence for many bases.

Example:

561

Factorization:

561 = 3 × 11 × 17

Despite being composite:

a^560 ≡ 1 (mod 561)

for many values of a.

Numbers with this behavior are called Carmichael numbers.

They fool the Fermat test.

Consequently, Fermat testing alone is insufficient for serious applications.

Miller-Rabin Overview

The most widely used practical primality test is the Miller-Rabin test.

It improves upon Fermat testing by examining the structure of:

n−1

and detecting many classes of composites that Fermat's test misses.

The algorithm:

  1. Decomposes:
n−1 = 2^s × d

where:

d

is odd.

  1. Performs repeated modular exponentiation.

  2. Searches for witnesses that prove compositeness.

For a randomly chosen base, the probability of a composite number passing is extremely small.

A few rounds provide cryptographic-grade confidence.

Deterministic Miller-Rabin

For fixed-width integers, specific witness sets are known.

For 64-bit integers, testing a small collection of bases provides deterministic correctness.

Typical witness set:

2
3
5
7
11
13
17

Combined with Miller-Rabin, this yields exact primality testing for ordinary machine integers.

Complexity Analysis

Method Complexity
Naive divisor test O(n)
Square-root test O(√n)
6k ± 1 test O(√n)
Fermat test O(log n) modular powers
Miller-Rabin O(k log³ n)
Deterministic Miller-Rabin (64-bit) Practical O(log³ n)

Here:

k

is the number of witness rounds.

Applications

Cryptography

RSA key generation repeatedly tests large candidate primes.

Hash Tables

Prime capacities often improve distribution properties.

Number Theory

Prime testing is a prerequisite for factorization algorithms.

Random Prime Generation

Modern cryptographic systems generate primes dynamically.

Finite Fields

Many algebraic structures rely on prime moduli.

Combinatorics

Prime moduli simplify modular arithmetic and inverse computation.

Common Mistakes

Testing Beyond the Square Root

Checking divisors larger than:

√n

wastes computation.

Forgetting Small Cases

The values:

0
1
2
3

require explicit handling.

Trusting Fermat Alone

Fermat testing is useful for filtering candidates but cannot prove primality.

Ignoring Overflow

Modular exponentiation must avoid intermediate overflow in fixed-width integer environments.

Using Floating-Point Square Roots

Floating-point rounding can occasionally introduce subtle errors.

Prefer:

isqrt()

when available.

Testing

def test_primes():
    assert is_prime(2)
    assert is_prime(3)
    assert is_prime(5)
    assert is_prime(97)

    assert not is_prime(1)
    assert not is_prime(4)
    assert not is_prime(21)
    assert not is_prime(221)

Boundary testing is particularly important because primality code often fails on small inputs.

Takeaway

Primality testing determines whether an integer possesses nontrivial divisors. Simple divisor checking leads naturally to the square-root bound, which reduces complexity from O(n) to O(√n). Number-theoretic results such as Fermat's Little Theorem enable much faster probabilistic tests, while Miller-Rabin provides practical primality testing for very large integers. These algorithms form the foundation of cryptography, finite-field arithmetic, random prime generation, and many advanced number-theoretic computations.