Skip to the notes
JSGroundwork
JSGroundwork handwritten · web dev
✎Playground→⌘Problems↻Review🔥Progress

Chapters

34 chapters
⌕
Beginner10›
B1Complexity analysisB2Arrays & stringsB3HashingB4Two pointersB5Sliding windowB6Binary searchB7Sorting algorithmsB8Stacks & queuesB9Linked listsB10Basic recursion
Intermediate12›
I1TreesI2Tree problems in depthI3Heaps & priority queuesI4Graphs: representationI5Graph problemsI6BacktrackingI7DP: 1DI8DP: 2DI9Greedy algorithmsI10IntervalsI11Bit manipulationI12Matrix problems
Advanced12›
A1Advanced DPA2Union-FindA3Advanced graph algorithmsA4Minimum Spanning TreeA5TriesA6Segment & Fenwick treesA7String algorithmsA8Monotonic stack & queueA9Design problemsA10Advanced backtrackingA11Topological patternsA12Interview strategy
/ search[ ] chaptert top

DSA in JS levels

1Beginner2Intermediate3Advanced

Ready to read

JSJavaScript⑂Git◎Interview prepΣDSA in JSSDSystem Design
More topics15›
</>HTML{ }CSS⚛ReactNNext.jsNeNest.jsTSTypeScriptNoNode.js🐳DockerDBSQL & Databases✓Testing🔒Web Security☁Cloud & DevOps◈GraphQL◆Redis☸Kubernetes
100%
B1

Complexity analysis

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

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ⁿ) both explode — see the table below for real numbers
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 + hi) >> 1;
    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;
}

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) = 2T(n-1) + O(1)doesn't fit the form (n-1, not n/b)O(2ⁿ)

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.

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 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.

Practice this layer

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

Classify a Function's Growth Rate5 tests · beginnerRewrite a Nested Loop as a Linear Scan5 tests · beginnerCount the Comparisons Exactly4 tests · beginner
↑back toThe cover↑ CovernextArrays & strings→