Polynomials & the FFT
By the end of this lesson you will be able to explain why polynomials have two "faces" (coefficients and point-values), why multiplying is easy in one face and hard in the other, how the complex roots of unity let us switch faces in Θ(n lg n) time, and you will be able to trace recursiveFft, bitReverseCopy and iterativeFft line by line, run the inverse transform, and multiply polynomials and huge integers with them. Nothing is assumed except the earlier lessons: Growth of functions (Θ notation) and Divide and conquer (recurrences, master theorem). Number display: every complex number on this page is shown rounded to 3 decimals (the program keeps full double precision), and a tiny part below 0.0005 is shown as 0.
Complex numbers in one minute
The FFT lives on the complex numbers, so we meet them first. A complex number is written z = a + bi, where a and b are ordinary (real) numbers and the special symbol i satisfies i·i = −1. We call a the real part and b the imaginary part. Because a number now has two parts, we draw it as a point (a, b) on a flat sheet, the complex plane: a is how far right, b is how far up.
Small example: i has (a, b) = (0, 1), the point straight up. Multiply i by i: i·i = −1, the point (−1, 0), straight left. Each multiplication by i turned the arrow by a quarter turn (90°), and two quarter turns make a half turn. Three more words we need:
- Magnitude |z| = √(a² + b²): the arrow's length. Numbers with |z| = 1 lie on the unit circle (the circle of radius 1 around the origin).
- Conjugate z̄ = a − bi: the mirror image across the horizontal axis. For numbers on the unit circle, z̄ = z⁻¹ (multiplying z by z̄ gives |z|² = 1). This is why the inverse transform later uses conjugate roots.
- Euler's formula e^{iθ} = cos θ + i sin θ: the point on the unit circle at angle θ (in radians; a full turn is 2π). Multiplying e^{iα}·e^{iβ} = e^{i(α+β)}: angles add.
Coefficient form: Horner's rule, addition, multiplication
A polynomial is A(x) = a₀ + a₁x + … + a_{n−1}x^{n−1}: a sum of powers of the variable x, each with a number in front (its coefficient). It has degree-bound n (every exponent is strictly below n; the true degree, the biggest exponent with a nonzero coefficient, may be smaller). Its coefficient representation is the vector a = (a₀, …, a_{n−1}); here index j holds the coefficient of x^j, so the maths and the code use the same indices (no shift).
Evaluate means "plug in a number x₀ and compute A(x₀)". Computing every power x₀^j separately would cost about n²/2 multiplications. Horner's rule regroups the sum so each step needs one multiplication and one addition:
Work from the innermost bracket outwards: start with r = 0, then for j = n−1 down to 0 do r = a_j + x₀·r. After the last step r = A(x₀). That is Θ(n) time. Watch it run on the small example A(x) = 3 + 2x + x² at x₀ = 2 (expected value 3 + 4 + 4 = 11):
Now the grade-school multiplication itself: one frame per output coefficient, showing exactly which pairs a_k·b_{j−k} land on x^j.
Dart for these three operations (0-indexed, identical to the maths, so there is no index shift):
Input size → what's feasible. n ≤ 106 coefficients → Horner's Θ(n) evaluation is instant; the Θ(n²) grade-school product is fine up to about n = 2000 (4·106 steps) and hopeless at n = 105 (1010 steps) → use the FFT multiplication section below.
Point-value form: evaluate, add and multiply point-wise
Turning coefficients into point-values is evaluation (Horner at n points: Θ(n²)). The reverse, interpolation, comes next. The uniqueness theorem says n distinct points determine exactly one polynomial of degree-bound n (the Vandermonde matrix of the points is invertible, its determinant ∏_{j<k}(x_k − x_j) is nonzero). In this form:
- Add: if C = A + B then C(x_k) = A(x_k) + B(x_k). Θ(n), provided A and B are evaluated at the same points.
- Multiply: if C = A·B then C(x_k) = A(x_k)·B(x_k). Θ(n). The catch: C has degree-bound m + n − 1, so we need at least m + n − 1 points (for two length-n inputs, about 2n).
The next players evaluate A and B at chosen points, add and multiply them point-wise, then interpolate back (using Lagrange, explained in the interpolation section) and compare with the coefficient-form answer. The second example deliberately uses too few points, so you see the failure the theorem predicts.
Input size → what's feasible. evaluating at n arbitrary points is Θ(n²): fine for n ≤ 2000, too slow at n = 105; point-wise add/multiply are Θ(n) once the values exist, so the conversion is the bottleneck.
Interpolation and Lagrange's formula
The numerator N_k(x) = ∏_{j≠k}(x − x_j) is a polynomial that is zero at every other point (one factor vanishes). Dividing by the number d_k = N_k(x_k) = ∏_{j≠k}(x_k − x_j) rescales it to equal exactly 1 at x_k. Computing all coefficients this way is Θ(n²) if you are smart: multiply out P(x) = ∏_j (x − x_j) once (Θ(n²)), then get every N_k(x) = P(x)/(x − x_k) by synthetic division (dividing a polynomial by a linear factor in Θ(n)), evaluate d_k = N_k(x_k) with Horner (Θ(n)), and add (y_k/d_k)·N_k into a running total (Θ(n)). Building each N_k from scratch would instead cost Θ(n²) per point, Θ(n³) overall. The player follows the smart version, one basis polynomial per step.
Dart for Lagrange's basis polynomial Lk and for the uniqueness theorem (Vandermonde determinant, and "n distinct points force equality"). Indices k, j = 0..n−1 are the Dart indices: no shift.
Input size → what's feasible. Lagrange is Θ(n²): fine for n ≤ 1000 well-spread points; at n = 105 it is 1010 steps (too slow) and numerically unstable → interpolate at the roots of unity with the inverse DFT instead.
Complex roots of unity
Small example: n = 4. Then ω₄ = e^{2πi/4} = cos 90° + i sin 90° = i, and the four roots are 1, i, −1, −i (the compass points). Check: i⁴ = 1. Here is n = 8 next to what happens when you square its roots:
Three facts make the FFT work. Each has a player below that walks through it on a circle, with a custom-input version:
Halving lemma: for even n > 0, squaring the n roots gives the n/2 roots of order n/2, each exactly twice: (ω_n^{k+n/2})² = ω_n^{2k+n} = ω_n^{2k} = ω_{n/2}^k.
Summation lemma: for k not divisible by n, Σ_{j=0}^{n−1} (ω_n^k)^j = ((ω_n^k)ⁿ − 1)/(ω_n^k − 1) = 0 (the marks balance around the circle). Also ω_n^{n/2} = −1.
Cancellation lemma
Read the players: the left circle has the n coarse marks, the right circle the dn fine marks. Mark dk on the fine circle sits at exactly the same angle as mark k on the coarse one.
Halving lemma
The left circle shows ω_n^k (blue) and its opposite partner ω_n^{k+n/2} = −ω_n^k (orange). Squaring doubles an angle, so opposite marks (180° apart) land on the same mark of the right circle (n/2 marks, green).
Summation lemma
Draw the powers r⁰, r¹, r², … of r = ω_n^k as arrows placed head to tail. When the arrows form a closed regular polygon, they add up to 0.
Dart for a root of unity (angle from k mod n, so big exponents stay accurate):
Dart that checks the three lemmas numerically, and the facts about n-th roots of unity and degree-bounds used above (a degree-bound-n polynomial has at most n roots; a product needs la + lb − 1 coefficients, padded to a power of 2). No index shift: ωnk uses k = 0..n−1 in the maths and the code.
Input size → what's feasible. one root of unity costs O(1) (a cos and a sin of the angle of k mod n); all n roots cost Θ(n), instant for n ≤ 106.
The DFT: evaluating at the roots of unity
The DFT (discrete Fourier transform) of a = (a₀,…,a_{n−1}) is y_k = A(ω_nᵏ) = Σ_j a_j ω_n^{kj}, for k = 0..n−1. As a matrix-vector product y = V_n·a, where V_n has entry ω_n^{kj} in row k, column j (a Vandermonde matrix at the roots of unity). Small example, n = 4 and a = (1, 2, 3, 4) (the polynomial 1 + 2x + 3x² + 4x³), computed live by this page:
Straight from the definition this is n sums of n terms: Θ(n²). The FFT (fast Fourier transform) computes exactly the same y in Θ(n lg n) when n is a power of 2 (we assume that throughout).
Dart for the DFT definition and the inverse DFT matrix. The matrix Vn has entry (k, j) = ωnkj with 0-based k, j: the same indices as Dart.
Input size → what's feasible. the definition costs Θ(n²): fine for n ≤ 2000, but n = 218 would be 6.9·1010 steps (too slow) → use the FFT, Θ(n lg n).
recursiveFft(a)
Small example first, by hand, n = 2 and a = (a₀, a₁): A(x) = a₀ + a₁x, roots 1 and −1, so y = (a₀ + a₁, a₀ − a₁). Here the even half is (a₀) and the odd half is (a₁), each already its own DFT, and the combine loop computes exactly y₀ = a₀ + 1·a₁ and y₁ = a₀ − 1·a₁. Bigger sizes repeat this pattern on top of each other.
Below, y0 is the DFT of the even half a0 and y1 is the DFT of the odd half a1. The stack shows every active call (a call is one running copy of the procedure, waiting for its children); the circle on the right shows the roots of unity of the running call, with ωnk (blue) and its partner −ωnk = ωnk+n/2 (orange).
The recursion tree and its cost (analysis visual). Each box holds the indices of a that one call receives; every call splits its indices by parity. Lines 10–14 of a call of size s do s/2 butterflies, so every level does n/2 butterflies in total, and there are lg n levels of non-trivial calls:
Dart implementation (the pseudocode's sign argument lets the same code also compute the inverse's transform later; sign = 1 is this page's convention). Index shift: none, the pseudocode and Dart both count from 0:
The Θ(n lg n) claim in numbers: count the butterflies of iterativeFft for n = 2, 4, …, 216, compare with the recurrence T(n) = 2T(n/2) + n/2 and with n².
Input size → what's feasible. n a power of 2 with n ≤ 218 → about 2.4·106 butterflies, instant; the recursion is only lg n = 18 levels deep but allocates new lists at every level, so for n ≥ 220 prefer iterativeFft (the iterative section below).
The inverse DFT (interpolation at the roots of unity)
Inverse-matrix theorem: (V_n⁻¹)_{jk} = ω_n^{−kj}/n. Proof idea: entry (j,j′) of V_n⁻¹V_n is Σ_k ω_n^{k(j′−j)}/n, which equals 1 when j′ = j and 0 otherwise by the summation lemma (since |j′−j| < n, j′−j is not divisible by n unless zero). So a_j = (1/n) Σ_k y_k ω_n^{−kj}. Compare with y_k = Σ_j a_j ω_n^{kj}: run the same FFT with the roles of a and y swapped, ω_n replaced by ω_n⁻¹ = e^{−2πi/n} (the conjugate, since |ω| = 1), and divide by n. Hence DFT⁻¹ also costs Θ(n lg n). In the code below we reuse the fast iterative transform (next sections) with sign = −1.
Small example: n = 2. y = (a₀ + a₁, a₀ − a₁). Applying the same butterfly with ω = 1 gives (y₀ + y₁, y₀ − y₁) = (2a₀, 2a₁); dividing by n = 2 recovers (a₀, a₁). The players compute each y′_j = Σ_k y_k ω_n^{−kj} term by term (that is what the sign = −1 butterflies produce; the page checks the result against iterativeFft with the conjugate roots), then divide by n:
Input size → what's feasible. n a power of 2 with n ≤ 218 → one FFT plus n divisions ≈ 2.4·106 butterflies; pad shorter inputs with zeros up to the next power of 2.
Polynomial multiplication in Θ(n lg n): evaluate → pointwise multiply → interpolate
Convolution theorem: a ⊗ b = DFT⁻¹_{2n}(DFT_{2n}(a) · DFT_{2n}(b)), where the vectors are padded with zeros to length 2n (more exactly: to a power of 2 that is at least the product's degree-bound) and "·" is the pointwise product. The padding is not optional: with too few points the product's high coefficients wrap around and add onto the low ones (a cyclic convolution). Steps: (1) pad, (2) two forward FFTs, (3) n pointwise multiplications, (4) one inverse FFT. Three transforms, so Θ(n lg n). Worked example computed live by this page — A(x) = 1 + 2x, B(x) = 3 + x, padded to n = 4:
Now step through the three phases yourself: evaluate both polynomials at the n roots of unity, multiply pointwise, then interpolate with the inverse DFT. The page's own FFT computes every value, and each caption cross-checks the result against grade-school (naive) multiplication.
Analysis visuals. (a) What goes wrong without padding: the same product (1 + x)² computed with n = 2 and n = 4. (b) The cost comparison: grade-school multiplies n² pairs; the FFT route does three transforms of (n/2) lg n butterflies plus n pointwise products and n divisions.
Dart, including big-integer multiplication: write each integer's decimal digits as the coefficients of a polynomial (least-significant digit first). Then x = P(10), y = Q(10), and x·y = (PQ)(10): multiply the polynomials, round each coefficient to an integer, then carry to get decimal digits again.
The convolution theorem as a numeric check (Dart). The transform length n decides whether the product wraps around.
Input size → what's feasible. polynomials with ≤ 105 coefficients each → padded n = 218 and ≈ 7·106 butterflies (instant) against 1010 steps for the grade-school method; below ~50–100 coefficients the plain Θ(n²) loop is just as fast.
bitReverseCopy(a, A)
Small example first: n = 4. Indices 0,1,2,3 are 00, 01, 10, 11 in two bits; reversed they read 00, 10, 01, 11 = 0, 2, 1, 3. So a₁ goes to slot 2 and a₂ goes to slot 1; a₀ and a₃ stay. (A bit is one binary digit; see D03.)
Look at the recursion of recursiveFft: the first split sends indices ending in bit 0 left and ending in bit 1 right; the next split looks at the second-lowest bit, and so on. So the leaf that holds a_k is at position rev(k), where rev(k) is the lg n-bit binary expansion of k reversed. bitReverseCopy places a_k into A[rev(k)] (lines 2–3 of the panel: for k from 0 to n − 1, A[rev(k)] ← a[k]). The players show a as symbols a₀…, with binary indices as pointers.
Analysis visual. The table lists k, its lg n-bit binary, the reversed bits and the resulting slot for n = 8; the leaf order of the recursion tree above (0, 4, 2, 6, 1, 5, 3, 7) is exactly "which a_k lands in slot 0, 1, 2, …".
Input size → what's feasible. n ≤ 220 → about 2·107 bit-by-bit steps, instant; a reverse-binary counter or table makes it Θ(n).
iterativeFft(a)
Small example: n = 4, a = (a₀,a₁,a₂,a₃). After bitReverseCopy, A = (a₀,a₂,a₁,a₃). Stage 1 (m = 2, ω = 1) combines neighbours: (a₀+a₂, a₀−a₂, a₁+a₃, a₁−a₃). Stage 2 (m = 4) combines those two blocks with twiddles ω₄⁰ = 1 and ω₄¹ = i. That is the whole algorithm; for larger n there are just more stages.
The butterfly network (analysis visual) for n = 8: wires run left to right, inputs on the left already in bit-reversed order (label a_k means "a_k sits here"), lg n = 3 stages, each with n/2 = 4 butterflies. The numbers on the wires are the twiddle exponents (ω_m^j).
In the players below the current group (lines 6–7) is orange, the two cells overwritten by the latest butterfly (lines 8–13) are red, and a live copy of this network under the array turns green for every butterfly already executed and red for the one just executed. The circle shows ω_m and the twiddle factor ω = ω_m^j in use.
Input size → what's feasible. n a power of 2 with n ≤ 218 runs in about 0.1 s; in double precision integer products stay exact while coefficients are ≤ ~100 and lengths ≤ 105 (see the multiplication section), beyond that use an NTT (question 23).
Quiz
Interview questions
Cheat sheet
All procedures here are deterministic and data-independent, so best = average = worst case.
| Procedure | Time (best = avg = worst) | Space | In place? | Use when |
|---|---|---|---|---|
| Horner evaluation | Θ(n) | O(1) | yes | One point, coefficient form |
| Coefficient add | Θ(n) | O(n) | can be | Same degree-bound; either form works |
| Naive polynomial multiply (convolution) | Θ(n²) | O(n) | — | Small n (below roughly 50–100) |
| Point-value add / multiply | Θ(n) | O(n) | can be | After conversion; needs ≥ m + n − 1 shared points for a product |
| Evaluation at n arbitrary points (Horner each) | Θ(n²) | O(n) | — | Arbitrary points |
| Lagrange interpolation (all coefficients) | Θ(n²) | O(n) | — | Arbitrary distinct points |
| Naive DFT (definition) | Θ(n²) | O(n) | — | Reference / any n |
| recursiveFft | Θ(n lg n) | Θ(n lg n) allocated in total, Θ(n) live per level | no | Clarity; n a power of 2 |
| bitReverseCopy | Θ(n lg n) (Θ(n) with table) | Θ(n) | can swap in place | Prepares iterativeFft |
| iterativeFft | Θ(n lg n) — (n/2) lg n butterflies | Θ(n) | yes (after the copy) | Production, parallel circuit depth Θ(lg n) |
| Inverse DFT | Θ(n lg n) | Θ(n) | yes | ω → ω⁻¹, divide by n |
| Polynomial / big-integer multiply via FFT | Θ(n lg n) | Θ(n) | no | Large n; round results, watch precision (use NTT for exactness) |