Your DFT is 530× too slow. The fix isn't a faster computer — it's noticing that most of
that work is the same work done twice. Cooley and Tukey spotted it in 1965 and made
real-time signal processing possible. Today you spot it too. Time to transform!
What You'll Build
A hand-rolled single split: transform the even samples and the odd samples separately, stitch
them back together, and prove the answer is identical — for half the work.
Learning Objectives
Split a signal into even and odd samples and transform each half
Recombine two half-spectra with a twiddle factor
Explain how one complex multiply produces two output bins
Count the operations saved per split, and after full recursion
Justify why N must be a power of two
Concepts Introduced
ID
Concept
363
Divide And Conquer
364
Even Odd Split
365
Recursive Decomposition
366
Subproblem
367
Recombination Step
368
Logarithmic Stages
369
Complexity Reduction
370
Power Of Two Constraint
371
Redundant Computation
372
Symmetry Exploitation
Background
The observation
Split your N samples into evens and odds. Transform each half separately, giving E[k] and
O[k]. Then the full transform is:
It's a point on the unit circle — a rotation. It accounts for the odd samples being offset by
one position from the evens. Lab 18 gives it a proper table.
Why stop at one split?
One split halves the work. But each half is itself a DFT — so split those too. And
again. Keep going until each piece is a single sample, whose transform is just itself
with no arithmetic at all. That's log₂(N) levels of splitting, and it's where
N·log₂(N) comes from.
# Lab 17: Divide and Conquer -- From DFT to FFT## Last lab we found the DFT is 530x too slow, and that it recomputes the same# angles thousands of times. That is a hint: there is REDUNDANT WORK in there.## Here is the observation that unlocks everything. Split your samples into# the even-numbered ones and the odd-numbered ones. Transform each half# separately. Then the full transform can be rebuilt from those two halves:## X[k] = E[k] + W^k * O[k]# X[k + N/2] = E[k] - W^k * O[k]## Look at the second line. It reuses E[k] and O[k] -- ALREADY COMPUTED for# the first line. One multiplication buys you two output bins.## Do that recursively and N^2 collapses to N log N.importconfigimportmathimporttimedefdft(signal):"""The brute-force DFT from Lab 14."""n=len(signal)real,imag=[],[]forkinrange(n):re=im=0.0fortinrange(n):angle=2*math.pi*k*t/nre+=signal[t]*math.cos(angle)im-=signal[t]*math.sin(angle)real.append(re)imag.append(im)returnreal,imagdefdft_split(signal):"""One level of divide and conquer, done by hand. Transform the even samples and the odd samples separately, then stitch the results together. The answer is identical to dft() -- but the two half-size DFTs cost 2*(N/2)^2 = N^2/2 instead of N^2. """n=len(signal)half=n//2evens=[signal[i]foriinrange(0,n,2)]odds=[signal[i]foriinrange(1,n,2)]e_re,e_im=dft(evens)o_re,o_im=dft(odds)real=[0.0]*nimag=[0.0]*nforkinrange(half):# W^k, the "twiddle factor" -- a rotation by -2*pi*k/nangle=-2*math.pi*k/nwr=math.cos(angle)wi=math.sin(angle)# W^k * O[k] (complex multiply)tr=wr*o_re[k]-wi*o_im[k]ti=wr*o_im[k]+wi*o_re[k]# Two outputs from one multiplication. THIS is the saving.real[k]=e_re[k]+trimag[k]=e_im[k]+tireal[k+half]=e_re[k]-trimag[k+half]=e_im[k]-tireturnreal,imagdefmagnitudes(real,imag):return[math.sqrt(real[k]**2+imag[k]**2)forkinrange(len(real))]# =========================================================================# PART 1 -- prove the split gives the same answer# =========================================================================N=32signal=[math.sin(2*math.pi*3*i/N)+0.5*math.sin(2*math.pi*7*i/N)foriinrange(N)]direct=magnitudes(*dft(signal))split=magnitudes(*dft_split(signal))print("=== PART 1: does splitting change the answer? ===")print("%4s%12s%12s%12s"%("bin","direct DFT","split DFT","difference"))worst=0.0forkinrange(N//2+1):d=abs(direct[k]-split[k])ifd>worst:worst=difdirect[k]>0.01ork<9:print("%4d%12.4f%12.4f%12.2e"%(k,direct[k],split[k],d))print()print("largest difference anywhere: %.2e"%worst)print("Identical (to float precision). The split is exact, not an approximation.")# =========================================================================# PART 2 -- count the work# =========================================================================print()print("=== PART 2: how much work did we save? ===")print()print("%8s%14s%16s%10s"%("N","direct (N^2)","one split","saved"))fornin(16,32,64,128,256,512):direct_ops=n*n# two half-size DFTs, plus n/2 recombinationssplit_ops=2*(n//2)**2+n//2print("%8d%14d%16d%9.0f%%"%(n,direct_ops,split_ops,100*(1-split_ops/direct_ops)))print()print("One split saves about half the work. But why stop at one split?")# =========================================================================# PART 3 -- keep splitting# =========================================================================print()print("=== PART 3: splitting all the way down ===")print()print("Each split halves the work again. Keep going until each piece is a")print("single sample -- whose DFT is just itself, requiring no arithmetic.")print()print("%8s%10s%14s%16s%12s"%("N","splits","direct N^2","FFT N*log2(N)","speedup"))fornin(16,64,256,512,1024,4096):stages=int(math.log(n,2)+0.5)fft_ops=n*stagesprint("%8d%10d%14d%16d%11.0fx"%(n,stages,n*n,fft_ops,(n*n)/fft_ops))print()print("At N=512 that is a %.0fx reduction in arithmetic."%(512*512/(512*9)))print("This is why the FFT matters. Same answer, a fraction of the work.")# =========================================================================# PART 4 -- why powers of two# =========================================================================print()print("=== PART 4: why N must be a power of two ===")print("Halving only works cleanly if the size keeps dividing by 2.")print()fornin(512,500):print("N = %d:"%n,end=" ")x=nchain=[]whilex%2==0andx>1:chain.append(x)x//=2chain.append(x)print(" -> ".join(str(c)forcinchain)," (clean)"ifx==1else" <- STUCK at %d, cannot halve"%x)# =========================================================================# PART 5 -- measure it# =========================================================================print()print("=== PART 5: measured, not just counted ===")n=128sig=[math.sin(2*math.pi*5*i/n)foriinrange(n)]start=time.ticks_ms()dft(sig)direct_ms=time.ticks_diff(time.ticks_ms(),start)start=time.ticks_ms()dft_split(sig)split_ms=time.ticks_diff(time.ticks_ms(),start)print("N = %d"%n)print(" direct DFT : %5d ms"%direct_ms)print(" ONE split : %5d ms (%.1fx faster)"%(split_ms,direct_ms/split_ms))print()print("And that is from a single split. Lab 20 does all %d of them."%int(math.log(512,2)+0.5))
12345
bin direct DFT split DFT difference
3 16.0000 16.0000 1.91e-06
7 8.0000 8.0000 9.54e-07
largest difference anywhere: 2.39e-06
Identical to float precision. This is not an approximation — it's an exact restructuring of
the same arithmetic.
Step 2 — Count what one split saves
12
N direct (N^2) one split saved
512 262144 131328 50%
Two half-size DFTs cost 2·(N/2)² = N²/2. Half the work, same answer.
Step 3 — Recurse all the way down
123
N splits direct N^2 FFT N*log2(N) speedup
512 9 262144 4608 57x
4096 12 16777216 49152 341x
Nine splits for 512 samples. 57× less arithmetic — and the advantage grows with N, which is
why the FFT matters more the bigger your problem gets.
Halving only works if the size keeps dividing evenly. 512 splits nine times to reach 1;
500 jams at 125.
(Other FFT variants handle non-powers-of-two — mixed-radix, Bluestein's — but radix-2 is the
one you can hold in your head, and 512 is a fine size for audio.)
Step 5 — Measure it
123
N = 128
direct DFT : 1213 ms
ONE split : 551 ms (2.2x faster)
One split, measured on hardware, gives 2.2×. Lab 20 does all nine.
Expected Output
Your millisecond values will differ; the ~50% saving per split and the 57× count at N=512
should not.
Troubleshooting
Symptom
Likely cause
Fix
Split disagrees with direct
Twiddle sign wrong
The angle is -2πk/n, negative
Only the first half is right
Missing the X[k+N/2] line
Both outputs come from one product
Errors around 1e-6
Normal float32 behaviour
See Lab 15
Crash on odd N
Can't halve
Use a power of two
Challenges
Split twice. Apply the split to each half as well, giving four quarter-size DFTs. What
fraction of the original work remains?
Make it recursive. Write fft_recursive(signal) that calls itself until the input is
length 1. Check it against the direct DFT.
Where's the limit? The count says 57× at N=512, but Lab 20 will measure ~146×. Why might
the measured gain exceed the arithmetic count? (Hint: what else did we stop doing?)
Check Your Understanding
Write the two recombination equations from memory.
Why does one complex multiply give two output bins?
Where does the log₂(N) in N·log₂(N) come from?
Why must N be a power of two for radix-2?
Is the split an approximation? Justify your answer from the measured output.
You found the redundant work
That's the whole insight — the rest is bookkeeping. Next lab handles the bookkeeping:
what order the data ends up in, and how to stop recomputing the same angles.