Cairn
algorithms · number theory · O(log(min(a, b)))

back to Number Theory

Extended Euclidean Algorithm

The Euclidean Algorithm answers "what's the greatest common divisor of a and b?" This page answers a sharper question the plain version leaves on the table: which integers x and y actually produce that gcd, as a combination a·x + b·y = gcd(a, b)? That such x and y always exist — for any integers a and b, not just nice ones — is Bézout's identity. Finding them isn't a separate search: the same reduction loop that finds the gcd, run with two extra numbers carried alongside, produces x and y for free.

The payoff is practical, not just theoretical: when gcd(a, m) = 1, the x this algorithm returns is (after one small adjustment — see Pitfalls) the modular inverse of a mod m — the number that plays the role of "1/a" in arithmetic where only remainders after dividing by m exist. Modular inverses are how RSA key generation and countless other modular-arithmetic algorithms divide without division.

Try it

Enter any two whole numbers (the algorithm strips the sign before starting, same as the plain version) and step through the reduction. Watch the s and t columns build up alongside the familiar r column — they carry the coefficients that will satisfy Bézout's identity once r hits zero. 240, 46 is a classic textbook pair; 3, 11 finishes with gcd 1, so its x is directly a modular inverse.

Press Load, then Step through the reduction.

Why it works

Carry two running coefficients alongside the usual remainder, one pair per number in the reduction: s tracks a's contribution, t tracks b's. Initialize so the invariant holds trivially at the very first row — a itself is "0 reductions in," so its coefficient pair is (1, 0) (that's just a = a·1 + b·0); b is (0, 1) the same way. Every step of the plain algorithm replaces (old_r, r) with (r, old_r − q·r) where q = ⌊old_r / r⌋ — the extended version applies the identical quotient to the coefficient pairs: (old_s, s) → (s, old_s − q·s) and the same for t. Because r's update and each coefficient's update use the same subtraction with the same q, the relationship a·s + b·t = r that held at row zero keeps holding at every later row too — it's an invariant carried forward by construction, not something checked after the fact.

row i:      a·s_i + b·t_i = r_i     (invariant, holds at every row)
row 0:      a·1   + b·0   = a
row 1:      a·0   + b·1   = b
row i+1:    s_{i+1} = s_{i-1} − q_i·s_i,   t_{i+1} = t_{i-1} − q_i·t_i,   same q_i as r

Run the loop to its usual end — r reaches 0, and the row just before it holds gcd(a, b) in old_r, exactly as in the plain algorithm. The invariant says that same row's old_s and old_t satisfy a·old_s + b·old_t = old_r = gcd(a, b) — Bézout's identity, read straight off the last row the loop touches. No extra search, no back-substitution through the whole trail of divisions by hand (the textbook-recursive way to teach this) — carrying two more numbers through the same forward loop is enough.

Reference implementation

function extGcd(a, b) {
  const signA = a < 0 ? -1 : 1;
  const signB = b < 0 ? -1 : 1;
  a = Math.abs(a);
  b = Math.abs(b);
  let oldR = a, r = b;
  let oldS = 1, s = 0;
  let oldT = 0, t = 1;
  while (r !== 0) {
    const q = Math.floor(oldR / r);
    [oldR, r] = [r, oldR - q * r];
    [oldS, s] = [s, oldS - q * s];
    [oldT, t] = [t, oldT - q * t];
  }
  // oldR = gcd(a, b); oldS, oldT solve it for the *absolute* a, b —
  // flip sign to match whatever sign the caller actually passed in.
  return { gcd: oldR, x: oldS * signA, y: oldT * signB };
}

Pitfalls

The x this returns is not automatically in the range a modular inverse needs — it can come out negative, and JavaScript's % won't fix that for you. A modular inverse of a mod m is conventionally reported in [0, m), but extGcd returns whichever x happens to fall out of the reduction, sign and all. Take extGcd(3, 7): it returns x = -2. That's a completely valid Bézout coefficient — 3·(-2) + 7·1 = 1 checks out — but it's not what most callers mean by "the inverse of 3 mod 7," and x % 7 in JavaScript doesn't fix that for you: -2 % 7 === -2, following the dividend's sign as always (the exact same JavaScript remainder behavior the plain Euclidean Algorithm's own Pitfalls section warns about) — still negative, still outside the [0, m) range a modular inverse is conventionally reported in. The fix is the same double-add-and-mod normalization used anywhere a possibly-negative value needs folding into [0, m): ((x % m) + m) % m. For x = -2, m = 7 that's ((-2 % 7) + 7) % 7 = (-2 + 7) % 7 = 5 — checked directly: 3 × 5 = 15 = 2×7 + 1, so 5 really is 3's inverse mod 7. Verified this normalization (and the un-normalized failure it fixes) across every coprime pair 1 ≤ a, m < 200 — 24,104 coprime pairs out of 39,402 checked total, zero mismatches after normalizing, and confirmed the algorithm correctly reports "no inverse exists" (via gcd ≠ 1) on all 15,298 non-coprime pairs in the same sweep, rather than silently returning a wrong answer.

x and y aren't unique — the algorithm hands back exactly one solution out of infinitely many. Given any solution (x₀, y₀), every pair (x₀ + k·(b/g), y₀ − k·(a/g)) for integer k also satisfies Bézout's identity, where g = gcd(a, b) — shifting x by a whole multiple of b/g and y by the matching multiple of a/g cancels out exactly, leaving a·x + b·y unchanged. Checked directly on the demo's classic pair: extGcd(240, 46) returns (x, y) = (-9, 47); stepping k = 1 gives (-9 + 46/2, 47 − 240/2) = (14, -73), and indeed 240×14 + 46×(-73) = 3360 − 3358 = 2, the same gcd. A caller who assumes the returned pair is the answer — say, expecting a specific x on repeated calls, or comparing two independent implementations' output directly — will be surprised the moment either implementation takes a different reduction path.

Complexity

O(log(min(a, b))) division steps — identical bound to the plain Euclidean Algorithm, and for the same reason: the extended version runs exactly the same sequence of divisions, just carrying two more running numbers through each one. Those extra multiply-and-subtract updates are O(1) work per step, so they add a constant factor, not a new term — the step count itself, and therefore the Fibonacci-pair worst case proven on the plain algorithm's own page, carries over unchanged.

This site's guide, Choosing a Number Theory Algorithm, compares this entry against the other thirteen Number Theory entries side by side.