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 AlgorithmsTLDR: 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:
- What Big O actually measures (hint: not milliseconds)
- The 7 complexity classes every engineer must know
- A 4-step process to derive complexity from any code
- Space complexity and the hidden cost of recursion
- The data structure complexity table every interviewer expects you to have memorised
- 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.
| Case | Meaning | Example: Linear Search |
| Best case | Most favourable input | Target is at index 0 → O(1) |
| Average case | Expected over random inputs | Target at middle on average → O(n/2) → O(n) |
| Worst case | Most unfavourable input | Target 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 Class | n = 1 | n = 10 | n = 100 | n = 1,000 | n = 1,000,000 |
| O(1) | 1 | 1 | 1 | 1 | 1 |
| O(log n) | 0 | 3 | 7 | 10 | 20 |
| O(n) | 1 | 10 | 100 | 1,000 | 1,000,000 |
| O(n log n) | 0 | 33 | 665 | 10,000 | 20,000,000 |
| O(n²) | 1 | 100 | 10,000 | 1,000,000 | 10¹² |
| O(2ⁿ) | 2 | 1,024 | 10³⁰ | ∞ (heat death) | ∞ |
| O(n!) | 1 | 3,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 Structure | Access | Search | Insert | Delete | Space |
| Array | O(1) | O(n) | O(n) | O(n) | O(n) |
| ArrayList | O(1) | O(n) | O(1) amortised* | O(n) | O(n) |
| LinkedList | O(n) | O(n) | O(1) at head/tail | O(1) at known node | O(n) |
| HashMap | O(1) avg | O(1) avg | O(1) avg | O(1) avg | O(n) |
| HashSet | — | O(1) avg | O(1) avg | O(1) avg | O(n) |
| Stack | O(n) | O(n) | O(1) push | O(1) pop | O(n) |
| Queue | O(n) | O(n) | O(1) enqueue | O(1) dequeue | O(n) |
| BST (balanced) | O(log n) | O(log n) | O(log n) | O(log n) | O(n) |
| Heap (min/max) | O(1) peek | O(n) | O(log n) | O(log n) | O(n) |
| TreeMap | O(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:
HashMapworst 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.LinkedListinsert is only O(1) if you already hold a reference to the insertion point. Finding that point is O(n).Heapgives 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 Pattern | Time Complexity | Space Complexity | Canonical Example |
| Single loop, fixed work per iteration | O(n) | O(1) | Linear scan, prefix sum |
| Two independent sequential loops | O(n) | O(1) | Two-pass algorithms |
| Two nested loops, both over n | O(n²) | O(1) | Bubble sort, naive duplicate check |
| Loop + binary search inside | O(n log n) | O(1) | Search for each element in sorted array |
| Divide input in half each call | O(log n) | O(log n) recursive / O(1) iterative | Binary search |
| Divide + merge (recursion + O(n) merge) | O(n log n) | O(n) | Merge sort |
| Two recursive branches per call | O(2ⁿ) | O(n) call stack | Naive Fibonacci, subset enumeration |
| HashMap/HashSet frequency counting | O(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 other | O(n) | O(1) | Pair sum in sorted array, palindrome check |
| DFS / BFS on a graph with V vertices, E edges | O(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
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.
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ⁿ.
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.
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²).
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.
🔗 Related Posts
- The Ultimate Data Structures Cheat Sheet — complete complexity tables for every Java collection with decision criteria for when to use each
- Binary Search Patterns — O(log n) in action: five variants of binary search with worked Java solutions
- Sliding Window — the O(n) pattern that eliminates redundant inner loops in subarray and substring problems
- Two Pointer Technique — O(n) time, O(1) space: the pair-problem pattern that makes nested loops obsolete

Written by
Abstract Algorithms
@abstractalgorithms
More Posts
The Dual Write Problem: Why Two Writes Always Fail Eventually — and How to Fix It
TLDR: Any service that writes to a database and publishes a message in the same logical operation has a dual write problem. try/catch retries don't fix it — they turn failures into duplicates. The Transactional Outbox pattern co-writes business data ...
ID Generation Strategies in System Design: Base62, UUID, Snowflake, and Beyond
TLDR: Short shareable IDs need Base62 (URL shorteners). Database primary keys at scale need time-ordered IDs (Snowflake, UUID v7). Security tokens need random IDs (UUID v4, NanoID). Picking the wrong strategy either causes B-tree fragmentation at 50M...
Probabilistic Data Structures Explained: Bloom Filters, HyperLogLog, and Count-Min Sketch
TLDR: Probabilistic data structures — Bloom Filters, Count-Min Sketch, HyperLogLog, and Cuckoo Filters — trade a small, bounded probability of being wrong for orders-of-magnitude better memory efficiency and O(1) speed. Bloom filters answer "definite...
