Complexity, honestly
Big-O as interviewers actually use it — what the notation is really claiming, why "amortised O(1)" is a mathematical statement and not a hand-wave, and the specific complexity claims candidates get wrong most often.
What you'll be able to do
- State a complexity claim precisely, including which variable it is in and whether it is worst case or amortised
- Derive the amortised O(1) cost of dynamic array append from the doubling argument, rather than asserting it
- Avoid the six complexity claims that are most often stated wrongly in interviews
- Choose between two solutions using the constraint bound rather than instinct
Before this: reading-the-problem
Almost everyone can say "that's O(n log n)". Far fewer can say what that sentence is claiming, and the gap shows up in interviews as an inability to defend a design when the interviewer pushes once.
What the notation actually claims
f(n) = O(g(n)) means: there exist constants c > 0 and n₀ such that for all n ≥ n₀, f(n) ≤ c · g(n). Three consequences follow, and each one is a thing candidates get wrong.
It is an upper bound, not an estimate. Binary search is O(n). That statement is true. It is also useless, which is why we say O(log n) — we mean the tight bound, Θ. Nobody will fault you for saying O when you mean Θ, but you should know that when an interviewer asks "is that tight?", they are asking a real question.
It says nothing about small inputs. Insertion sort beats merge sort below roughly 16–32 elements, which is why production sorts are hybrids: real library sorts run a merge or quick strategy down to a small threshold and then finish with insertion sort. "Asymptotically worse" and "slower on your data" are different claims.
It hides the constant, and the constant is sometimes the whole story. Two O(n) algorithms can differ by 20× if one walks a contiguous array and the other chases pointers through a linked list. A cache line is 64 bytes; a sequential scan gets 8 useful 8-byte reads per memory fetch, and a pointer chase gets 1. Big-O cannot see this. Interviewers at systems-heavy companies can.
Which variable?
This is the most common sloppiness, and it is easy to fix. "Building a hash set of the input is O(n)" — n what? Characters? Words? Total bytes? When a problem has more than one size, name them.
- Two-sum on an array: n = number of elements. One variable, no risk.
- Comparing two strings: O(n + m), and collapsing that to O(n) is wrong when one is a billion characters and the other is three.
- Word-break on a dictionary: n = length of the string, m = number of words, k = the longest word. Three variables, and the answer is O(n · k) with a hash set — not O(n · m), which is what you get if you loop the dictionary.
- Graph problems: always O(V + E), never "O(n)". Say which n you mean and interviewers relax visibly.
Watch this if the doubling argument below does not yet feel like a proof. It builds the same result the other way round — starting from the resize cost and summing it — which is the version most people find convincing first.
Amortised is a proof, not a shrug
"Appending to a dynamic array is amortised O(1)" gets said constantly and defended rarely. The defence is short and worth being able to give, because being asked to give it is a standard follow-up.
A dynamic array holds a fixed-capacity buffer. Append writes into the next free slot in O(1). When the buffer is full, it allocates a buffer of double the capacity, copies everything across in O(current size), and then writes. So a single append is O(n) in the worst case. The claim is about the sequence.
1Count the total work, not the per-operation work▾
Start empty and do n appends. Resizes happen at sizes 1, 2, 4, 8, …, up to the last power of two below n. The copying work is the sum of those sizes:
1 + 2 + 4 + 8 + ... + 2^k where 2^k < n
That is a geometric series, and it sums to 2^(k+1) - 1 < 2n. Add the n constant-time writes and total work is under 3n. Divide by n operations: under 3 per operation, a constant. That is the whole proof.
// The doubling argument, measured rather than asserted.
function appendsWithCopyCount(n) {
let capacity = 1;
let size = 0;
let copies = 0;
for (let i = 0; i < n; i++) {
if (size === capacity) {
copies += size; // copy every existing element into the new buffer
capacity *= 2;
}
size++;
}
return copies;
}
for (const n of [10, 100, 1000, 100000, 1000000]) {
const copies = appendsWithCopyCount(n);
console.log(`n=${String(n).padStart(7)} copies=${String(copies).padStart(7)} copies/n=${(copies / n).toFixed(3)}`);
}
# The doubling argument, measured rather than asserted.
def appends_with_copy_count(n):
capacity = 1
size = 0
copies = 0
for _ in range(n):
if size == capacity:
copies += size # copy every existing element into the new buffer
capacity *= 2
size += 1
return copies
for n in [10, 100, 1000, 100000, 1000000]:
copies = appends_with_copy_count(n)
print(f"n={str(n).rjust(7)} copies={str(copies).rjust(7)} copies/n={copies / n:.3f}")
n= 10 copies= 15 copies/n=1.500 n= 100 copies= 127 copies/n=1.270 n= 1000 copies= 1023 copies/n=1.023 n= 100000 copies= 131071 copies/n=1.311 n=1000000 copies=1048575 copies/n=1.049
Run it: the ratio hovers just under 1 and never grows, no matter how large n gets. That bounded ratio is the meaning of amortised O(1).
2Notice that the growth factor is load-bearing▾
Now change capacity *= 2 to capacity += 1 and run it again. The ratio stops being constant and starts growing linearly, because the total copying becomes 1 + 2 + ... + n ≈ n²/2. Growing by a constant amount gives amortised O(n) per append. Growing by a constant factor gives amortised O(1). Any factor above 1 works — implementations use values from 1.125 to 2, trading memory slack against copy frequency.
3Distinguish amortised from average▾
They are not synonyms, and mixing them up is a tell.
Amortised is a worst-case guarantee over a sequence: any n operations cost O(n) total. No adversary can defeat it, because the accounting is airtight.
Average case is a claim about a distribution of inputs. Hash table lookup is O(1) on average, assuming keys distribute well. An adversary who knows your hash function can choose keys that all collide and make every lookup O(n) — the algorithmic complexity attack that pushed language runtimes toward randomised hashing.
So: dynamic array append is amortised O(1) and no input can break it. Hash lookup is average O(1) and a chosen input can. Both get called "O(1)" in conversation; only one of them survives an adversary.
The six claims candidates get wrong
| Claim | Reality |
|---|---|
| "Sorting is O(n log n)" | Comparison sorting is Ω(n log n). Counting and radix sort beat it when keys are small integers — O(n + k). Saying so is a real signal on "sort the ages of a billion people". |
| "Hash map access is O(1)" | Average O(1), worst case O(n) with adversarial or degenerate keys. Also: hashing a k-character string is O(k), not O(1), which matters when strings are long. |
| "Building a heap is O(n log n)" | Bottom-up heapify is O(n). n inserts is O(n log n). Different construction, different bound. |
| "Recursion means O(n) space" | Only if the recursion is not tail-position and your runtime doesn't eliminate it. The space is O(max depth) — O(log n) for balanced binary search, O(n) for a skewed tree. |
| "Slicing a string is free" | In most languages it copies: O(length of the slice). A recursive solution that slices per call is quietly quadratic. Pass indices instead. |
| "Two nested loops means O(n²)" | Not when the inner bound moves monotonically. The two-pointer and sliding-window patterns are nested loops that are O(n), because the inner index never resets. |
That last row is the entire idea behind the next three lessons: a nested loop whose inner pointer only ever moves forward touches each element a bounded number of times, and is therefore linear. Counting loop nesting levels is a heuristic. Counting how many times each element is touched is the actual analysis.
Reading the bound backwards
Given a constraint, you can usually infer the intended solution class. This table is worth knowing cold, because it turns "I'm stuck" into "I'm looking for the wrong shape of answer".
| Bound on n | Operation budget | Intended shape |
|---|---|---|
| n ≤ 12 | n! ≈ 4.8 × 10⁸ | Permutations — full search over orderings |
| n ≤ 25 | 2ⁿ ≈ 3.4 × 10⁷ | Subsets, bitmask DP, meet-in-the-middle |
| n ≤ 500 | n³ = 1.25 × 10⁸ | Floyd–Warshall, interval DP |
| n ≤ 5 000 | n² = 2.5 × 10⁷ | Quadratic DP, all-pairs on a small set |
| n ≤ 10⁵ | n log n ≈ 1.7 × 10⁶ | Sort, heap, binary search, balanced tree |
| n ≤ 10⁷ | n | Single linear pass, counting, sliding window |
| n ≤ 10¹⁸ | log n | Binary search on the answer, matrix power, maths |
The bottom row is the one people miss. When a bound is absurdly large — 10¹⁸ — the input cannot even be read, so the answer is not an algorithm over the input at all. It is closed form, or binary search over the answer space, or fast exponentiation.
Space, which people forget to state
Say the space bound unprompted; roughly half of candidates don't, and it is an explicit rubric line at several companies.
Count only what you allocate beyond the input. A hash set of distinct characters over a fixed alphabet is O(1) — 128 slots is 128 slots whether the string is ten characters or ten million. A hash set of elements is O(n). Recursion costs O(depth) in stack frames whether or not you allocated anything, and an in-place algorithm that recurses is not O(1) space.