What is the complexity of this code?

Four shapes turn linear code quadratic, and interviewers use all of them: in on a list inside a loop, += on a string in a loop, pop(0) used as a queue, and sorted() called inside a loop. Each is cheap once and fatal repeated.

Overview

The pattern

Quadratic code rarely looks quadratic. There is no visible nested loop — instead, an O(n) operation hides inside an O(n) loop, and the cost multiplies.

The reason it survives review and testing is scale. A thousand items finish in milliseconds; a hundred thousand takes minutes. The bug ships because the test data was small.

nO(n) workO(n²) work
1,0001 thousand1 million — still instant
10,00010 thousand100 million — noticeable
100,000100 thousand10 billion — minutes
1,000,0001 million1012 — hours

The skill being tested is knowing which innocent-looking operations are O(n), so you can spot them inside a loop.

Dicts, sets & hashingConceptualMedium

Step through it

What to watch

  • Every individual line here is idiomatic and fine.
  • The cost comes from the nesting, which is invisible on small input.
  • Each fix is one line, and each is a different structure.

Say this out loud

"That's O(n²) - the membership test inside the loop is a linear scan. Build a set once before the loop and it's O(n)."

What is the complexity of this code?

Here is a loop. What is its complexity? (The four traps interviewers actually use.)

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.

1Python
Output
2Python
Output
3Python
Output
4Python
Output
5Python
Output
6Python
Output
7Python
Output
8Python
Output

The five that account for most of it

1. Membership testing against a list.

9Python
Output

This is the most common instance by a wide margin, and the fix is one word.

2. String concatenation in a loop.

10Python
Output

3. list.pop(0) or insert(0, x).

11Python
Output

4. Repeated min, max, sum or len on a growing structure.

12Python
Output

5. Slicing inside a loop or recursion.

13Python
Output
OperationCostThe O(1) or O(n) alternative
x in listO(n)x in set or x in dict
s += x in a loopO(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) repeatedlyO(n)Running maximum, or a heap
s[1:] repeatedlyO(n)Index arithmetic
del lst[0]O(n)deque
lst.index(x)O(n)Dict from value to index
sorted() inside a loopO(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:

14Python
Output

Nested comprehensions over the same data.

15Python
Output

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.

16Python
Output

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.

How the code works

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.

How the code works

  1. lookup = set(pool)Built once, outside the loop. That single line converts O(n·m) into O(n + m) and is the fix for the most common trap of the four.
  2. parts.append(...) then "".join(parts)One allocation at the end instead of one per step. The Buf class exists so CPython's in-place string resize cannot hide the cost.
  3. q.popleft()deque is a doubly linked list of blocks, so both ends are O(1). A queue on a list is the second most common accidental quadratic.
  4. the growth columnThe number to read. Doubling the input roughly quadruples a quadratic and roughly doubles a linear one — which is how you demonstrate a complexity claim without arguing about it.

Change one thing

  • Double the sizes again. The ratios hold, which is the point of measuring growth rather than absolute time.
  • Move set(pool) inside the comprehension. It becomes worse than the list version — the fix is building it once, not using a set.

Where this runs

Real CPython, compiled to WebAssembly and running on your own machine — nothing is uploaded. The first run takes a few seconds while the interpreter downloads; after that it is immediate. Need more room, or want to paste your own attempt? Use the Python compiler.

Check yourself

0 of 3

Answer without scrolling back up.

  1. for x in items: if x in a_list: ... has complexity:

  2. Which fixes a queue built on list.pop(0)?

  3. How do you demonstrate a quadratic without arguing about it?

Cheat sheet

What is the complexity of this code?

Four shapes turn linear code quadratic, and interviewers use all of them: in on a list inside a loop, += on a string in a loop, pop(0) used as a queue, and sorted() called inside a loop. Each is cheap once and fatal repeated.

INTERVIEW · vizlearn.in/interview/accidental-quadratic-complexity.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.