Run it
All four traps and all four fixes, each timed at two input sizes so you can watch the broken version quadruple while the fixed one doubles.
The five that account for most of it
1. Membership testing against a list.
This is the most common instance by a wide margin, and the fix is one word.
2. String concatenation in a loop.
3. list.pop(0) or insert(0, x).
4. Repeated min, max, sum or len on a growing structure.
5. Slicing inside a loop or recursion.
| Operation | Cost | The O(1) or O(n) alternative |
|---|
x in list | O(n) | x in set or x in dict |
s += x in a loop | O(n) | "".join(parts) |
lst.pop(0) | O(n) | deque.popleft() |
lst.insert(0, x) | O(n) | deque.appendleft() |
lst.remove(x) | O(n) | Set, or mark as deleted |
max(lst) repeatedly | O(n) | Running maximum, or a heap |
s[1:] repeatedly | O(n) | Index arithmetic |
del lst[0] | O(n) | deque |
lst.index(x) | O(n) | Dict from value to index |
sorted() inside a loop | O(n log n) | Sort once outside |
The four shapes
1. x in a_list inside a loop. The membership test is O(n). Fix: build a set once, before the loop.
2. result += piece in a loop. Strings are immutable, so each step copies everything so far. Fix: collect into a list and "".join at the end.
3. list.pop(0) or insert(0, x). Both shift every other element. Fix: collections.deque.
4. sorted() inside a loop. Usually the data has not changed and the sort belongs outside it. Fix: sort once, or keep a heap if it really does change.
Why they survive code review
None of them looks wrong. Each line is idiomatic Python that would be fine on its own, and on a hundred elements every version is instant. The bug ships, and then someone doubles the input and everything takes four times as long.
The habit worth building is to read the cost of a line rather than its appearance, and to notice when a linear operation has ended up inside a loop. "What is this line's complexity, and how many times does it run?" catches all four.
How to answer in the room
Name the shape, give the complexity, and give the fix in one sentence: "the membership test is a linear scan, so it's O(n²) — build a set before the loop and it's O(n)". Do not hedge; these have definite answers.
If asked to prove it, double the input and show the time going up fourfold. That is what the program below does for all four.
The ones that hide better
Dictionary key ordering. Nothing wrong here, and it is easy to write the list version by habit:
Nested comprehensions over the same data.
list.count inside a loop is the same mistake as in, and it reads even more innocently.
Pandas row-by-row growth.
# O(n^2): each concat copies the whole frame
df = pd.DataFrame()
for row in rows:
df = pd.concat([df, pd.DataFrame([row])])
# O(n): build a list, construct once
df = pd.DataFrame(rows)
Database queries in a loop — the N+1 problem. Not quadratic in CPU, and the same shape: n round trips where one query would do. SELECT ... WHERE id IN (...) or an ORM join/prefetch replaces it, and the win is often larger than any algorithmic fix, because each round trip costs milliseconds.
Re-reading a file inside a loop, and re-compiling a regular expression per iteration. re.compile outside the loop, or rely on the module's internal cache.
How to find it
Test with big input. The fastest diagnostic there is: run with 1,000 and then 10,000 items. Linear code takes 10× longer; quadratic code takes 100×. That ratio identifies the class of the bug before any profiling.
Profile.
Look for a line whose call count scales with n² rather than n — ncalls is often more revealing than the time column.
timeit for micro-comparisons when deciding between two formulations.
Read every loop and ask what each line costs. The habit worth building: when scanning a loop body, mentally annotate each operation as O(1) or O(n). Anything O(n) inside an O(n) loop is the bug.
Know your data structures. Most of these fixes are the same fix — swap a list for a set or dict when the question is "is this present", and swap a list for a deque when the question is "what is at the front".
What it is testing
Can you spot O(n) operations that look constant? in, count, index, pop(0), slicing, and string +=.
Do you reach for the right structure? Set for membership, dict for lookup, deque for both-ends access, Counter for frequencies.
Do you know why += on strings is quadratic? Immutability — each concatenation allocates and copies.
Can you diagnose it empirically? The 10×-input test distinguishes linear from quadratic in seconds.
Do you avoid premature optimisation? Quadratic behaviour on a ten-element list is not worth fixing. Knowing when it matters is part of the answer.
Recap in one screen
- Quadratic code usually has no nested loop — it has an O(n) operation inside an O(n) loop.
- The frequent offenders:
in on a list, += on strings, pop(0), count, repeated max, and slicing. - The fixes are almost always a data-structure swap: set, dict, deque, or
Counter. - Test with 1,000 then 10,000 items: 10× slower is linear, 100× is quadratic.
- The same shape appears as N+1 database queries, where the cost is round trips rather than CPU.