Skip to the chapter
←DSAGGroundworkΣDSA
DSA/Beginner/B1
1 / 48 written chapters
/

Levels and plan
  1. B1Complexity analysis
  2. B2JavaScript toolkit
  3. B3Arrays & strings
  4. B4Prefix sums
  5. B5Hashing
  6. B6Two pointers
  7. B7Sliding window
  8. B8Binary search
  9. B9Basic recursion
  10. B10Sorting algorithms
  11. B11Stacks & queues
  12. B12Linked lists
  1. I1Trees
  2. I2BST operations
  3. I3Tree problems in depth
  4. I4Heaps & priority queues
  5. I5Graphs: representation
  6. I6BFS on grids
  7. I7Graph problems
  8. I8Topological patterns
  9. I9Backtracking
  10. I10DP: 1D
  11. I11DP: 2D
  12. I12DP state machines
  13. I13Greedy algorithms
  14. I14Intervals
  15. I15Bit manipulation
  16. I16Matrix problems
  17. I17Tries
  18. I18Monotonic stack & queue
  19. I19Math for interviews
  20. I20Design & simulation
  21. I21Divide and conquer
  22. I22Randomization
  1. A1Advanced DP
  2. A2Union-Find
  3. A3Advanced graph algorithms
  4. A4Minimum Spanning Tree
  5. A5Advanced graphs
  6. A6Segment & Fenwick trees
  7. A7Sparse tables
  8. A8String algorithms
  9. A9Design problems
  10. A10Advanced backtracking
  11. A11Games & probability DP
  12. A12Ordered structures
  13. A13Sweep line & geometry
  14. A14Interview strategy

ΣB1 · Part 1 · Beginner

Complexity analysis

The one skill every interviewer is silently scoring, whether they say so or not.

30 min read3 diagrams19 sections7 exercises
1 of 48

In this chapter

  • What Big-O actually measures
  • The formal definition — worth seeing once
  • Big-O's two siblings: Ω and Θ
  • Best, average, and worst case — three different questions
  • Multiple inputs — when there isn't just one "n"

and 14 more

about 30 min
19On this page
  1. What Big-O actually measures
  2. The formal definition — worth seeing once
  3. Big-O's two siblings: Ω and Θ
  4. Best, average, and worst case — three different questions
  5. Multiple inputs — when there isn't just one "n"
  6. The picture that makes it click
  7. Why the shape matters more than it seems — actual numbers
  8. The complexities you'll actually see
  9. Reading complexity out of code
  10. Loops that don't count up by one
  11. What built-in calls really cost
  12. Dropping constants and lower-order terms
  13. Recurrence relations — how you actually derive O(n log n)
  14. Memoized recursion: states times work per state
  15. Space complexity — the part people forget
  16. Amortized complexity — the array.push() case
  17. The potential method: amortized cost with a bank account
  18. Output size, and numbers that are values rather than sizes
  19. The constraints tell you the target complexity

What Big-O actually measures

Big-O is not a speed measurement. It's a description of how the work grows as the input grows. Two functions can both be "O(n)" and one can be 100x slower than the other in real seconds — Big-O doesn't care. It only answers one question: if you double the input, roughly what happens to the work?

The formal definition — worth seeing once

You'll almost never need to write this out in an interview, but knowing it makes every informal explanation click into place. Formally, f(n) = O(g(n)) means: there exist positive constants c and n₀ such that f(n) ≤ c · g(n) for every n ≥ n₀. In plain words — past some point, f(n) never grows faster than a constant multiple of g(n). Big-O is an upper bound on growth, not an exact count.

n f(n) — your actual function c · g(n) — a scaled bound after n₀, f(n) always stays under the bound n₀
Left of n₀, f(n) can do anything — even exceed the bound (see the spike). Right of n₀, it never crosses back above c·g(n) — that's the entire promise Big-O makes.

This is exactly why constants get dropped: f(n) = 5n is still O(n), because you can always pick a big enough c (say, c = 5) to make the inequality true. Big-O cares about the shape as n → ∞, not the specific multiplier.

Big-O's two siblings: Ω and Θ

Big-O gets all the attention in interviews, but it's technically only the upper bound. Two related notations describe the other directions — worth being able to name if asked "isn't that also Ω(something)?"

NotationMeansPlain English
O(g(n))upper bound"at worst, this many operations" — never more, could be less
Ω(g(n))lower bound"at best, this many operations" — never fewer
Θ(g(n))tight boundboth O and Ω hold — this is genuinely how it grows, not just a ceiling

Example: linear search is O(n) (never worse than scanning everything) and Ω(1) (you might get lucky and find it first) — so it's not Θ(n) in general, because best and worst case differ. Merge sort, on the other hand, always does Θ(n log n) — best, average and worst case are all the same shape, so people often say "O(n log n)" and "Θ(n log n)" almost interchangeably for it. In interviews, saying "O" when you technically mean "Θ" is common and accepted — but knowing the difference exists signals real understanding.

Best, average, and worst case — three different questions

"What's the complexity of X" is actually an incomplete question — the answer can depend on which input you're worried about.

AlgorithmBest caseAverage caseWorst case
Linear searchO(1) — target is firstO(n)O(n) — target is last or missing
QuicksortO(n log n)O(n log n)O(n²) — already-sorted input, bad pivot
Binary searchO(1) — target is the middleO(log n)O(log n)
Insertion sortO(n) — already sortedO(n²)O(n²) — reverse sorted

Unless told otherwise, interviewers want the worst case — it's the guarantee that holds no matter what input shows up. But naming the best case too (especially when it differs a lot, like quicksort's) shows you actually understand the algorithm's behavior instead of having memorized one number.

Multiple inputs — when there isn't just one "n"

Plenty of real problems take two different collections, and it's a common mistake to collapse them into one variable when they shouldn't be. If a function has an array of size a and a second array of size b:

// O(a + b) — two SEPARATE passes, not nested
function concat(arr1, arr2) {
  const result = [];
  for (const x of arr1) result.push(x);  // O(a)
  for (const x of arr2) result.push(x);  // O(b)
  return result;
}

// O(a × b) — NESTED, every element of one meets every element of the other
function hasCommonElement(arr1, arr2) {
  for (const x of arr1) {
    for (const y of arr2) {
      if (x === y) return true;
    }
  }
  return false;
}
⚠ "O(n²)" can be the wrong (and misleading) answer If a candidate calls the first example "O(n²)" because they see two loops, that's a real mistake — the loops are sequential, not nested, so it's O(a + b), which simplifies to O(n) only if a and b are actually the same order of magnitude. Saying "O(a + b)" out loud instead of collapsing to a single n shows you're reasoning about the actual inputs, not pattern-matching "two loops = squared."

The picture that makes it click

Every explanation of Big-O eventually points at the same chart. Look at it once, properly, and the notation stops being abstract letters and starts being a shape you recognize on sight.

n ops O(1) O(log n) O(n) O(n log n) O(n²) O(2ⁿ) O(n²) and O(2ⁿ) explode — real numbers in the table below
Same input size n on the x-axis for every curve — the only difference is the shape of the growth. Green stays cheap, red gets unusable fast.
Say it like this → "Big-O describes the growth rate of an algorithm's work relative to input size, ignoring constants — it's a shape, not a stopwatch."

Why the shape matters more than it seems — actual numbers

The chart makes the shape obvious, but the real gut-punch is what these shapes mean at realistic input sizes. This is the table that explains why an interviewer's face changes when your solution is O(n²) on an input that might be a million elements.

Complexityn = 10n = 1,000n = 1,000,000
O(1)111
O(log n)~3~10~20
O(n)101,0001,000,000
O(n log n)~33~10,000~20,000,000
O(n²)1001,000,0001,000,000,000,000
O(2ⁿ)1,024more than atoms in the universe—

A modern CPU does roughly 10⁸–10⁹ simple operations per second. At n = 1,000,000, an O(n) solution finishes in a blink; an O(n²) solution needs a trillion operations — that's minutes to hours, not milliseconds, on the exact same input. This is the entire reason interviewers care so much about the shape and so little about your variable names.

The complexities you'll actually see

Name Notation Feels like Example
Constant O(1) same work no matter the input size array index access, hash map lookup
Logarithmic O(log n) work halves each step binary search
Linear O(n) one pass over the input a single loop, array scan
Linearithmic O(n log n) a linear pass, log n times merge sort, quicksort (average)
Quadratic O(n²) a loop inside a loop comparing every pair, bubble sort
Exponential O(2ⁿ) doubles with every extra input naive recursive Fibonacci, subsets
Factorial O(n!) every possible ordering brute-force permutations

In interviews, almost every answer you'll ever give is one of these seven. If you can name which shape your solution is and defend why, you've already cleared the bar most candidates trip on.

Reading complexity out of code

The rule of thumb: count the loops, not the lines.

// O(1) — no loop, fixed number of steps
function first(arr) {
  return arr[0];
}

// O(n) — one loop over the input
function sum(arr) {
  let total = 0;
  for (let i = 0; i < arr.length; i++) {
    total += arr[i];
  }
  return total;
}

// O(n²) — a loop inside a loop, both sized by n
function hasDuplicatePair(arr) {
  for (let i = 0; i < arr.length; i++) {
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[i] === arr[j]) return true;
    }
  }
  return false;
}

// O(log n) — the search space halves every step
function binarySearch(sorted, target) {
  let lo = 0, hi = sorted.length - 1;
  while (lo <= hi) {
    const mid = lo + Math.floor((hi - lo) / 2);
    if (sorted[mid] === target) return mid;
    if (sorted[mid] < target) lo = mid + 1;
    else hi = mid - 1;
  }
  return -1;
}
⚠ The hidden O(n) inside a loop arr.includes(x), arr.indexOf(x) and [...set] are each O(n) on their own. Call one of them inside a loop that already runs n times, and the whole function is quietly O(n²) — even though you only wrote one visible for. This exact trap is one of the most common ways candidates lose points without realizing it.
// looks like O(n) — is actually O(n²)
function hasDuplicate(arr) {
  const seen = [];
  for (const x of arr) {
    if (seen.includes(x)) return true;  // O(n) work, n times
    seen.push(x);
  }
  return false;
}

// the fix: swap the array for a Set → O(1) lookup → true O(n)
function hasDuplicateFast(arr) {
  const seen = new Set();
  for (const x of arr) {
    if (seen.has(x)) return true;
    seen.add(x);
  }
  return false;
}

Dry run: hasDuplicate([3, 1, 4, 1]) — counting the hidden work

xseen beforeseen.includes(x) comparisonsMatch?
3[]0no
1[3]1 (vs 3)no
4[3, 1]2 (vs 3, 1)no
1[3, 1, 4]2 (vs 3, then 1 — match)yes → return true

0 + 1 + 2 + 2 = 5 comparisons to check an array of only 4 elements — the cost of each includes call grows with how much of seen exists so far, not a fixed amount. That growing-per- iteration cost is the O(n²) signature hiding behind what reads like a single, innocent for...of loop.

Loops that don't count up by one

"Count the loops" works when every loop runs n times. Plenty of loops don't: the variable doubles, the inner loop starts where the outer one is, the condition tests i * i. For all of these the honest method is one question, asked of each loop in turn: how many times does the body run, as a function of n? The five shapes below cover almost everything you will meet. Each function counts its own body, so the table afterwards is measured, not guessed.

function countDoubling(n) {
  let steps = 0;
  for (let i = 1; i < n; i *= 2) steps++;
  return steps;
}

function countTriangle(n) {
  let steps = 0;
  for (let i = 0; i < n; i++) {
    for (let j = i + 1; j < n; j++) steps++;
  }
  return steps;
}

function countSqrt(n) {
  let steps = 0;
  for (let i = 2; i * i <= n; i++) steps++;
  return steps;
}

function countHarmonic(n) {
  let steps = 0;
  for (let i = 1; i <= n; i++) {
    for (let j = i; j <= n; j += i) steps++;
  }
  return steps;
}

function countNestedDoubling(n) {
  let steps = 0;
  for (let i = 0; i < n; i++) {
    for (let j = 1; j < n; j *= 2) steps++;
  }
  return steps;
}
ncountDoublingcountTrianglecountSqrtcountHarmoniccountNestedDoubling
1044522740
10074,9509482700
1,00010499,500307,06910,000
10,0001449,995,0009993,668140,000

Read the columns against the n that produced them and the shapes appear. Here is where each one comes from.

  • Doubling is logarithmic. i takes the values 1, 2, 4, 8, ..., 2k−1 and stops once 2k reaches n. So the body runs ⌈log₂ n⌉ times: 10 times for n = 1,000, 20 for a million, and only 30 for a billion. Halving (n = Math.floor(n / 2) until it hits 0) is the same loop run backwards, which is exactly why binary search is O(log n).
  • A dependent inner loop is quadratic, with a half in front. When the outer variable is i, the inner loop runs n − 1 − i times, so the total is (n−1) + (n−2) + ... + 1 + 0 = n(n−1)/2. For n = 5 that is 4 + 3 + 2 + 1 + 0 = 10. The ½ is a constant, so the class is still O(n²): halving the work of a quadratic loop does not make it a different shape.
  • A condition on i * i is a square root. i * i <= n is the same test as i <= √n, so the loop runs about √n times. Trial division for primality stops there because if n = a · b with a ≤ b, then a ≤ √n: any factor pair has a member at or below the root. A million costs about a thousand steps.
  • A stride that grows with i is harmonic. The inner loop of countHarmonic runs ⌊n/i⌋ times, so the total is n · (1 + ½ + ⅓ + ... + 1/n) ≈ n · ln n. It is not n² because most values of i are large and their inner loop runs only a few times; for n = 100 the exact count is 482, close to 100 · ln 100 ≈ 460. This is the shape of the sieve of Eratosthenes, though the real sieve only strides from primes, which brings it down to O(n log log n).
  • A linear loop around a logarithmic one multiplies. countNestedDoubling runs n times a loop that runs log n times: O(n log n), and the table shows exactly n × ⌈log₂ n⌉ (1,000 × 10 = 10,000).

One more shape matters a great deal for the next chapters: a loop inside a loop whose inner variable does not reset. This is the engine of every two-pointer and sliding-window solution.

function countWindowMoves(nums, limit) {
  let left = 0, sum = 0, moves = 0;
  for (let right = 0; right < nums.length; right++) {
    sum += nums[right];
    moves++;
    while (sum > limit && left <= right) {
      sum -= nums[left];
      left++;
      moves++;
    }
  }
  return moves;
}

It has a while inside a for, and it is O(n), not O(n²). left never moves backwards and never passes right + 1, so across the whole run it advances at most n times in total. right advances n times. The inner loop does not restart from zero on every outer step, so the two loops share a budget of at most 2n moves instead of multiplying. For countWindowMoves([4, 2, 3, 5, 1], 6) the moves are 5 steps of right plus 3 of left, 8 in all, well under 2n = 10.

⚠ Nested loops do not always mean a product Before multiplying two loops, ask whether the inner loop's variable is reset to the same starting value on every outer step. If it is (let j = 0 inside the outer body), multiply the two counts. If it is shared and only ever moves forward, add them. The mistake runs both ways: calling a pointer loop O(n²) undersells the solution, and calling a real double loop O(n) because "the inner one is small" oversells it.

What built-in calls really cost

Most hidden quadratics come from one line that looks like a single step but is a loop inside the engine. The language specification does not promise any of these costs, and an engine is free to be faster than the table, but this is what to assume in an interview and say out loud. Read the right-hand column as "the work this line is allowed to hide".

CallAssumeWhy
arr[i], arr.length, s[i], s.charCodeAt(i)O(1)one index computation
push, popamortized O(1)the end of the array is the cheap end
shift, unshift, splice(i, k)O(n)elements after the change move over
slice(a, b)O(b − a)it builds a new array, so it copies the piece
concat, [...arr], Array.from(arr), reverse, joinO(n)each one touches every element
includes, indexOf, find, some, everyO(n)a scan that may stop early, worst case the whole array
map, filter, forEach, reduceO(n) × the callbacka callback that itself scans makes the whole call quadratic
sort(compare)O(n log n) comparisonsmultiply by what one comparison costs
Math.max(...arr)O(n)spreading a very large array into arguments can also throw a RangeError
s + t, s.slice(a, b), s.repeat(k)O(length of the result)strings are immutable, so a new one is built
s === tO(length of the shorter)comparison walks until the first difference
s.split(""), [...s]O(n)one new element per character
map.get, map.set, set.has, set.addO(1) averagefor a string key add the O(L) cost of hashing a key of length L
Object.keys, Object.values, Object.entriesO(n)builds an array of every property

Two rows deserve a second look. The callback row is where most accidental quadratics are born: arr.filter((x) => other.includes(x)) is one line and O(n × m), because the callback's includes scans other for every element. And the string-key row is the polite lie of hash maps: a map of n strings, each of length L, costs O(n · L) to fill, not O(n), because every key has to be hashed character by character. When the strings are short that is a constant, and when the interviewer says "each word can be up to 10,000 characters", it is not.

Here is the table at work on a short function. Read it line by line, the way you would out loud.

function lengthsReversed(words) {          // n words, each up to L characters
  const sorted = [...words].sort();         // spread O(n); sort O(n log n) compares, each up to O(L)
  const out = [];
  for (const w of sorted) {                 // n iterations
    out.unshift(w.length);                  // O(n) every time: the hidden loop
  }
  return out;
}
LineCost
[...words]O(n)
.sort()O(n log n) comparisons × O(L) per comparison = O(n · L · log n)
out.unshift(...) in a loop of nn calls × O(n) each = O(n²)
TotalO(n · L · log n + n²), with O(n) extra space

Which term wins depends on the input: with short words the quadratic unshift loop dominates, and with a few very long words the sort does. Naming both terms instead of rounding to one is what shows the interviewer you looked inside the lines. The fix for the loop is to push and reverse once at the end, which is O(n).

Dropping constants and lower-order terms

O(2n) is written as O(n). O(n² + n) is written as O(n²). Big-O describes what dominates as n gets large — the constant factor and the smaller terms stop mattering. This is also why "my solution does 3 passes instead of 1" is still O(n), just with a bigger constant. It's a legitimate follow-up question ("can you get it to one pass?") but it doesn't change the Big-O class.

Recurrence relations — how you actually derive O(n log n)

For recursive code, "count the loops" doesn't work — you need a recurrence relation: an equation describing the work at size n in terms of the work at smaller sizes. Merge sort's recurrence is the classic example:

T(n) = 2·T(n/2) + O(n)
       ↑           ↑
       2 subproblems   the merge step, linear work
       of half the size

Read it as: "the cost of sorting n elements equals the cost of sorting two halves, plus the linear-time work to merge them back together." Solving this (formally, by repeatedly substituting, or informally with the recursion-tree diagram from the sorting chapter — each of log n levels does O(n) total work) gives T(n) = O(n log n).

The Master Theorem — the shortcut for "obvious shape" recurrences For any recurrence of the form T(n) = a·T(n/b) + O(nᵈ) (a subproblems, each of size n/b, plus O(nᵈ) work to combine them), compare d to log_b(a):
  • if d < log_b(a) → T(n) = O(n^(log_b a)) — the recursion dominates
  • if d = log_b(a) → T(n) = O(nᵈ log n) — balanced (this is merge sort: a=2, b=2, d=1, log₂2=1=d)
  • if d > log_b(a) → T(n) = O(nᵈ) — the combine step dominates
AlgorithmRecurrencea, b, dResult
Binary searchT(n) = T(n/2) + O(1)a=1, b=2, d=0O(log n)
Merge sortT(n) = 2T(n/2) + O(n)a=2, b=2, d=1O(n log n)
Binary tree traversalT(n) = 2T(n/2) + O(1)a=2, b=2, d=0O(n)
Naive recursive FibonacciT(n) = T(n-1) + T(n-2) + O(1)doesn't fit the form (n-1 and n-2, not n/b)O(2ⁿ) upper bound (the tight bound is O(φⁿ), φ ≈ 1.618)

You will not be asked to apply the Master Theorem from memory in most interviews — but being able to write down a recurrence for your own recursive solution, and reason informally about "how many levels × how much work per level," is a real and commonly-tested skill.

Memoized recursion: states times work per state

A recurrence like T(n) = T(n−1) + T(n−2) + O(1) counts calls, and it is the right tool for plain recursion. Add a cache and it stops being the right tool, because most of those calls no longer do anything. The count that matters changes to this:

The memoization rule Time = (number of distinct states) × (work done in one state, not counting the recursive calls it makes). Each state's body runs once, because every later call to it is answered from the cache in O(1). Space = the cache (one entry per state) plus the deepest chain of calls.

Watch the rule on Fibonacci, counting both the calls and the bodies that actually run.

function fibNaive(n, stats) {
  stats.calls++;
  if (n < 2) return n;
  return fibNaive(n - 1, stats) + fibNaive(n - 2, stats);
}

function fibMemo(n, stats, memo = new Map()) {
  stats.calls++;
  if (n < 2) return n;
  if (memo.has(n)) return memo.get(n);
  stats.bodies++;
  const value = fibMemo(n - 1, stats, memo) + fibMemo(n - 2, stats, memo);
  memo.set(n, value);
  return value;
}
nfibNaive callsfibMemo callsfibMemo bodies run
625115
10177199
2021,8913919
302,692,5375929

There are n − 1 states that run a body (the values 2 through n), and each body does O(1) work besides its two recursive calls, so the time is O(n). The calls column is about twice the bodies column because every body makes two calls and only the first call into a state does real work. That is the general shape: calls ≤ states × calls per body, so counting bodies loses at most a constant factor.

The rule matters most when the work per state is not constant. Minimum coins to make an amount is the standard example:

function minCoins(coins, amount) {
  const memo = new Map();
  function solve(left) {
    if (left === 0) return 0;
    if (left < 0) return Infinity;
    if (memo.has(left)) return memo.get(left);
    let best = Infinity;
    for (const coin of coins) {
      best = Math.min(best, solve(left - coin) + 1);
    }
    memo.set(left, best);
    return best;
  }
  const result = solve(amount);
  return result === Infinity ? -1 : result;
}
QuantityValueReason
Statesamountleft can be 1, 2, ..., amount; 0 and negatives return immediately
Work per statecoins.lengthone loop over the coin list, plus O(1) cache work
TimeO(amount × coins.length)states × work per state
SpaceO(amount)the cache has one entry per state

With coins = [1, 2, 5] and an amount of 11, 100 or 1,000, the count of bodies run is exactly 11, 100 and 1,000: one per amount, as the table says. Without the cache the same recursion branches three ways at every level and its call tree grows exponentially in the amount.

⚠ Two things the rule does not tell you The stack is still there. Memoization removes repeated work, not depth: minCoins([1], 10000) recurses 10,000 calls deep and throws a RangeError on a default Node 24 stack, even though it has only 10,000 states. A bottom-up DP table has no such limit. The state must be the whole state. If the answer depends on two parameters but the cache key uses one, the cache returns answers computed for a different question, and you get wrong results, not slow ones. The number of states is the product of the ranges of every parameter in the key.

The same rule prices every DP you will write later: a table of r × c cells with O(1) work per cell is O(r · c); a table of n cells where each looks back over all earlier cells is O(n²). See DP in one dimension for the full treatment.

Space complexity — the part people forget

Space complexity counts extra memory your algorithm uses, not counting the input itself. Two things people forget to count:

  • Output that isn't asked for as input — building a new array to return costs O(n) space, even if you never call new Array() explicitly.
  • The call stack. Recursion isn't free — each call frame sits on the stack until it returns. A recursive function that goes n levels deep costs O(n) space even if it allocates nothing else.
a few variables — that's it O(1) space — flat, no matter how big n is frame n frame 3 frame 2 frame 1 O(n) space — one stack frame per call, held until it returns
Same task, two different space profiles — the iterative version never grows a stack; the recursive one grows one frame per call in flight.
// O(n) time, O(1) space — no extra structure grows with input
function maxValue(arr) {
  let max = -Infinity;
  for (const x of arr) if (x > max) max = x;
  return max;
}

// O(n) time, O(n) space — the call stack holds n frames
function sumRecursive(arr, i = 0) {
  if (i === arr.length) return 0;
  return arr[i] + sumRecursive(arr, i + 1);
}

Amortized complexity — the array.push() case

array.push() is described as O(1), but that's an amortized average, not a per-call guarantee. Under the hood a dynamic array is backed by a fixed-size buffer; most pushes are O(1), but occasionally the buffer is full and the engine allocates a new, bigger one and copies everything over — an O(n) operation. Because that expensive copy happens rarely (typically doubling the capacity each time), the average cost per push, spread over many calls, works out to O(1). "Amortized O(1)" means exactly this: not every single call is cheap, but the total cost over many calls divides out to a constant per call.

cap 1 cap 2 cap 4 cap 8 copy! Total copying across n pushes: 1+2+4+8+…+n/2 ≈ n — spread over n pushes, that's O(1) each This is the "aggregate method": sum the TOTAL cost of n operations, then divide by n to get the amortized cost PER operation — not the same as "average case" over random inputs.
Doubling means the sizes form a geometric series — that series summing to roughly n is the entire proof.
⚠ Amortized ≠ average case — a common mix-up Average case is about the distribution of possible inputs (quicksort is fast on average because most inputs don't trigger worst-case pivots). Amortized is about the distribution of cost across a sequence of operations on the same structure, regardless of input — push() is amortized O(1) no matter what values you push, because the guarantee comes from the doubling strategy, not from luck.

The potential method: amortized cost with a bank account

The aggregate method above works when you can add up the whole sequence at once. Many structures are easier to reason about one operation at a time, and for that you use a potential function Φ: a number computed from the structure's current state that stands for "work we have already paid for but not yet done". The amortized cost of an operation is its actual cost plus the change in potential:

amortized = actual + Φ(after) − Φ(before)

Add this up over a sequence of n operations and the Φ terms cancel from end to end, leaving total actual cost = total amortized cost − Φ(final) + Φ(start). So if Φ starts at 0 and never goes negative, the total actual cost is at most the total amortized cost. If every operation has a constant amortized cost, the whole sequence does too.

For the doubling array, pick Φ = 2 · size − capacity. Then the argument has only two cases:

  • A push with room to spare. Actual cost 1. The size grows by one, so Φ rises by 2. Amortized = 1 + 2 = 3.
  • A push into a full array (size = capacity = c). Actual cost c + 1: copy the c elements, then write one. Before the push Φ = 2c − c = c. Afterwards the size is c + 1 and the capacity is 2c, so Φ = 2(c + 1) − 2c = 2. Amortized = (c + 1) + (2 − c) = 3. The big copy is paid for by the potential the cheap pushes stored up.

So every push has amortized cost at most 3, and n pushes cost at most 3n in total. The code below replays the argument by running a model of the array and recording both numbers for every push.

function simulatePushes(n) {
  let size = 0, capacity = 0, actual = 0;
  const amortized = [];
  for (let k = 0; k < n; k++) {
    const before = 2 * size - capacity;
    let cost = 1;
    if (size === capacity) {
      cost += size;
      capacity = Math.max(1, capacity * 2);
    }
    size++;
    actual += cost;
    amortized.push(cost + (2 * size - capacity) - before);
  }
  return { actual, amortized };
}

Dry run: the first six pushes

pushsize, capacity beforeactual costΦ before → afteramortized
10, 0 (full)10 → 12
21, 1 (full)21 → 23
32, 2 (full)32 → 23
43, 412 → 43
54, 4 (full)54 → 23
65, 812 → 43

Total actual cost of the first five pushes is 1 + 2 + 3 + 1 + 5 = 12, against a budget of 3 × 5 = 15. The expensive pushes (3 and 5) are exactly the ones where Φ drops. For n = 1,000 the simulation gives a total actual cost of 2,023 and a largest amortized value of 3. The first push shows 2 rather than 3 only because the model starts from capacity 0.

⚠ The potential has two jobs, and both can be forgotten Φ must start at 0 (or at its minimum) and must never go negative. With Φ = 2 · size − capacity, an array that starts with capacity 1 and size 0 would begin at Φ = −1 and the "total actual ≤ total amortized" step would quietly fail. Also keep the idea separate from average case: Φ is a proof device about a sequence of operations, not a probability over inputs.

You will almost never write a potential function in an interview. What is worth having is the story it tells: every cheap operation pays a little extra into an account, and an occasional expensive one spends the account. That is how to defend "amortized O(1)" for a dynamic array, for the two-stack queue, and for a monotonic stack where each element is pushed once and popped at most once.

Output size, and numbers that are values rather than sizes

Two cases make "count the loops" give an answer that is right on paper and wrong in practice.

The output is part of the work. No algorithm can run faster than the time it takes to write its answer. A function that returns every subset of n numbers must produce 2n arrays whose lengths add up to n · 2n−1, so it is Θ(n · 2n) however cleverly it is written.

function allSubsets(nums) {
  const out = [[]];
  for (const x of nums) {
    const size = out.length;
    for (let i = 0; i < size; i++) out.push([...out[i], x]);
  }
  return out;
}

Counting the work: the loop over nums runs n times, but the inner loop doubles each round (1, 2, 4, ..., 2n−1 subsets), and each [...out[i], x] copies the subset it extends. For n = 10 the result has 1,024 subsets with 5,120 elements in total; for n = 20 it has 1,048,576 subsets and 10,485,760 elements. Say so out loud: "O(n · 2ⁿ), and that is also the size of the output, so it cannot be beaten". The same reasoning prices all permutations at O(n · n!) and all pairs at O(n²).

A number can be an input whose value is the loop bound. An algorithm is pseudo-polynomial when its cost is polynomial in the numeric value of an input but exponential in the number of digits it takes to write that input. The subset-sum check below is the classic case:

function canMakeSum(nums, target) {
  const reachable = new Uint8Array(target + 1);
  reachable[0] = 1;
  for (const x of nums) {
    for (let s = target; s >= x; s--) {
      if (reachable[s - x]) reachable[s] = 1;
    }
  }
  return reachable[target] === 1;
}
targetdigits needed to write itsteps for n numbersmemory
1,0004about n × 1,0001 KB
1,000,0007about n × 1,000,0001 MB
1,000,000,00010about n × 1,000,000,0001 GB

The time is O(n · target) and the space is O(target). It is "polynomial" only if you treat target as the input size. But adding one digit to the target multiplies the work by ten, so in terms of the length of the input it is exponential. The honest interview statement is "O(n · T) time and O(T) space, which is fine because T is at most a few thousand here". Two others you will meet: trial-division primality (for (let i = 2; i * i <= n; i++)) is O(√N) in the value N, about 31,621 steps for N = 1,000,000,007, and the 0/1 knapsack DP is O(n · W) in the capacity W.

Say it like this → "This is O(n · W), where W is the numeric capacity, not the input length. It is pseudo-polynomial: fine for W up to about a million, hopeless if W can be a billion, and in that case I'd look for a different formulation."

The constraints tell you the target complexity

The most useful complexity skill in an interview happens before any code: read the limits on n and work backwards. A comfortable budget to plan for is about 108 simple steps, which is a rule of thumb for choosing an approach, not a measurement. The table lists the largest growth rate that fits under that budget for each size of n, and the kind of solution that usually lives there.

Largest nFitsWork at that nTypical approach
about 10O(n!)10! = 3,628,800try every ordering: permutations, brute-force backtracking
about 20 to 25O(2ⁿ), or O(n · 2ⁿ) near 202²⁰ = 1,048,576 (times 20 for n · 2ⁿ); 2²⁵ = 33,554,432try every subset, bitmask, meet in the middle
about 500O(n³)500³ = 125,000,000three nested loops, interval DP, Floyd–Warshall
about 5,000O(n²)5,000² = 25,000,000all pairs, 2D DP, two nested loops
about 10⁵O(n log n)about 1.7 millionsorting, heaps, binary search per element, balanced trees
about 10⁷O(n)about 10⁷one pass, hash map, two pointers, prefix sums
10⁹ or moreO(log n) or O(1)about 30binary search on the answer, math formula, fast exponentiation

Use it in two directions. Forward: when a problem says n ≤ 105 and your first idea is an O(n²) pair loop (1010 steps), you know before writing a line that the idea is too slow and a hash map, sort, or pointer technique is needed. Backward: when the limit is n ≤ 20 and someone proposes a clever O(n) greedy, the small limit is a hint that the intended solution is an exponential search, and the greedy may be wrong. The sizes in the table also tell you what not to over-engineer: at n ≤ 1,000, an O(n²) loop is 106 steps and a perfectly good answer.

⚠ The table is a starting guess, not a proof Constants matter at the edges: an O(n log n) solution with a heavy comparator, or an O(n²) one that allocates in its inner loop, can fail where the table says it fits. Strings multiply everything by their length (see the string-key row above). And when the statement gives no limits at all, ask for them: it is the single question that most changes your answer.
The habit to build Before you write a line of code in an interview, say the shape out loud: "I'll scan the array once and use a hash map for lookups, so this should be O(n) time, O(n) space." State it, then build toward it — it turns your solution into a plan instead of a guess.
// what's the time complexity of this function? try changing
// the input size in your head before running — does the pattern hold?
function countPairs(arr) {
  let count = 0;
  for (let i = 0; i < arr.length; i++) {
    for (let j = 0; j < arr.length; j++) {
      if (arr[i] + arr[j] === 10) count++;
    }
  }
  return count;
}

console.log(countPairs([1, 9, 2, 8, 3, 7]));

Two nested loops, each running the full length of the array → O(n²) time, O(1) space. Notice it doesn't matter that the inner loop "only" checks a sum — the shape is decided by the loop structure, not what's inside it.

Before you move on
  • Read the time and space complexity off a function by counting loops and allocations, not lines of code.
  • Spot a hidden O(n) call (includes, indexOf, spreading a Set) sitting inside a loop, and explain why that makes the whole function O(n²).
  • Explain the difference between O, Ω and Θ, and why interviews almost always mean "worst case" when they say Big-O.
  • Derive O(n log n) from a recurrence like T(n) = 2T(n/2) + O(n), at least at the level of "why the log n shows up."
  • State a solution's complexity out loud before writing it, as a plan rather than an afterthought.
  • Work out how many times a loop body runs when the variable doubles, starts at the outer index, tests i * i, or strides by i, and tell a pointer loop that shares a budget from a nested loop that multiplies.
  • Price the built-in calls in a loop (slice, spread, sort, string keys) instead of treating each as one step.
  • Cost a memoized recursion as states times work per state, and say why a cache does not shrink the call stack.
  • Say why an output of size 2ⁿ forces the time, and what "pseudo-polynomial" means for a loop bounded by a value.
  • Turn a limit such as n ≤ 10⁵ into the complexity you are allowed to use before writing any code.

07Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 8 testsClassify a Function's Growth Ratebeginner
  • 8 testsRewrite a Nested Loop as a Linear Scanbeginner
  • 9 testsCount the Comparisons Exactlybeginner
  • 8 testsHow Many Times Does the Body Run?beginner
  • 7 testsNaive Fibonacci: Calls and Stack Depthbeginner
  • 8 testsSolve the Recurrencebeginner
  • 7 testsThe True Cost of n Pushesbeginner
All 217 for this level →

✓Check yourself

5 questions, pass at 4. The check runs in your browser and the result stays on this device, so it is a self-check and not a certificate.

B1

Finished reading?

0 of 48 chapters read in this series

Mark as read

→Next stop

  1. Back toLevels and plan
  2. B1you are here
  3. Next · B2The JavaScript toolkit for DSAA correct algorithm can still fail in JavaScript: a slow shift, a sort that puts 10 before 2, a product that loses digits, a stack that runs out. This is the reference for all of it.42 min read

01On this page

19 sections · 0%

  1. 01What Big-O actually measures
  2. 02The formal definition — worth seeing once
  3. 03Big-O's two siblings: Ω and Θ
  4. 04Best, average, and worst case — three different questions
  5. 05Multiple inputs — when there isn't just one "n"
  6. 06The picture that makes it click
  7. 07Why the shape matters more than it seems — actual numbers
  8. 08The complexities you'll actually see
  9. 09Reading complexity out of code
  10. 10Loops that don't count up by one
  11. 11What built-in calls really cost
  12. 12Dropping constants and lower-order terms
  13. 13Recurrence relations — how you actually derive O(n log n)
  14. 14Memoized recursion: states times work per state
  15. 15Space complexity — the part people forget
  16. 16Amortized complexity — the array.push() case
  17. 17The potential method: amortized cost with a bank account
  18. 18Output size, and numbers that are values rather than sizes
  19. 19The constraints tell you the target complexity
Mark as read

[] chapters · t top