Complexity Traps: Where O(n) Hides O(n²)
The most dangerous complexity errors aren’t in your algorithm design — they’re in implementation details that turn a theoretically fast solution into a slow one. This chapter catalogs the most common traps.
Hidden Nested Loops
String Concatenation in Loops
# TRAP: O(n²) — each concatenation copies the entire string
s = ""
for word in words:
s += word # creates a new string of length O(n) each time
# FIX: O(n) — join pre-allocates the result
s = "".join(words)
Substring Search
// TRAP: s.find(sub) is O(n*m) in worst case (not O(n))
// If called inside a loop, total complexity balloons
for (int i = 0; i < n; i++)
if (s.find(queries[i]) != string::npos) // O(n * m) per call
count++;
// FIX: Use KMP or Rabin-Karp for all queries, or Aho-Corasick for multiple patterns
List vs. Deque for Queue Operations
# TRAP: list.pop(0) is O(n) — shifts all elements
from collections import deque
q = list()
q.pop(0) # O(n)
# FIX: deque.popleft() is O(1)
q = deque()
q.popleft() # O(1)
Amortized Operations Hiding Constant Factors
Hash tables are O(1) amortized, but the constant factor and worst case matter:
- Worst case: All keys collide → O(n) per operation. With n operations on n elements, worst case is O(n²).
- Resizing overhead: A hash map with 10^6 insertions spends significant time on rehashing. The amortized O(1) doesn’t tell the full story for a single large batch.
- String hashing: Hashing a string of length k costs O(k), not O(1). Using strings as hash keys in a loop over n strings of length k gives O(nk), not O(n).
Recursion Tree Complexity Mistakes
The Master Theorem Trap
Not all divide-and-conquer follows the Master Theorem. If subproblems overlap (like Fibonacci recursion), the recurrence T(n) = 2T(n-1) + O(1) gives O(2^n), not the O(n) you might expect from seeing “divide by 2.”
Incorrect Branching Factor
# Thinking: "I explore at most 2 options at each level, so it's O(2^n)"
# Reality: The branching factor might not be constant
# If branching factor grows with n, complexity could be O(n!) or worse
Always draw the recursion tree for small n (n=3, 4) and count the nodes before generalizing.
Sorting Hidden Inside API Calls
Many language operations hide a sort:
| Operation | Hidden Cost |
|---|---|
collections.Counter(arr).most_common(k) | O(n log n) if k is large |
sorted(set(arr)) | O(n log n) |
heapq.nlargest(k, arr) | O(n log k) — often assumed O(n) |
bisect.insort(arr, x) | O(n) for list insertion |
| Dictionary key iteration order | No guaranteed order in older Python; insertion order in 3.7+ |
Interview trap: “My solution is O(n) because I just iterate through the map.” But building the map was O(n log n) due to sorted insertion.
Copying Containers Unnecessarily
// TRAP: vector passed by value copies O(n) elements
void process(vector<int> arr) { // O(n) copy!
sort(arr.begin(), arr.end());
}
// FIX: pass by reference
void process(vector<int>& arr) {
sort(arr.begin(), arr.end());
}
// TRAP: returning vector by value from a function called in a loop
vector<int> getSlice(const vector<int>& v, int l, int r) {
return vector<int>(v.begin() + l, v.begin() + r); // O(r-l) copy
}
// Calling this n times in a loop = O(n²)
Iterator Invalidation
// TRAP: erasing while iterating over a vector invalidates the iterator
for (auto it = v.begin(); it != v.end(); it++) {
if (*it == target) v.erase(it); // UB: it is invalidated
}
// FIX: erase returns the next valid iterator
for (auto it = v.begin(); it != v.end(); ) {
if (*it == target) it = v.erase(it);
else it++;
}
This doesn’t change time complexity but causes crashes. In interviews, it’s a correctness trap that wastes debugging time.
Database and Network Complexity Masking
In system design + coding interviews:
- A single SQL query can be O(n) or O(n log n) — the database is doing the work
- Network latency makes O(1) API calls take 50-200ms each. Calling an API in a loop of 10^5 items = 50+ seconds
- Batch operations exist for a reason: one bulk query vs. 10^5 individual queries
The “Looks Linear” Checklist
Before claiming O(n), verify:
- Are there any nested function calls? Even O(1) functions called n times in a loop are O(n), but O(n) functions called n times are O(n²).
- Am I creating new objects in a loop? Each allocation + initialization has a cost.
- Am I using a data structure with hidden costs? (hash map, set, priority queue)
- Am I comparing or hashing strings/containers? These aren’t O(1).
- Is the input being modified/copyied? Pass by reference, not by value.
Interview Questions
-
Is
s += cO(1) or O(n) in Python? Explain the difference between CPython’s optimization and the general guarantee. -
Your solution uses a hash set for O(1) lookups. What is the worst-case complexity? When does it occur?
-
You call
sorted(arr)once and then do O(n) work. Is your overall complexity O(n log n)? What ifsorted()is called inside a loop? -
Analyze the complexity of this code:
for i in range(n): s = "".join(str(x) for x in arr[:i])What is it and why?
-
You use
bisect.insort()in a loop of n iterations. What is the total complexity? -
A function processes n items, each requiring a hash of a string of average length k. What is the complexity?
-
Explain why
list.pop(0)is O(n) anddeque.popleft()is O(1). -
Your recursive function calls itself twice with n-1 and n-2. What is the complexity? How does memoization change it?
-
You call an O(n) function inside a loop that runs O(log n) times. Is the total O(n log n)? What if the loop variable affects the argument size of the function?
-
An API endpoint returns data for one user in 100ms. You need data for 10,000 users. How do you avoid the O(n) latency trap?