All Posts

Big O Notation Explained: Time Complexity, Space Complexity, and Why They Matter in Interviews

Master Big O notation: measure algorithm efficiency from O(1) to O(n²) with growth diagrams, worked examples, and the interview mental model.

Abstract AlgorithmsAbstract Algorithms
··32 min read
Share
Share on X / Twitter
Share on LinkedIn
Copy link

TLDR: Big O notation describes how an algorithm's resource usage grows as input size grows — not how fast it runs on your laptop. Learn to identify the 7 complexity classes (O(1) through O(n!)), derive time and space complexity by counting loops and stack frames, and communicate your analysis in three sentences that satisfy any FAANG interviewer.


📖 Why Your Correct Solution Might Still Get Rejected

Picture this: a candidate walks into a FAANG loop interview, gets a duplicate-detection problem on a 10-element array, and writes a perfectly working solution. Two nested for loops, checking every pair. Output is correct. Test cases pass. The interviewer nods and asks one question: "What's the time complexity of this solution?"

The candidate stares at the code. "It works… is that not enough?"

It is not enough — and that single answer ends their chance at the offer.

The interviewer was not testing whether the code compiles. They were testing whether the candidate understands why the same solution that processes 10 records in milliseconds would take 28 hours to process 10 million records. At FAANG scale, an O(n²) solution on a 100-million-row user table is not a slow program. It is an outage.

Big O notation is the language engineers use to reason about scale before writing a single line of production code. It is not optional trivia — it is the core mental model that separates engineers who build systems that survive growth from those who build systems that collapse under it.

This post teaches you:

  1. What Big O actually measures (hint: not milliseconds)
  2. The 7 complexity classes every engineer must know
  3. A 4-step process to derive complexity from any code
  4. Space complexity and the hidden cost of recursion
  5. The data structure complexity table every interviewer expects you to have memorised
  6. A 3-sentence formula for communicating complexity under pressure

By the end, you will be able to look at any loop, any recursive call, any data structure operation and state its complexity — with justification — in under 30 seconds.


🔍 Big O From First Principles: The Language of Scale

Before any formal definition, think about a library. Imagine you need to find a specific book. If the library is sorted alphabetically, you can open to the middle, decide which half your book is in, and repeat — halving the search space each time. That is O(log n). If the library is completely unsorted and you must check every shelf, that is O(n). If you need to compare every book against every other book for some reason, that is O(n²).

Big O notation formalises this intuition: it is the vocabulary engineers use to describe how the work changes as the library grows. Not "it took 3 seconds" — but "if the library doubles in size, the work doubles too" (O(n)) or "if the library doubles, the work increases by only one extra comparison" (O(log n)).

Three things Big O is not:

  • It is not a measurement of actual runtime in milliseconds. Two O(n) algorithms can differ by 10× in wall-clock speed.
  • It is not an exact count of operations. Constants and low-order terms are deliberately dropped.
  • It is not always about the worst case — but worst case is the default in interviews unless stated otherwise.

Three things Big O is:

  • A growth family — algorithms cluster into classes (constant, logarithmic, linear…) that behave fundamentally differently at scale.
  • An upper bound — O(f(n)) means "the algorithm does no more than a constant multiple of f(n) operations for large n."
  • A communication tool — when you say "my solution is O(n log n)", every engineer in the room immediately understands the scaling story.

With this mental model in place, the next section shows you the three algebraic rules that make Big O practical to derive.


📊 What Big O Actually Measures: Growth Rate, Not Clock Time

A common misconception: Big O measures how long your code takes to run. It does not. Two computers running the same O(n²) algorithm will show different millisecond times, but both exhibit the same shape of growth. Double the input, quadruple the work. That shape — the growth rate — is what Big O captures.

The formal definition (plain English): Big O notation describes an upper bound on how resource usage (operations, memory) scales as the input size n grows toward infinity. We write O(f(n)) where f(n) is the dominant growth term.

The Three Rules That Make Big O Usable

Rule 1 — Drop constants. O(2n) and O(5n) are both just O(n). Why? Because as n grows toward a million, the constant multiplier becomes irrelevant compared to the exponent. A 2× faster computer running an O(n²) algorithm will still be slower than a slower computer running an O(n log n) algorithm at large enough n.

Rule 2 — Drop lower-order terms. O(n² + n + 50) simplifies to O(n²). The dominant term swallows everything else at scale. At n=1,000, n² is 1,000,000 while n is only 1,000 — a 1,000× difference.

Rule 3 — Big O is worst-case by default. Unless explicitly told otherwise, interviewers mean worst-case when they ask "what's the complexity?" A linear search finding the target in position 0 (O(1) best case) is irrelevant when your interviewer wants to know what happens when the target is at position n−1.

CaseMeaningExample: Linear Search
Best caseMost favourable inputTarget is at index 0 → O(1)
Average caseExpected over random inputsTarget at middle on average → O(n/2) → O(n)
Worst caseMost unfavourable inputTarget at last position or absent → O(n)

The "Big O is about the shape of the curve" mental model: O(1) is a flat horizontal line. O(n) is a straight diagonal. O(n²) is a parabola curving steeply upward. O(2ⁿ) is a near-vertical cliff. Optimising an algorithm means flattening that curve.


🔢 The 7 Complexity Classes, Ranked from Best to Worst

There are seven complexity families that cover the vast majority of algorithms you will encounter. The diagram below shows them ordered from most efficient (flat) to least efficient (explosive).

The following flowchart reads left to right, from best to worst. Each node represents a complexity class. The arrow direction means "this is worse than." Use this as a mental map: when you derive the complexity of a solution, you want to be as far left as possible.

graph LR
    A["🟢 O(1)\nConstant"] --> B["🟢 O(log n)\nLogarithmic"]
    B --> C["🟡 O(n)\nLinear"]
    C --> D["🟡 O(n log n)\nLinearithmic"]
    D --> E["🔴 O(n²)\nQuadratic"]
    E --> F["🔴 O(2ⁿ)\nExponential"]
    F --> G["💀 O(n!)\nFactorial"]

    style A fill:#16a34a,color:#fff,stroke:#14532d
    style B fill:#65a30d,color:#fff,stroke:#3f6212
    style C fill:#ca8a04,color:#fff,stroke:#713f12
    style D fill:#ea580c,color:#fff,stroke:#7c2d12
    style E fill:#dc2626,color:#fff,stroke:#7f1d1d
    style F fill:#b91c1c,color:#fff,stroke:#450a0a
    style G fill:#7f1d1d,color:#fff,stroke:#3b0764

To make these growth rates tangible, here is how many operations each class requires for different input sizes. Notice how exponential classes become practically unusable long before the others:

Complexity Classn = 1n = 10n = 100n = 1,000n = 1,000,000
O(1)11111
O(log n)0371020
O(n)1101001,0001,000,000
O(n log n)03366510,00020,000,000
O(n²)110010,0001,000,00010¹²
O(2ⁿ)21,02410³⁰∞ (heat death)
O(n!)13,628,800

O(1) — Constant Time: The Dream

The algorithm always takes the same number of operations regardless of input size. Accessing an array by index (arr[5]) is O(1) — the CPU computes the memory address directly. A HashMap.get(key) is O(1) average — the hash function maps the key to a bucket in one step.

// O(1) — reading the first element, no matter how long the array is
public int getFirst(int[] arr) {
    return arr[0];   // single memory lookup — n doesn't matter
}

// O(1) average — HashMap lookup via hash function
public String getUsername(Map<Integer, String> users, int id) {
    return users.get(id);  // one hash computation, one bucket access
}

O(log n) — Logarithmic Time: Halving the Problem

Each operation cuts the remaining problem in half. Binary search on a sorted array of 1 million elements takes only 20 comparisons — because log₂(1,000,000) ≈ 20. Balanced binary search tree (BST) operations (insert, delete, lookup) are O(log n) because the tree height is log₂(n).

// O(log n) — each iteration halves the search space
public boolean contains(int[] sortedArr, int target) {
    int left = 0, right = sortedArr.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;  // avoids integer overflow
        if (sortedArr[mid] == target) return true;
        else if (sortedArr[mid] < target) left = mid + 1;   // discard left half
        else right = mid - 1;                               // discard right half
    }
    return false;
}

O(n) — Linear Time: One Pass Through the Data

The work grows directly proportional to input size. A single loop that visits each element once is the canonical O(n) pattern. ArrayList.contains(), linear search, and computing an array sum are all O(n).

// O(n) — visits every element exactly once
public int findMax(int[] arr) {
    int max = arr[0];           // O(1) initialisation
    for (int x : arr) {         // executes n times
        if (x > max) max = x;   // O(1) per iteration
    }
    return max;                 // O(1)
    // Total: O(1) + O(n) × O(1) + O(1) = O(n)
}

O(n log n) — Linearithmic Time: Sorting Sweet Spot

This is the complexity of efficient general-purpose sorting algorithms: Merge Sort, Heap Sort, and the TimSort algorithm that powers Arrays.sort() and Collections.sort() in Java. You pay O(n) to touch every element and O(log n) for the recursive depth — combined, O(n log n). This is the best achievable complexity for comparison-based sorting.

// O(n log n) — Java's built-in sort uses TimSort (hybrid of Merge Sort + Insertion Sort)
int[] arr = {5, 2, 8, 1, 9};
Arrays.sort(arr);  // O(n log n) guaranteed

List<Integer> list = Arrays.asList(3, 1, 4, 1, 5);
Collections.sort(list);  // also O(n log n)

O(n²) — Quadratic Time: Nested Loops Are a Warning Sign

Two nested loops, each iterating over n elements, produce n × n = n² total operations. Bubble sort, selection sort, and naive duplicate detection all fall here. Acceptable for small inputs (n < 1,000); dangerous for large ones.

// O(n²) — the "does any pair sum to target?" brute force
public boolean twoSum(int[] arr, int target) {
    for (int i = 0; i < arr.length; i++) {           // n iterations
        for (int j = i + 1; j < arr.length; j++) {   // up to n iterations per outer loop
            if (arr[i] + arr[j] == target) return true;
        }
    }
    return false;
    // Total: n × (n-1)/2 comparisons ≈ O(n²)
}

This is precisely the solution the FAANG candidate wrote at the start of this post. Correct for 10 elements. Catastrophic for 10 million.

O(2ⁿ) — Exponential Time: Recursive Trees Without Memoisation

Every recursive call branches into 2 sub-calls, and each of those branches into 2 more. The number of calls doubles with every additional input element. Classic naive recursive Fibonacci is O(2ⁿ): fib(50) requires over a trillion function calls.

// O(2ⁿ) — each call spawns two more calls; total calls ≈ 2^n
public int fib(int n) {
    if (n <= 1) return n;             // base case
    return fib(n - 1) + fib(n - 2);  // two recursive calls — doubles work each level
}
// fib(5)  → ~30 calls
// fib(10) → ~177 calls
// fib(50) → ~2.25 trillion calls — never use this in production

O(n!) — Factorial Time: Permutation Exhaustion

Generating all permutations of n elements requires n! operations. For n=10 that is 3.6 million. For n=20 it is 2.4 quintillion. Used in brute-force solutions to the Travelling Salesman Problem. Never acceptable at scale; mentioned in interviews as the worst case to recognise and avoid.


⚙️ How to Derive Time Complexity in 4 Steps

Deriving complexity is a mechanical process once you know the rules. Apply these four steps in order to any piece of code.

The flowchart below maps out the exact decision process. Read it top-down: start by counting your loops, check whether they are nested, check whether the input halves, and arrive at your complexity class. Follow this chart on every solution you write until it becomes automatic.

flowchart TD
    A(["🔍 Start: examine your code"]) --> B{"How many loops\nover n elements?"}
    B -->|"Zero loops"| C["✅ O(1) — Constant\nNo input traversal"]
    B -->|"One loop"| D{"Does each iteration\nhalve the remaining\nproblem?"}
    D -->|"Yes — e.g. binary search"| E["✅ O(log n) — Logarithmic"]
    D -->|"No — full traversal"| F{"Does the loop body\nmake a recursive call\nthat also halves?"}
    F -->|"Yes — e.g. merge sort"| G["✅ O(n log n) — Linearithmic"]
    F -->|"No"| H["✅ O(n) — Linear"]
    B -->|"Two nested loops"| I{"Do both loops\niterate over n?"}
    I -->|"Yes"| J["⚠️ O(n²) — Quadratic"]
    I -->|"Inner loop is\nconstant size k"| K["✅ O(n·k) → simplify to O(n)"]
    B -->|"Recursive branching\nfactor of 2 at each call"| L["🚨 O(2ⁿ) — Exponential\nAdd memoisation to fix"]

    style C fill:#16a34a,color:#fff
    style E fill:#65a30d,color:#fff
    style G fill:#ca8a04,color:#fff
    style H fill:#ca8a04,color:#fff
    style J fill:#dc2626,color:#fff
    style K fill:#ca8a04,color:#fff
    style L fill:#7f1d1d,color:#fff

Step 1 — Count the loops. Each independent loop over n elements contributes a factor of n. No loops means O(1).

Step 2 — Identify nesting. Loops nested inside other loops multiply. An outer O(n) loop containing an inner O(n) loop gives O(n²). An outer O(n) loop containing an inner O(log n) loop gives O(n log n).

Step 3 — Check for halving. If a loop or recursive call reduces the remaining problem by half each time, add a log n factor.

Step 4 — Drop constants and lower-order terms. O(3n + 2 log n + 7) simplifies to O(n). Take the dominant term, strip its coefficient.

The next section applies these four rules to three concrete interview-style problems — annotating every line so you can see exactly which rule each code construct maps to.


🧪 Three Classic Interview Problems Solved with Complexity Derivation

These three examples cover the three most common complexity patterns in coding interviews: a single-pass scan, a nested brute-force search (and its HashSet upgrade), and the halving pattern of binary search. Every line is annotated to show the direct mapping to the 4 derivation rules above. Work through each derivation yourself before reading the answer — that active effort is how the pattern becomes automatic.

Problem 1 — Find the Maximum Element in an Unsorted Array

We need the largest value in an array with no ordering guarantees. There is no shortcut: the maximum could be the last element, so we must inspect all n positions. This is the canonical O(n) single-pass scan.

public int findMax(int[] arr) {
    int max = arr[0];           // ← O(1): one assignment — constant regardless of n
    for (int x : arr) {         // ← Step 1: one loop; runs n times
        if (x > max) max = x;   // ← O(1) per iteration: one comparison, one assignment
    }
    return max;                 // ← O(1): one return
}
// Derivation: O(1) + n × O(1) + O(1)
// = O(1) + O(n) + O(1) = O(n)     ← Step 4: drop the constants
// Time: O(n)   Space: O(1) — only `max` allocated

Step 1: one loop. Step 2: not nested. Step 3: does not halve. Step 4: no constants to drop → O(n) time, O(1) space.

Problem 2 — Does Any Pair Sum to Target? (Brute Force → Optimised)

This problem has two solutions that demonstrate the most important trade-off in algorithm design: spending more space to save time. The brute-force uses two nested loops; the optimised version uses a HashSet to eliminate the inner loop entirely. This is the exact O(n²) → O(n) upgrade interviewers expect you to propose.

// ❌ O(n²) — check every pair: the brute-force approach
public boolean twoSumBrute(int[] arr, int target) {
    for (int i = 0; i < arr.length; i++) {          // ← Step 1: outer loop — n iterations
        for (int j = i + 1; j < arr.length; j++) {  // ← Step 2: nested loop — n iterations
            if (arr[i] + arr[j] == target) return true;
        }
    }
    return false;
}
// Derivation: outer runs n times; inner runs n-1, n-2, ..., 1 times
// Total: n(n-1)/2 ≈ n²/2 → Step 4: drop constant → O(n²)
// Time: O(n²)   Space: O(1)

// ✅ O(n) — the HashSet upgrade: one pass, O(1) average lookup
public boolean twoSumOptimal(int[] arr, int target) {
    Set<Integer> seen = new HashSet<>();   // ← allocated once: O(n) space worst case
    for (int x : arr) {                   // ← Step 1: one loop — n iterations
        if (seen.contains(target - x)) return true;   // ← O(1) avg: replaces inner loop
        seen.add(x);                                   // ← O(1) avg: insert into HashSet
    }
    return false;
}
// Time: O(n) — one loop × O(1) per iteration
// Space: O(n) — the HashSet can hold up to n distinct elements
// Trade-off one-liner: "O(n) time at the cost of O(n) space — I replaced the inner loop
//                       with a HashSet lookup."

This is exactly the solution the candidate in our opening story wrote — and why it fails at 10 million elements. The O(n) upgrade is the answer the interviewer was waiting to hear.

Problem 3 — Binary Search on a Sorted Array

The sorted array gives us information we can exploit: if the middle element is smaller than our target, the target must be in the right half. Each comparison discards half the remaining candidates. A sorted array of one billion elements takes only 30 iterations — because log₂(1,000,000,000) ≈ 30.

public int binarySearch(int[] arr, int target) {
    int left = 0, right = arr.length - 1;
    while (left <= right) {               // ← Step 3: halves each iteration → log₂(n) times
        int mid = left + (right - left) / 2;  // avoids integer overflow vs (left+right)/2
        if (arr[mid] == target) return mid;
        else if (arr[mid] < target) left = mid + 1;   // ← discard entire left half
        else right = mid - 1;                         // ← discard entire right half
    }
    return -1;
}
// After each iteration: search space n → n/2 → n/4 → ... → 1
// Iterations until 1 element remains: log₂(n)
// Time: O(log n)   Space: O(1) — only `left`, `right`, `mid` allocated

Step 1: one loop. Step 2: not nested. Step 3: does halve → O(log n) time, O(1) space.


🗄️ Space Complexity: The Resource Interviewers Forget to Ask About

Time complexity gets all the attention, but space complexity is the other axis of the efficiency conversation — and interviewers frequently dock points for candidates who only address time.

Definition: Space complexity measures how much extra memory an algorithm allocates as n grows. We ignore the input array itself (it already exists) and count only the auxiliary space — additional data structures, variables, and stack frames the algorithm creates.

The question "Can you do it in O(1) extra space?" is a signal that the interviewer wants an in-place solution: no extra arrays, no hash maps, just a fixed number of pointer variables.

The Call Stack Is Hidden Memory

Every recursive function call pushes a new stack frame onto the call stack — local variables, return address, and parameters. The deeper the recursion, the more memory consumed. This is the most common space complexity trap for beginners: a recursive solution that looks like it uses "no memory" actually consumes O(depth) stack space.

The diagram below shows three algorithms side by side, comparing how many stack frames each consumes. Notice how iterative binary search uses a constant single frame while recursive Fibonacci's deepest call path grows proportional to n.

graph TD
    subgraph IT["Iterative Binary Search — O(1) extra space"]
        I1["One stack frame\n— while loop runs in place\n— no new frames added"]
    end

    subgraph RB["Recursive Binary Search (n=1024) — O(log n) stack space"]
        R1["binarySearch(arr, 0, 1023)"] --> R2["binarySearch(arr, 0, 511)"]
        R2 --> R3["binarySearch(arr, 0, 255)"]
        R3 --> R4["binarySearch(arr, 0, 127)"]
        R4 --> R5["... 10 frames total\n(log₂ 1024 = 10)"]
    end

    subgraph RF["Recursive fib(5) — O(n) stack depth on longest path"]
        F1["fib(5)"] --> F2["fib(4)"]
        F2 --> F3["fib(3)"]
        F3 --> F4["fib(2)"]
        F4 --> F5["fib(1) ← base case\n5 frames deep"]
    end

This diagram illustrates a crucial insight: two algorithms can both be called "recursive" yet have radically different space costs. Recursive binary search only goes log n frames deep because it halves the problem. Naive recursive Fibonacci goes n frames deep on the left-most call path.

Space Complexity Examples at a Glance

O(1) extra space — in-place array reversal with two pointers:

public void reverseInPlace(int[] arr) {
    int left = 0, right = arr.length - 1;
    while (left < right) {
        int temp = arr[left];   // 3 variables total — O(1) regardless of n
        arr[left] = arr[right];
        arr[right] = temp;
        left++;
        right--;
    }
}
// Space: O(1) — only `left`, `right`, `temp` — three integers, constant regardless of n

O(n) extra space — using a HashSet to find duplicates:

public boolean hasDuplicate(int[] arr) {
    Set<Integer> seen = new HashSet<>();  // can grow to n elements → O(n) space
    for (int x : arr) {
        if (!seen.add(x)) return true;   // add() returns false if already present
    }
    return false;
}
// Time: O(n)   Space: O(n) — the HashSet is the additional allocation

O(log n) space — recursive binary search (stack frames):

public int binarySearchRecursive(int[] arr, int left, int right, int target) {
    if (left > right) return -1;
    int mid = left + (right - left) / 2;
    if (arr[mid] == target) return mid;
    else if (arr[mid] < target) return binarySearchRecursive(arr, mid + 1, right, target);
    else return binarySearchRecursive(arr, left, mid - 1, target);
}
// Space: O(log n) — each recursive call halves the range,
//        so the call stack grows to at most log₂(n) frames

The interviewer asking "can you do it in O(1) space?" is asking whether you can rewrite the algorithm to avoid the HashSet (or any other growing allocation) — usually by sorting first or using the original array itself as a marker.


📋 Data Structure Complexities You Must Memorise

Every data structure question in an interview implicitly tests whether you know the cost of the operations you're calling. When you choose a LinkedList over an ArrayList — or a TreeMap over a HashMap — the interviewer expects a complexity justification.

Data StructureAccessSearchInsertDeleteSpace
ArrayO(1)O(n)O(n)O(n)O(n)
ArrayListO(1)O(n)O(1) amortised*O(n)O(n)
LinkedListO(n)O(n)O(1) at head/tailO(1) at known nodeO(n)
HashMapO(1) avgO(1) avgO(1) avgO(1) avgO(n)
HashSetO(1) avgO(1) avgO(1) avgO(n)
StackO(n)O(n)O(1) pushO(1) popO(n)
QueueO(n)O(n)O(1) enqueueO(1) dequeueO(n)
BST (balanced)O(log n)O(log n)O(log n)O(log n)O(n)
Heap (min/max)O(1) peekO(n)O(log n)O(log n)O(n)
TreeMapO(log n)O(log n)O(log n)O(log n)O(n)

* Amortised O(1) for ArrayList.add(): most additions are O(1). Occasionally the backing array doubles in size (O(n) copy), but averaged across n additions the cost per addition is O(1).

Three facts from this table that trip up candidates:

  • HashMap worst case is O(n) — when every key hashes to the same bucket (hash collision storm). Average and expected case is O(1). Good hash functions make this rare.
  • LinkedList insert is only O(1) if you already hold a reference to the insertion point. Finding that point is O(n).
  • Heap gives you O(1) access to the top element only. Searching for any other element is O(n).

🎤 How to Communicate Complexity in an Interview

Knowing the complexity is half the battle. Communicating it clearly under pressure is the other half. Interviewers consistently report that candidates who derive the correct answer but explain it poorly score lower than candidates who use a structured, confident three-sentence formula.

The 3-Sentence Formula

Sentence 1 — State the complexity:

"This solution runs in O(n) time and uses O(1) extra space."

Sentence 2 — Justify it:

"The single for-loop visits each of the n elements exactly once. We use two pointer variables, which are a constant amount of memory regardless of input size."

Sentence 3 — Name the trade-off (if one exists):

"We could achieve O(1) time for lookups by pre-loading into a HashMap, but that would cost O(n) extra space. The current solution is the better choice when memory is constrained."

Not every solution has a trade-off. If it does not, skip sentence 3. If the interviewer asks "can you do better?", then introduce the trade-off — do not volunteer it unprompted if your solution is already optimal for the stated constraints.

Common Interview Pitfalls That Cost Offers

1. Forgetting the recursion stack. When asked about space complexity of a recursive DFS over a graph with n nodes, the answer includes the O(n) call stack depth — not just any explicit data structures you allocate.

2. Conflating amortised O(1) with guaranteed O(1). ArrayList.add() is amortised O(1). In a real-time system where every millisecond matters, the occasional O(n) resize spike may be unacceptable. Know the distinction.

3. Saying "O(n²) but it's fast in practice" without data. This line is a red flag to interviewers. Saying "fast in practice" without quantifying n, the constant factor, or a benchmark is hand-waving. Back it up or don't say it.

4. Ignoring HashMap's worst case. For most purposes HashMap is O(1), but an interviewer testing security-conscious code design may ask: "What if an adversary crafts inputs to force worst-case hash collisions?" The answer is O(n) worst case, and the mitigation is using a LinkedHashMap with randomised hashing or a TreeMap (O(log n) guaranteed).

5. Stopping at time complexity. Always volunteer space complexity even when not asked. It signals that you think about the full resource picture — and it differentiates you from candidates who only know the time axis.


🧭 Quick Pattern Recognition Cheatsheet

Once you have seen enough problems, you stop needing to trace through each one step-by-step. You recognise the structural pattern and immediately know the complexity. Use this table as a rapid-lookup during interview prep.

Code PatternTime ComplexitySpace ComplexityCanonical Example
Single loop, fixed work per iterationO(n)O(1)Linear scan, prefix sum
Two independent sequential loopsO(n)O(1)Two-pass algorithms
Two nested loops, both over nO(n²)O(1)Bubble sort, naive duplicate check
Loop + binary search insideO(n log n)O(1)Search for each element in sorted array
Divide input in half each callO(log n)O(log n) recursive / O(1) iterativeBinary search
Divide + merge (recursion + O(n) merge)O(n log n)O(n)Merge sort
Two recursive branches per callO(2ⁿ)O(n) call stackNaive Fibonacci, subset enumeration
HashMap/HashSet frequency countingO(n)O(n)Character frequency, anagram check
Sliding window (fixed or variable)O(n)O(1) or O(k)Max-sum subarray, longest unique substring
Two pointers moving toward each otherO(n)O(1)Pair sum in sorted array, palindrome check
DFS / BFS on a graph with V vertices, E edgesO(V + E)O(V)Connected components, shortest path

The last row is a bonus for graph problems: the complexity is O(V + E) not O(n²) — because BFS/DFS visits each vertex once and each edge once. This is a common complexity that beginners quote incorrectly as O(n²) because they confuse a graph traversal with a nested loop.


🌍 Where Big O Decisions Shape Real Production Systems

Big O is not an academic exercise — it is a daily engineering decision. Every time a senior engineer chooses a data structure, writes a query, or designs an API, they are implicitly reasoning about complexity. Here are three real scenarios where the choice between complexity classes has production consequences.

Google's Search Index: O(log n) vs O(n) at Planetary Scale

Google's inverted index maps every word on the web to the documents that contain it. The index has billions of entries. When you search for "big o notation", Google does not scan all entries linearly — that would be O(n) against billions of records, taking hours. Instead, it uses a B-tree structure that retrieves the entry for "big" and "notation" in O(log n) operations each. The difference between O(log n) and O(n) at a billion-record scale is the difference between a 30-millisecond response and a 30-minute one.

The lesson: For any lookup against a large dataset, O(log n) (sorted + binary search or B-tree) vs O(n) (linear scan) is the difference between a product that works and one that does not.

Database Query Optimisers: O(1) Index Hit vs O(n) Full Table Scan

A SQL query without an index on the WHERE clause column forces a full table scan: O(n) where n is the number of rows. With a B-tree index, the database descends the tree in O(log n) to find the matching rows. Database administrators talk about "cardinality" and "selectivity" — but they are fundamentally reasoning about whether an O(log n) index seek is cheaper than an O(n) scan given the data distribution.

Amazon's DynamoDB is designed entirely around O(1) key-value lookups: every read and write on the primary key is O(1) regardless of table size. The trade-off is that queries that don't use the primary key require a full table scan (O(n)) — which is why DynamoDB's cost model encourages pre-computing access patterns at schema design time.

Rate Limiters: O(1) with HashMap vs O(n) with a List

A rate limiter that needs to check "has this user ID exceeded 100 requests in the last minute?" has two naive options:

  • Store all requests in a list and count matching user IDs: O(n) per check, where n is the total number of requests in the window.
  • Store request counts in a HashMap keyed by user ID: O(1) per check regardless of total request volume.

At 100,000 requests per second, the O(1) HashMap check is non-negotiable. This is why every production rate limiter — from Nginx's limit_req to Stripe's API gateway — uses hash-based storage. Big O reasoning made this an obvious engineering choice long before any benchmarking was needed.


🛠️ Java Collections Framework: Complexity Guarantees Baked In

The Java Collections Framework (JCF) is the standard library you use every day in interviews. What makes it powerful is that every collection's time complexity is documented in the Javadoc — meaning you can reach for the right data structure with confidence.

What the JCF is: A unified architecture for representing and manipulating groups of objects. It defines interfaces (List, Set, Map, Queue, Deque) and multiple concrete implementations, each with different performance characteristics.

How it implements complexity guarantees: Each concrete class has a documented performance contract. HashMap uses an array of buckets with linked-list chaining (or tree nodes for large buckets since Java 8), guaranteeing O(1) average-case operations. TreeMap uses a Red-Black tree, guaranteeing O(log n) for all operations. PriorityQueue uses a binary heap array, giving O(log n) insert/delete and O(1) peek.

// Choosing the right collection for the right job — with complexity justification

// 1. O(1) amortised insert + O(n) random access → ArrayList
List<Integer> list = new ArrayList<>();
list.add(42);          // O(1) amortised — append to backing array
int val = list.get(5); // O(1) — direct index into array

// 2. O(1) average lookup/insert → HashMap
Map<String, Integer> freq = new HashMap<>();
freq.put("apple", 1);            // O(1) average — hash to bucket
int count = freq.getOrDefault("apple", 0); // O(1) average

// 3. O(log n) sorted iteration → TreeMap (backed by Red-Black tree)
Map<Integer, String> sorted = new TreeMap<>();
sorted.put(3, "three");  // O(log n) — maintains sorted order
sorted.put(1, "one");    // O(log n)
// iterating TreeMap visits keys in sorted order — O(n log n) total

// 4. O(log n) min extraction → PriorityQueue (min-heap by default)
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.offer(5);  // O(log n) — sifts up to maintain heap property
minHeap.poll();    // O(log n) — removes min, sifts down to restore heap
minHeap.peek();    // O(1) — reads min without removing

// 5. O(1) push/pop → ArrayDeque as Stack or Queue
Deque<Integer> stack = new ArrayDeque<>();
stack.push(10);   // O(1) — add to front of deque
stack.pop();      // O(1) — remove from front

When choosing a data structure in an interview, state the complexity contract explicitly: "I'm using a HashMap here because I need O(1) average-case lookups. If the interviewer needs sorted order, I'd switch to a TreeMap at the cost of O(log n) per operation."

For a deeper look at each data structure implementation and when to reach for each one, see the companion post on The Ultimate Data Structures Cheat Sheet.


📚 Lessons Learned: The Five Mistakes Beginners Make with Big O

1. Counting lines of code instead of operations. A 3-line function with a hidden Collections.sort() call is O(n log n), not O(1). Always look into library calls and understand their cost.

2. Forgetting the hidden O(n) cost of string concatenation in Java. In Java, String s = s + x inside a loop is O(n²) because each concatenation creates a new String object copying all previous characters. Use StringBuilder.append() (O(1) amortised) instead.

// ❌ O(n²) — each + creates a new String, copying all previous characters
String result = "";
for (String word : words) {
    result = result + word;  // hidden O(n) copy per iteration → O(n²) total
}

// ✅ O(n) — StringBuilder appends to a mutable char buffer
StringBuilder sb = new StringBuilder();
for (String word : words) {
    sb.append(word);   // O(1) amortised append
}
String result2 = sb.toString();  // O(n) final copy — paid once

3. Treating all O(n) algorithms as equally fast. Two O(n) algorithms can have a 10× real-world performance difference because of cache locality, branch prediction, and constant factors. Big O tells you the shape — not the absolute speed. An O(n) array scan is far faster in practice than an O(n) linked list scan due to CPU cache lines.

4. Calling a DFS "O(n²)" because of the nested structure. Graph traversal is O(V + E), not O(V²), because each edge is visited exactly once across the entire traversal — not once per vertex. The nested appearance is misleading.

5. Skipping the space analysis entirely. Every answer about a recursive solution must mention the O(depth) call stack space. Every answer about a HashSet solution must mention the O(n) space. Interviewers notice when space complexity is absent from the analysis.


📌 TLDR

  • Big O = growth rate, not clock time. Drop constants and lower-order terms. O(2n + 5) is O(n).
  • Worst case is the default. Unless told otherwise, always analyse worst case.
  • The 7 classes to know: O(1) → O(log n) → O(n) → O(n log n) → O(n²) → O(2ⁿ) → O(n!).
  • Derive complexity in 4 steps: count loops → check nesting → check halving → drop constants.
  • Space complexity counts auxiliary memory, not input. Recursive algorithms carry O(depth) hidden stack cost.
  • Reach for HashMap first for O(1) average lookups; use TreeMap when you need sorted order at O(log n).
  • The 3-sentence interview formula: state complexity → justify it → name the trade-off.
  • The O(n²) → O(n) upgrade almost always involves trading space for time: add a HashSet or HashMap to eliminate the inner loop.

📝 Practice Quiz

  1. What is the time complexity of the following method?

    public int sum(int[] arr) {
        int total = 0;
        for (int x : arr) total += x;
        return total;
    }
    
    • A) O(1) — it's just addition
    • B) O(n) — the loop visits each element once
    • C) O(n²) — there is a loop and an assignment Correct Answer: B — A single loop over all n elements with O(1) work per iteration gives O(n). The assignment inside the loop does not add a complexity dimension; it is still one operation per element.
  2. A recursive method calls itself twice per invocation and stops when n reaches 0. What is its time complexity?

    • A) O(n) — it's just a recursive method
    • B) O(n log n) — it's recursive like merge sort
    • C) O(2ⁿ) — branching factor of 2 at each level gives exponential growth Correct Answer: C — When a recursive function branches into 2 sub-calls at each level with no input halving, the call tree doubles at every level. With n levels, total calls = 2ⁿ.
  3. You solve a "find duplicates" problem by storing all elements in a HashSet. What is the space complexity?

    • A) O(1) — HashSet operations are O(1)
    • B) O(log n) — hash functions involve logarithms
    • C) O(n) — the HashSet can hold up to n distinct elements Correct Answer: C — Space complexity asks how much extra memory grows with n. The HashSet allocates one slot per unique element, so in the worst case (all elements unique) it holds n entries → O(n) auxiliary space.
  4. The following code has two loops. What is the overall time complexity?

    for (int i = 0; i < n; i++) { doWork(i); }         // Loop A
    for (int j = 0; j < n; j++) { doOtherWork(j); }    // Loop B
    
    • A) O(n²) — there are two loops
    • B) O(n) — the loops run sequentially, not nested
    • C) O(2n) — each loop runs n times Correct Answer: B — Sequential (non-nested) loops add their complexities: O(n) + O(n) = O(2n) → drop the constant → O(n). Only nested loops multiply: O(n) × O(n) = O(n²).
  5. You have an O(n²) solution that works correctly. The interviewer asks "can you improve it?" What is the most common technique for reducing a nested-loop duplicate-check from O(n²) to O(n)?

    • A) Use a recursive approach — recursion is always faster
    • B) Sort the array first — sorting reduces all problems to O(n log n)
    • C) Trade space for time — store seen elements in a HashSet for O(1) average lookups, eliminating the inner loop Correct Answer: C — The canonical O(n²) → O(n) upgrade for duplicate/pair problems replaces the inner loop with a HashSet lookup. One outer loop (O(n)) × O(1) HashSet lookup = O(n) total, at the cost of O(n) additional space.


Abstract Algorithms

Written by

Abstract Algorithms

@abstractalgorithms