Big-O Notation: A Practical Guide for Working Developers

July 13, 2026 · 10 min read

Every data structures conversation eventually lands on Big-O, and most explanations either drown you in math or hand-wave it entirely. The truth sits in the middle: Big-O is a growth rate — a way of answering "if I double the input, what happens to the work?" — and you can usually read it straight off the shape of the code.

What Big-O measures (and what it ignores)

Big-O describes how running time (or memory) scales with input size, ignoring constant factors and lower-order terms. 3n² + 40n + 7 is just O(n²) — because once n is large, the n² term dwarfs everything else.

That "ignores constants" part is a feature, not sloppiness. Constants depend on the machine, the language, and the compiler. Growth rate doesn't. An O(n) algorithm on a slow laptop will eventually beat an O(n²) algorithm on a supercomputer — the only question is how big n has to get.

It also means Big-O is not a stopwatch. For small inputs, an O(n²) algorithm with tiny constants can absolutely outrun an O(n log n) one with heavy setup — which is exactly why real sort implementations switch to insertion sort for small slices.

The ladder, from best to worst

ComplexityNamen = 1,000Typical example
O(1)Constant1Hash map lookup, array index
O(log n)Logarithmic~10Binary search
O(n)Linear1,000Single loop, linear scan
O(n log n)Linearithmic~10,000Merge sort, heap sort
O(n²)Quadratic1,000,000Nested loops over same input
O(2ⁿ)ExponentialastronomicalNaive recursion on subsets
O(n!)Factorialdon'tBrute-force permutations

The gap between rows is the whole story. At n = 1,000, quadratic does a million operations where linearithmic does ten thousand. At n = 1,000,000, quadratic is a trillion — that's the difference between milliseconds and hours.

Reading complexity off the code

You rarely need formal analysis. A few patterns cover almost everything you will write or review.

A loop over the input is O(n):

function sum(arr) {
  let total = 0;
  for (const x of arr) total += x; // n iterations
  return total;
}

A nested loop over the same input is O(n²):

function hasDuplicate(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; // n·(n−1)/2 comparisons worst case
    }
  }
  return false;
}

Halving the input each step is O(log n):

function binarySearch(arr, target) {
  let lo = 0, hi = arr.length - 1;
  while (lo <= hi) {
    const mid = (lo + hi) >> 1;
    if (arr[mid] === target) return mid;
    arr[mid] < target ? (lo = mid + 1) : (hi = mid - 1); // half remains
  }
  return -1;
}

Doing O(log n) work n times is O(n log n) — that's merge sort: log n levels of splitting, O(n) merging per level.

Sequential steps add, nested steps multiply. A loop followed by another loop is O(n + n) = O(n). A loop inside another loop is O(n · n) = O(n²). This one distinction resolves most "wait, is this quadratic?" questions in code review.

The hidden loops

The complexity bugs that actually ship to production are rarely explicit nested for loops. They're a method call inside a loop that is secretly linear itself:

// Looks linear. Is quadratic.
function findMissing(all, seen) {
  return all.filter((id) => !seen.includes(id)); // includes() is O(n)!
}

// Actually linear: build a Set once, O(1) lookups after.
function findMissingFast(all, seen) {
  const seenSet = new Set(seen);
  return all.filter((id) => !seenSet.has(id));
}

Array.prototype.includes, indexOf, find, and splice are all O(n). Inside a loop, each one silently multiplies your complexity by n. The same trap hides in string concatenation in a loop (each += can copy the whole string) and in unshift (shifts every element). When something is "fine in dev, dead in prod", the input grew and a hidden O(n²) woke up — this is the first thing to look for.

The same shape appears outside your code. A loop that runs one database query per item — the "N+1 query" problem — is O(n) round trips, and at a few milliseconds each, 1,000 items is several seconds of waiting. Replacing it with one query that fetches all the rows (WHERE id = ANY($1)) turns n network round trips into one. Big-O thinking applies to I/O as much as to CPU; the unit of work just got much more expensive.

Space complexity: the same idea, for memory

Every rule above applies to memory too. A function that builds an output array sized to the input is O(n) space; one that uses a few variables is O(1); recursion holds one stack frame per level, so a recursive descent n levels deep is O(n) space even if it allocates nothing.

Time and space often trade against each other. The duplicate-finder above went from O(n²) time / O(1) space to O(n) time / O(n) space — we bought speed with memory. Most classic optimizations (hash maps, memoization, prefix sums) are exactly this trade.

Best, worst, and average case

Big-O usually quotes the worst case, but the cases can differ wildly. Quick sort is O(n log n) on average but O(n²) on adversarial input. Hash map lookup is O(1) on average but O(n) if every key collides. When someone says "hash maps are O(1)", they mean expected — and for interviews and design docs it's worth saying which case you're quoting.

Amortized analysis covers the other subtlety: a dynamic array's push is usually O(1) but occasionally O(n) when it resizes. Averaged over a long sequence of pushes, each one costs O(1) — the expensive resizes are rare enough that they "amortize" away. That's why we still call push constant time.

Recursion: count the calls, then the work per call

Loops are easy to read; recursion needs one more step. Two questions cover most cases: how many calls happen in total, and how much work does each call do outside its recursive calls?

function fib(n) {                // each call makes two more
  if (n < 2) return n;
  return fib(n - 1) + fib(n - 2);
}

The call tree branches twice per level and is about n levels deep, so the number of calls grows exponentially — roughly O(2ⁿ) (more precisely about 1.6ⁿ). Each call does O(1) work, so that's the total. Memoising it — caching each fib(k) the first time it's computed — means each value is computed once: n calls of O(1) work, O(n) total. That one change is the whole idea of dynamic programming.

Divide-and-conquer recursion has a recognisable shape too. Merge sort splits the input in half (two calls on n/2) and does O(n) merging work per call:

T(n) = 2·T(n/2) + O(n)   →   O(n log n)
T(n) = 1·T(n/2) + O(1)   →   O(log n)       (binary search)
T(n) = 2·T(n/2) + O(1)   →   O(n)           (visiting every node of a balanced tree)
T(n) = 1·T(n−1) + O(n)   →   O(n²)          (selection sort, written recursively)

The intuition: draw the recursion tree, add up the work on each level, and multiply by the number of levels. When every level does the same total work (merge sort: n per level, log n levels), multiply. When the work shrinks fast enough down the tree, the top level dominates. The formal version is the master theorem, but the picture is enough for almost every interview. Space for recursion is the maximum depth of the tree — see recursion and the call stack.

Reading the required complexity from the constraints

Competitive programming problems and interview questions usually state input limits, and those limits tell you which complexity the intended solution has. A typical machine does very roughly 10⁸ simple operations a second, so:

if n is at most…aim fortypical technique
10–12O(n!) or O(2ⁿ · n)brute force over permutations, backtracking
20–25O(2ⁿ)subsets, bitmask DP
500O(n³)triple loops, interval DP
5,000O(n²)nested loops, 2D DP
10⁵ – 10⁶O(n log n) or O(n)sorting, heaps, two pointers, hashing
10⁹ and upO(log n) or O(1)binary search on the answer, maths

So "n ≤ 10⁵" quietly says "your O(n²) idea will time out — find something with a sort, a hash map or two pointers". Reading the constraints before designing saves a lot of wasted effort.

From brute force to optimal: one problem, three complexities

Problem: given an array and a target, return the indices of two numbers that add up to the target.

Brute force, O(n²) time, O(1) space — try every pair:

function twoSumBrute(nums, target) {
  for (let i = 0; i < nums.length; i++)
    for (let j = i + 1; j < nums.length; j++)
      if (nums[i] + nums[j] === target) return [i, j];
  return null;
}

Sort + two pointers, O(n log n) time — sort (keeping original indices), then move pointers inward: too small, move left up; too big, move right down. See two pointers.

Hash map, O(n) time, O(n) space — for each number, ask whether its complement has been seen:

function twoSum(nums, target) {
  const seen = new Map();               // value → index
  for (let i = 0; i < nums.length; i++) {
    const need = target - nums[i];
    if (seen.has(need)) return [seen.get(need), i];
    seen.set(nums[i], i);
  }
  return null;
}

The progression is the standard interview arc: state the brute force and its cost, identify the repeated work (the inner loop searches for the same kind of thing every time), and remove it with a better data structure — a hash map, at the price of O(n) memory.

What common JavaScript operations cost

operationcost
arr[i], arr.push(x), arr.pop()O(1) (push is amortised)
arr.shift(), arr.unshift(x)O(n) — every element moves
arr.includes / indexOf / findO(n)
arr.slice, [...arr], arr.concatO(n) — a copy
arr.splice(i, k)O(n) in general
arr.sort()O(n log n)
map.get / set / has, set.has / addO(1) average
Object.keys(obj)O(number of keys)
str += piece in a loopoften fine in modern engines; join an array if unsure
JSON.parse / stringifyO(size of the data)

shift is the one that surprises people: using an array as a queue with push and shift makes a BFS over a large graph quadratic. Use a head index or a real queue instead — see stacks and queues.

O, Θ and Ω

Strictly, Big-O is an upper bound: O(n²) means "grows no faster than n²", so a linear algorithm is technically also O(n²). Θ (theta) is a tight bound — grows exactly like — and Ω (omega) is a lower bound. Everyday usage says "O" and means "Θ", and that's fine in practice. The distinction matters in one common sentence: "comparison sorting is Ω(n log n)" means no comparison sort can do better, which is a much stronger claim than any one algorithm's O.

The base of a logarithm doesn't matter either: log₂ n and log₁₀ n differ by a constant factor, and constants are dropped. That's why it's just "O(log n)". Exponent bases do matter, though: O(2ⁿ) and O(3ⁿ) are genuinely different.

Practice

Work out the time and space complexity of each before reading the post:

Wrap-up

Big-O answers one question: what happens to the work when the input grows? Loops add when sequential and multiply when nested; halving is logarithmic; a linear helper inside a loop makes the loop quadratic — and that hidden case is where real-world slowness usually lives. You don't need the math background; you need the habit of glancing at every loop and asking "what's the cost of the thing inside it?" — and, before you write a line, checking the input limits to see which answer you need.