Data Structures & Algorithms (DSA) Module 18 – Maths for DSA
Primes: trial division, sieves and factorisation
Test whether a number is prime by trial division, list primes with the sieve of Eratosthenes, and factorise many numbers with a smallest-prime-factor table.
What you will learn
- Test primality by trial division up to the integer square root
- Generate every prime up to n with the sieve of Eratosthenes
- Decompose many numbers into primes with a smallest-prime-factor sieve
- Choose between trial division, a sieve and a factor table for a workload
Before you start
What you’ll learn · Before you start
What you will learn
- Test primality by trial division up to the integer square root
- Generate every prime up to n with the sieve of Eratosthenes
- Decompose many numbers into primes with a smallest-prime-factor sieve
- Choose between trial division, a sieve and a factor table for a workload
Before you start
On this page
A prime is a whole number greater than 1 whose only divisors are 1 and itself: 2, 3, 5, 7, 11 and so on. Three tools cover almost every prime question in DSA. To test one number n, trial division tries divisors up to √n, which costs O(√n), because a divisor larger than √n always comes paired with one smaller than √n. To list every prime up to n, the sieve of Eratosthenes crosses out the multiples of each prime, starting at its square, in O(n log log n) time and O(n) memory. To factorise many numbers up to n, a smallest-prime-factor sieve stores each number’s smallest prime factor, and any number then splits into primes with one lookup per factor, O(log n) each.
Trial division up to the square root
If n = d × e with d ≤ e, then d × d ≤ n, so d ≤ √n: every composite number has a divisor no larger than its square root. Trial division can therefore stop at √n, and after 2 it needs to try only odd divisors, about √n / 2 divisions for a prime:
import math
def is_prime(n):
"""Trial division by 2 and by odd numbers up to the integer square root. Returns (answer, divisions made)."""
if n < 2:
return False, 0
if n % 2 == 0:
return n == 2, 1
divisions = 1
for d in range(3, math.isqrt(n) + 1, 2):
divisions += 1
if n % d == 0:
return False, divisions
return True, divisions
for n in [97, 91, 1_000_003, 999_999_937, 10**12 + 39, 10**12 + 41]:
prime, divisions = is_prime(n)
print(f"{n:>17,} {'prime' if prime else 'composite':<9} {divisions:>7,} divisions")
print()
print("bound for 10^16 - 1: int(math.sqrt(n)) =", int(math.sqrt(10**16 - 1)), " math.isqrt(n) =", math.isqrt(10**16 - 1))
print("bound for (2^53 + 1)^2: int(math.sqrt(n)) =", int(math.sqrt((2**53 + 1) ** 2)), " math.isqrt(n) =", math.isqrt((2**53 + 1) ** 2)) Output
97 prime 5 divisions
91 composite 4 divisions
1,000,003 prime 500 divisions
999,999,937 prime 15,811 divisions
1,000,000,000,039 prime 500,000 divisions
1,000,000,000,041 composite 2 divisions
bound for 10^16 - 1: int(math.sqrt(n)) = 100000000 math.isqrt(n) = 99999999
bound for (2^53 + 1)^2: int(math.sqrt(n)) = 9007199254740992 math.isqrt(n) = 9007199254740993
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 trial_division.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
A prime near 10¹² takes 500,000 divisions, while most composites stop early: 10¹² + 41 is divisible by 3, so the
second division settles it. The bound itself has to be exact. int(math.sqrt(n)) turns n into a float first, which
gave 100,000,000 for 10¹⁶ − 1, one too large, and 2⁵³ for (2⁵³ + 1)², one too small: for the square of a prime that
large, the loop would stop one short of the only divisor it has to try. math.isqrt returns the exact integer square root of any whole number.
The sieve of Eratosthenes
To list the primes up to n, mark every number from 2 to n as possibly prime. The smallest number still marked, p, is prime, since no smaller prime divides it. Cross out its multiples from p² on: each smaller multiple k × p with k < p has a prime factor smaller than p and is already crossed out. Move to the next marked number, and stop once p² is larger than n; every number still marked is prime.
The sieve of Eratosthenes on 1 to 60, after crossing out the multiples of 2, 3, 5 and 7
Text description of the diagram
The numbers 1 to 60 are laid out in six rows of ten. The sieve needs only the primes up to 7 here, because 7 is the largest prime whose square, 49, is at most 60.
- The 17 primes, 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53 and 59, have a white box with a bold border. Nothing crossed them out.
- Every other number from 4 to 60 is shaded and carries a small tag in its top right corner: the prime that crossed it out first, which is its smallest prime factor. Even numbers carry 2; 9, 15, 21, 27, 33, 39, 45, 51 and 57 carry 3; 25, 35 and 55 carry 5; 49 carries 7.
- The first number that each prime crosses out has a dashed border: 4 for 2, 9 for 3, 25 for 5 and 49 for 7. Every smaller multiple of the prime, such as 15 for 5, was already crossed out by a smaller prime, which is why the crossing starts at the prime's square.
- The number 1 is grey: it is neither prime nor composite.
This program sieves up to 10⁷ with a bytearray, one byte a number, and counts how many times a number is crossed out:
import math
def sieve(n):
"""is_prime[i] is 1 exactly when i is prime, for 0 <= i <= n (the sieve of Eratosthenes)."""
is_prime = bytearray([1]) * (n + 1)
is_prime[0:2] = b"\x00\x00"
for p in range(2, math.isqrt(n) + 1):
if is_prime[p]:
# p is prime: cross out p*p, p*p + p, ..., up to n
is_prime[p * p :: p] = bytes(len(range(p * p, n + 1, p)))
return is_prime
is_prime = sieve(100)
print("primes up to 100:", [i for i in range(101) if is_prime[i]])
is_prime = sieve(10**7)
print(f"primes up to 10^7: {sum(is_prime):,}")
print()
print(" n crossings crossings / n ln ln n")
for n in (10**4, 10**5, 10**6, 10**7):
crossings = sum((n - p * p) // p + 1 for p in range(2, math.isqrt(n) + 1) if is_prime[p])
print(f"{n:>10,} {crossings:>10,} {crossings / n:>14.3f} {math.log(math.log(n)):>8.3f}") Output
primes up to 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]
primes up to 10^7: 664,579
n crossings crossings / n ln ln n
10,000 16,981 1.698 2.220
100,000 193,078 1.931 2.443
1,000,000 2,122,048 2.122 2.626
10,000,000 22,850,051 2.285 2.780
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sieve.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
It finds the 664,579 primes up to 10⁷. While n grows a thousandfold, the crossings per number grow only from 1.70 to 2.29, in step with ln ln n, which is where the O(n log log n) comes from. The memory is one byte a number, 10 MB for 10⁷.
Smallest prime factors: factorising many numbers
A sieve can record more than yes or no. Keep for every number x its smallest prime factor, spf[x], set by the first prime that crosses x out. Then x splits into primes by repeated division: spf[x] is one factor, and x / spf[x] holds the rest. A number up to n has at most log₂ n prime factors, so each factorisation takes O(log n) lookups:
import math
def smallest_prime_factors(n):
"""spf[i] is the smallest prime that divides i, for 2 <= i <= n."""
spf = list(range(n + 1))
for p in range(2, math.isqrt(n) + 1):
if spf[p] == p: # nothing smaller divides p, so p is prime
for m in range(p * p, n + 1, p):
if spf[m] == m:
spf[m] = p
return spf
def factorise(x, spf):
"""The prime factors of x, smallest first, with one table lookup per factor."""
factors = []
while x > 1:
factors.append(spf[x])
x //= spf[x]
return factors
def trial_factorise(x):
"""The same factors by trial division, and the number of divisions it made."""
factors, divisions, d = [], 0, 2
while d * d <= x:
divisions += 1
if x % d == 0:
factors.append(d)
x //= d
else:
d += 1
if x > 1:
factors.append(x)
return factors, divisions
N = 1_000_000
spf = smallest_prime_factors(N)
for x in [360, 999_983, 524_288, 735_134, 997_002]:
print(f"{x:>7,} = {' × '.join(map(str, factorise(x, spf)))}")
lookups = divisions = 0
for x in range(2, 100_001):
by_table = factorise(x, spf)
by_trial, made = trial_factorise(x)
assert by_table == by_trial
lookups += len(by_table)
divisions += made
print(f"factorising every number from 2 to 100,000: {lookups:,} table lookups, {divisions:,} trial divisions") Output
360 = 2 × 2 × 2 × 3 × 3 × 5 999,983 = 999983 524,288 = 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 735,134 = 2 × 31 × 71 × 167 997,002 = 2 × 3 × 3 × 3 × 37 × 499 factorising every number from 2 to 100,000: 343,614 table lookups, 6,956,686 trial divisions
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 spf.py
The most lookups below 10⁶ go to 524,288 = 2¹⁹, nineteen of them. Factorising every number from 2 to 100,000 took 343,614 lookups with the table and 6,956,686 trial divisions, about twenty times as many.
Choosing a method
| Criterion | Trial division | Sieve of Eratosthenes | Smallest-prime-factor sieve |
|---|---|---|---|
| Answers | Is this one number prime? | Which numbers up to n are prime? | What are the prime factors of x ≤ n? |
| Work | O(√n) per number | O(n log log n) once, then O(1) per question | About n log log n once, then O(log n) per number |
| Memory | None | One byte (or one bit) per number | One integer per number |
| When to choose | A few numbers | Many primality questions up to n | Many factorisations up to n |
Trial division needs no memory and suits a handful of questions. The sieve answers a primality question in one lookup after one pass, and the factor table answers factorisation questions the same way. Three further tools reach where these stop. A segmented sieve finds the primes in a far window, such as 10¹⁴ to 10¹⁴ + 10⁶, with memory for the window only. The Miller–Rabin test decides whether one large number is prime with a few modular powers. A linear sieve crosses out every composite exactly once.
Exercise and self-check
Count the primes in up to 100,000 ranges of numbers up to 10⁷, sieving only once.
Exercise · Medium · Python, JavaScript
Count the primes in many ranges
Write count_primes(queries) (JavaScript: countPrimes). queries is a list of pairs (l, r) (JavaScript: [l, r]) with 1 <= l <= r <= 10⁷, and the function returns one number per pair, in the same order: how many primes p satisfy l <= p <= r.
There can be 100,000 pairs, so testing every number of every range is far too slow. Sieve once, up to the largest r, and answer each pair from counts you prepare beforehand.
The sample tests check single numbers, calls whose largest range ends at 1 or 2, ranges that start at 1 or 2, ranges with no prime, a few hundred random ranges against trial division, the count up to 10⁶, and 10,000 ranges below 10⁶.
Python · Starter code · ranges.py
def count_primes(queries):
"""For each (l, r), the number of primes p with l <= p <= r."""
raise NotImplementedError("write count_primes") The sample tests · test_ranges.py
import random
from ranges import count_primes
def slow_is_prime(n):
"""Trial division: right, and too slow for large ranges."""
if n < 2:
return False
d = 2
while d * d <= n:
if n % d == 0:
return False
d += 1
return True
def test_single_numbers():
"""one-number ranges: 1 is not prime, 2 and 97 are, 91 is not"""
assert count_primes([(1, 1), (2, 2), (97, 97), (91, 91)]) == [0, 1, 1, 0]
def test_tiny_inputs():
"""a call whose largest r is 1 or 2 sieves only that far"""
assert count_primes([(1, 1)]) == [0]
assert count_primes([(2, 2)]) == [1]
assert count_primes([(1, 2), (1, 1)]) == [1, 0]
def test_small_ranges():
"""ranges that start at 1 or 2, and a range without primes"""
assert count_primes([(1, 10), (2, 10), (1, 100), (90, 100), (114, 126)]) == [4, 4, 25, 1, 0]
def test_order_is_kept():
"""the answers come back in the order of the queries"""
assert count_primes([(1, 100), (1, 10), (11, 20)]) == [25, 4, 4]
def test_random_against_trial_division():
"""300 random ranges below 3,000, compared with trial division"""
rng = random.Random(9)
queries = []
for _ in range(300):
l = rng.randint(1, 3000)
queries.append((l, rng.randint(l, 3000)))
expected = [sum(1 for x in range(l, r + 1) if slow_is_prime(x)) for l, r in queries]
assert count_primes(queries) == expected
def test_up_to_a_million():
"""78,498 primes up to 10^6, and the ones in its last thousand numbers"""
assert count_primes([(1, 10**6), (999_001, 10**6)]) == [78_498, 65]
def test_ten_thousand_ranges():
"""10,000 ranges below 10^6: their answers add up to a known total"""
rng = random.Random(4)
queries = []
for _ in range(10_000):
l = rng.randint(1, 10**6)
queries.append((l, min(10**6, l + rng.randint(0, 5_000))))
answers = count_primes(queries)
assert len(answers) == 10_000
assert sum(answers) == 1_977_492 JavaScript · Starter code · ranges.mjs
/** For each [l, r], the number of primes p with l <= p <= r. */
export function countPrimes(queries) {
throw new Error('write countPrimes');
} The sample tests · ranges.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { countPrimes } from './ranges.mjs';
/** Trial division: right, and too slow for large ranges. */
function slowIsPrime(n) {
if (n < 2) return false;
for (let d = 2; d * d <= n; d++) if (n % d === 0) return false;
return true;
}
/** A small seeded generator (Park and Miller), so every run builds the same ranges. */
function generator(seed) {
const random = () => (seed = (seed * 48271) % 2147483647) / 2147483647;
return (k) => Math.floor(random() * k);
}
test('one-number ranges: 1 is not prime, 2 and 97 are, 91 is not', () => {
assert.deepEqual(countPrimes([[1, 1], [2, 2], [97, 97], [91, 91]]), [0, 1, 1, 0]);
});
test('a call whose largest r is 1 or 2 sieves only that far', () => {
assert.deepEqual(countPrimes([[1, 1]]), [0]);
assert.deepEqual(countPrimes([[2, 2]]), [1]);
assert.deepEqual(countPrimes([[1, 2], [1, 1]]), [1, 0]);
});
test('ranges that start at 1 or 2, and a range without primes', () => {
assert.deepEqual(countPrimes([[1, 10], [2, 10], [1, 100], [90, 100], [114, 126]]), [4, 4, 25, 1, 0]);
});
test('the answers come back in the order of the queries', () => {
assert.deepEqual(countPrimes([[1, 100], [1, 10], [11, 20]]), [25, 4, 4]);
});
test('300 random ranges below 3,000, compared with trial division', () => {
const pick = generator(9);
const queries = [];
for (let i = 0; i < 300; i++) {
const l = 1 + pick(3000);
queries.push([l, l + pick(3001 - l)]);
}
const expected = queries.map(([l, r]) => {
let c = 0;
for (let x = l; x <= r; x++) if (slowIsPrime(x)) c++;
return c;
});
assert.deepEqual(countPrimes(queries), expected);
});
test("78,498 primes up to 10^6, and the ones in its last thousand numbers", () => {
assert.deepEqual(countPrimes([[1, 1000000], [999001, 1000000]]), [78498, 65]);
});
test('10,000 ranges below 10^6: their answers add up to a known total', () => {
const pick = generator(4);
const queries = [];
for (let i = 0; i < 10000; i++) {
const l = 1 + pick(1000000);
queries.push([l, Math.min(1000000, l + pick(5001))]);
}
const answers = countPrimes(queries);
assert.equal(answers.length, 10000);
assert.equal(answers.reduce((s, x) => s + x, 0), 1963500);
}); A hint
After the sieve, build count[i], the number of primes up to i: count[i] = count[i - 1] + is_prime[i]. The answer for (l, r) is then count[r] - count[l - 1], one subtraction per pair.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Why the sieve is right and what it costs, proved and counted Pro: opens with a Pro, Premium or Ultimate pass
The sieve needs two arguments: that every number left marked is prime, and that its work grows like n log log n. This part proves both, compares the counted crossings with the prediction up to 10⁹, measures four ways to store the marks and counts a sieve that crosses out every composite once.
In this part:
- Every number left marked is prime: the invariant, proved
- n log log n from the sum of 1/p, checked against the count
- One byte, one bit or half a bit per number, measured
- A linear sieve: every composite crossed out once
3 runnable examples (Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Primes in a far window, and numbers too large to sieve Pro: opens with a Pro, Premium or Ultimate pass
A sieve from 1 cannot reach 10¹⁴, and no sieve can test one 64-bit number. This part sieves a window near 10¹⁴ with the primes up to its square root, shows which composites fool Fermat’s test, and makes Miller–Rabin exact for every 64-bit number with twelve fixed bases.
In this part:
- A segmented sieve for 10¹⁴ to 10¹⁴ + 10⁶
- Fermat’s test, and the numbers that fool it
- Miller–Rabin: a stronger test, exact below 2⁶⁴ with twelve bases
2 runnable examples (Python) · about 3 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
The factor table in five languages, the 32-bit trap and a practice set Pro: opens with a Pro, Premium or Ultimate pass
The smallest-prime-factor table is the same array in every language; the traps are in the integer types around it. This part runs one program in Python, JavaScript, Java, C++ and Go, reproduces a 32-bit overflow that makes a famous prime look composite, and ends with a practice set.
In this part:
- One factor table in five languages, with run records
- The 32-bit trap reproduced: i × i wraps and a prime fails
- Edge cases worth a test
- Practice set
2 runnable examples (C++, Go, Java, JavaScript, Python) · about 3 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Interview questions
-
Fresher · Warm-up
Why does trial division only need to try divisors up to √n, and how many divisions does it make on a prime near 10¹²?
If n = d × e and both d and e were larger than √n, their product would be larger than n, so a composite number always has a divisor no larger than √n. When no number from 2 up to the integer square root divides n, no larger number can either, and n is prime. Testing 2 and then only the odd divisors, a prime near 10¹² needs about √n / 2 = 500,000 divisions; the run on 10¹² + 39 made exactly that many. Compute the bound with an exact integer square root, such as math.isqrt in Python: a float square root of a number above 2⁵³ can come out one too small and stop just before the divisor of a prime's square.
-
Mid to senior
Service A answers a million questions a day of the form "is n prime?" with n at most 10⁷. Service B checks single 64-bit numbers that users type in. What do you build for each, with memory and time, and what breaks if you reuse A's design for B?
-
Mid
You must factorise a million numbers, each at most 10⁷, within a second. Compare trial division with a smallest-prime-factor table using operation counts, give the table's memory in Python and in a compiled language, and say when the table is the wrong choice.
-
Senior
Count the primes between 10¹² and 10¹² + 10⁷. Why not sieve from 1, how does a segmented sieve work, how much memory and time does it need, and how would you check the count?
Key takeaways
- Trial division stops at the integer square root: a composite always has a divisor no larger than √n. Compute the
bound with
math.isqrt, never through a float. - The sieve of Eratosthenes crosses out the multiples of each prime from p² and stops at √n: O(n log log n) time and one byte (or one bit) per number.
- A smallest-prime-factor table factorises any number up to n with one lookup per prime factor, at most log₂ n.
- Choose by the workload: a few numbers, trial division; many primality questions, a sieve; many factorisations, a factor table.
References
- Dictionary of Algorithms and Data Structures: sieve of Eratosthenes (National Institute of Standards and Technology (NIST))
- math.isqrt: integer square root (Python Software Foundation)
- tracemalloc: trace memory allocations (Python Software Foundation)
- Ein Beitrag zur analytischen Zahlentheorie (F. Mertens, Journal für die reine und angewandte Mathematik 78, 1874) (De Gruyter)
- Riemann's hypothesis and tests for primality (G. L. Miller, Journal of Computer and System Sciences 13(3), 1976) (Elsevier)
- Probabilistic algorithm for testing primality (M. O. Rabin, Journal of Number Theory 12(1), 1980) (Elsevier)
- The pseudoprimes to 25·10⁹ (C. Pomerance, J. L. Selfridge and S. S. Wagstaff, Mathematics of Computation 35, 1980) (American Mathematical Society)
- Strong pseudoprimes to twelve prime bases (J. Sorenson and J. Webster, Mathematics of Computation 86, 2017) (American Mathematical Society)
- A linear sieve algorithm for finding prime numbers (D. Gries and J. Misra, Communications of the ACM 21(12), 1978) (Association for Computing Machinery)
- Class BigInteger (Java SE 25): isProbablePrime (Oracle)
- Package math/big: Int.ProbablyPrime (The Go Authors)
- The Go Programming Language Specification: integer overflow (The Go Authors)
- The Java Language Specification, Java SE 25: integer operations (Oracle)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress