Why Quadratic Algorithms Fall Apart at Scale
Interview prep and real engineering share at least one concern: the difference between an algorithm that scales gracefully and one that collapses under load. The classic example is intersecting two lists of numbers — returning values that appear in both. A naive double loop works fine on small inputs but becomes unusable surprisingly fast.
The Obvious Solution Is Often Quadratic
The straightforward approach checks every element of the first list against every element of the second:
import sys
# the actual code
def intersection(list1, list2):
result = []
for x in list1:
for y in list2:
if x == y:
result.append(y)
return result
# some boilerplate so that we can run it from the command line on lists of
# different sizes
def run(n):
# make 2 lists of n+1 elements
list1 = list(range(3, n)) + [2]
list2 = list(range(n+1, 2*n)) + [2]
# intersect them and print out the result
print(list(intersection(list1, list2)))
# Run with the program's first command line argument
run(int(sys.argv[1]))
With two lists of size n, the inner comparison runs n^2 times — hence the name quadratic.py. That mathematical property (x^2 is a quadratic function) is exactly what makes the slowdown so steep.
Measuring the Slowdown
Running this code against progressively larger inputs shows what quadratic growth actually feels like. With small lists, the timings look unremarkable:
$ time python3 quadratic.py 10
[2]
real 0m0.037s
$ time python3 quadratic.py 100
[2]
real 0m0.053s
$ time python3 quadratic.py 1000
[2]
real 0m0.051s
$ time python3 quadratic.py 10000 # 10,000
[2]
real 0m1.661s
Still under two seconds, so nothing seems wrong. But bump the inputs to 100,000 elements each, and the wait becomes painful:
$ time python3 quadratic.py 100000 # 100,000
[2]
real 2m41.059s
That burst from 1.6 seconds to 160 seconds is the signature of quadratic behavior: 10x more data costs roughly 100x more time. Scaling to a million elements would mean waiting around three hours. Even this contrived example demonstrates why quadratic algorithms become a production hazard once input sizes grow.
A Linear Alternative Using a Hash Set
The fix is to convert the first list into a hash set, then do a single pass over the second list checking membership. The code below avoids overly idiomatic Python so the logic stays readable:
import sys
# the actual algorithm
def intersection(list1, list2):
set1 = set(list1) # this is a hash set
result = []
for y in list2:
if y in set1:
result.append(y)
return result
# some boilerplate so that we can run it from the command line on lists of
# different sizes
def run(n):
# make 2 lists of n+1 elements
list1 = range(3, n) + [2]
list2 = range(n+1, 2*n) + [2]
# print out the intersection
print(intersection(list1, list2))
run(int(sys.argv[1]))
Two changes make the difference: building set1 from list1, and replacing the nested loops with one loop. The running time now scales linearly — doubling the input doubles the work rather than quadrupling it.
Linear Scaling in Practice
Benchmarks on the same machine confirm the improvement. The linear version handles lists from 10 up to 10,000,000 elements without complaint — sizes that would have brought the quadratic version to a crawl at 100,000:
$ time python3 linear.py 100
[2]
real 0m0.056s
$ time python3 linear.py 1000
[2]
real 0m0.036s
$ time python3 linear.py 10000 # 10,000
[2]
real 0m0.028s
$ time python3 linear.py 100000 # 100,000
[2]
real 0m0.048s <-- quadratic.py took 2 minutes in this case! we're doing it in 0.04 seconds now!!! so fast!
$ time python3 linear.py 1000000 # 1,000,000
[2]
real 0m0.178s
$ time python3 linear.py 10000000 # 10,000,000
[2]
real 0m1.560s
Even a list of 10 billion elements would be plausible from a time perspective (roughly 420 seconds, since it's 100x larger than the 4.2-second benchmark). The real blocker there is memory, not speed:
$ time python3 linear.py 10000000000
Traceback (most recent call last):
File "/home/bork/work/homepage/linear.py", line 18, in <module>
run(int(sys.argv[1]))
File "/home/bork/work/homepage/linear.py", line 13, in run
list1 = [1] * n + [2]
MemoryError
real 0m0.090s
user 0m0.034s
sys 0m0.018s
That's a separate concern outside this discussion, but worth remembering when designing for very large datasets.
Why the Membership Check Is Constant Time
The critical line in the fast version is the condition inside the loop:
def intersection(list1, list2):
set1 = set(list1) # this is a hash set
result = []
for y in list2:
if y in set1:
result.append(y)
return result
The check if y in set1 performs the same whether set1 holds a thousand elements or ten million. That's because set1 is a hash set — a hashmap with only keys — and hashmap lookups are effectively instantaneous regardless of size. The basecs series on hash tables and hash functions explains the mechanics behind this constant-time behavior.
Quadratic Code Creeps Into Real Systems
This isn't just an interview artifact. Accidental quadratic behavior shows up in production code often enough that Nelson Elhage documents real-world cases of performance bugs caused by exactly this pattern.
The insidious part is how harmless quadratic logic seems at small scales. A thousand elements completes so quickly you never notice the problem. Feed it a million, though, and hours evaporate. That's why it pays to recognize the pattern and reach for a hashmap when the easy linear alternative exists.
When the "Slow" Method Wins
Performance engineering has nuance, though. In constrained environments — embedded systems, for example — the constant-factor overhead of building a hashmap can outweigh its asymptotic advantage on small inputs. Friends working on such systems have told me this comes up regularly. A useful illustration is this benchmark showing linear search beating binary search on arrays of up to 100 elements. The right choice always depends on the actual data sizes you'll encounter.
Hashmaps aren't literal magic — the math behind their constant-time lookups is teachable and fascinating. But the way a single data structure converts an unusable program into one that handles millions of items smoothly is about as close to magic as engineering gets.



