Foundations

Big O Complexity

How to read it, how to derive it from code, and how Java's built-in collections perform across every operation you care about.

Foundations Β· Array Β· Linked List Β· Stack Β· Queue Β· HashMap Β· Binary Tree Β· BST Β· Heap Β· Trie Β· Graph Β· Sorting Β· Binary Search Β· Two Pointers Β· Sliding Window Β· Dynamic Prog.
01
Section One Β· Foundation

What Big O Measures

input: n algorithm f(n) ops O(f(n)) growth rate β€” not absolute time

Big O notation describes the growth rate of an algorithm’s resource usage β€” time or space β€” as a function of its input size n. It does not measure wall-clock time in milliseconds, because that depends on hardware, language, and compiler. It measures the rate of growth: if you double the input, does the algorithm take twice as long, four times as long, or the same amount of time? That growth rate is what determines whether an algorithm scales or breaks under real-world load.

Big O describes the upper bound β€” the worst the algorithm can do on any input of size n. This is deliberately pessimistic: an algorithm that runs in O(n) time might finish much faster on a sorted input, but the guarantee is that it will never be worse than linear. When comparing algorithms, upper bounds are more useful than averages because they tell you the worst case you will ever experience in production.

If I double the input size
β–Ά
Does the runtime double (O(n)), stay the same (O(1)), or quadruple (O(nΒ²))?
If I have 1 million records
β–Ά
Will this algorithm finish in milliseconds, minutes, or never?
If I choose this data structure
β–Ά
Which operations become cheap and which become expensive?
Constants and lower-order terms are dropped:
  • O(3n + 100) simplifies to O(n). O(nΒ² + n) simplifies to O(nΒ²).
  • Big O cares only about the dominant term as n grows large β€” constants and smaller terms become irrelevant at scale.
02
Section Two Β· Notation

Reading Big O Notation

O(1) Constant
The operation takes the same number of steps regardless of input size.
Java
// O(1) β€” index formula, no loop int first = array[0]; // always 1 operation map.get("key"); // hash lookup β€” 1 operation deque.peekFirst(); // head pointer read β€” 1 operation
O(1) does not mean fast:
  • O(1) means the time does not grow with input size β€” it could still be slow in absolute terms.
  • A HashMap.get() is O(1) but performs a hash computation, an array index, and possibly an equals() check β€” it is always the same cost regardless of map size.
O(log n) Logarithmic
The problem is halved at each step, so doubling the input adds only one more step.
Java
// O(log n) β€” input halved each iteration int lo = 0, hi = arr.length - 1; while (lo <= hi) { int mid = lo + (hi - lo) / 2; // avoids integer overflow if (arr[mid] == target) return mid; if (arr[mid] < target) lo = mid + 1; else hi = mid - 1; } // Binary search β€” 1M elements needs only ~20 comparisons
logβ‚‚(1,000,000) β‰ˆ 20:
  • A sorted array of one million elements needs at most 20 comparisons with binary search.
  • That is why O(log n) is called “essentially constant” at practical scales β€” TreeMap.get() on a map with a billion keys still finishes in ~30 comparisons.
O(n) Linear
The number of steps grows in direct proportion to the input size.
Java
// O(n) β€” visits every element once for (int x : list) { sum += x; // one operation per element } list.contains(target); // O(n) β€” scans from index 0 Collections.reverse(list);// O(n) β€” touches every element
O(n) is the best you can do for unsorted data:
  • If the data is unsorted and you need to find an element, you must look at every element in the worst case β€” O(n) is optimal.
  • Any claim of faster-than-linear search on unsorted data requires either preprocessing (sorting, hashing) or a structural guarantee.
O(n log n) Linearithmic
Appears in efficient sorting algorithms β€” slightly worse than linear but dramatically better than quadratic.
Java
// O(n log n) β€” divide and conquer sorting Collections.sort(list); // merge sort internally Arrays.sort(objectArray); // tim sort internally Arrays.sort(primitiveArray); // dual-pivot quicksort β€” O(n log n) avg
O(n log n) is the lower bound for comparison sorting:
  • Any algorithm that sorts by comparing elements cannot do better than O(n log n) β€” this is a mathematical proof, not a limitation of existing implementations.
  • The only way to sort faster is to use non-comparison methods like counting sort or radix sort, which require constraints on the input values.
O(nΒ²) Quadratic
A nested loop where each element is compared to every other element β€” doubles the input, quadruples the runtime.
Java
// O(nΒ²) β€” nested loops, n Γ— n iterations for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { // nΒ² total operations if (arr[i] + arr[j] == target) ... } } // 1,000 elements β†’ 1,000,000 operations // 10,000 elements β†’ 100,000,000 operations
O(nΒ²) is the threshold β€” avoid it for n > 10,000:
  • At n=10,000 a quadratic algorithm performs 100 million operations.
  • At n=100,000 it performs 10 billion β€” seconds become minutes.
  • Whenever you see a nested loop over the same collection, ask whether a HashMap can reduce the inner loop to O(1).
O(2ⁿ) Exponential
Each additional input element doubles the total work β€” grows faster than any polynomial, unusable for n > 30.
Java
// O(2ⁿ) β€” naive recursive fibonacci int fib(int n) { if (n <= 1) return n; return fib(n - 1) + fib(n - 2); // each call spawns two more } // fib(30) β†’ ~1 billion calls // fib(50) β†’ ~1 quadrillion calls β€” never finishes
Memoization converts O(2ⁿ) to O(n):
  • Storing previously computed results eliminates redundant recursive calls. fib(n) with a cache (dynamic programming) runs in O(n) time and O(n) space β€” the same answer, exponentially faster.
  • Recognising exponential growth is the first step to knowing when dynamic programming applies.
Complexity at Scale
Notation Name n=10 n=100 n=1,000 n=1,000,000
O(1) Constant 1 1 1 1
O(log n) Logarithmic 3 7 10 20
O(n) Linear 10 100 1,000 1,000,000
O(n log n) Linearithmic 33 664 9,966 19,931,568
O(nΒ²) Quadratic 100 10,000 1,000,000 10ΒΉΒ² (unusable)
O(2ⁿ) Exponential 1,024 10³⁰ (∞) ∞ ∞
═══════════════════════════════════════════════════════════════════════ -->
03
Section Three Β· Derivation

Deriving Complexity From Code

Reading Big O from existing analysis is one skill; deriving it from code you are looking at is another. The rules are mechanical β€” you do not need to count every operation, only identify the dominant pattern. Four patterns cover 95% of what you will encounter.

Rule 1 β€” Single Loop
A loop that runs n times contributes O(n) to the overall complexity.
Java
// Each loop body runs exactly n times β†’ O(n) for (int i = 0; i < n; i++) { process(arr[i]); // O(1) work per iteration } // Total: n Γ— O(1) = O(n) // Regardless of what is inside β€” as long as it is O(1): for (String s : list) { map.put(s, map.getOrDefault(s, 0) + 1); // O(1) HashMap ops } // Still O(n) β€” the loop count dominates
The loop body must be O(1):
  • If the loop body itself is O(n) β€” for example, calling list.contains() inside a for loop β€” the total becomes O(n) Γ— O(n) = O(nΒ²).
  • Always analyse the body before multiplying.
Rule 2 β€” Nested Loops
Two nested loops over the same collection multiply their iterations β€” n Γ— n = nΒ².
Java
// Outer loop: n iterations for (int i = 0; i < n; i++) { // Inner loop: n iterations for each outer β†’ n Γ— n total for (int j = 0; j < n; j++) { process(arr[i], arr[j]); // O(nΒ²) total } } // Different sizes: O(n Γ— m) for (String a : listA) { // n iterations for (String b : listB) { // m iterations if (a.equals(b)) ... // O(n Γ— m) total } }
HashMap breaks the inner loop:
  • If the inner loop is searching for a match, a HashMap built in O(n) reduces the inner search to O(1), converting the entire algorithm from O(nΒ²) to O(n).
  • This is the most common optimisation pattern in array interview problems.
Rule 3 β€” Input Halved Each Step
When the active portion of the input is halved at each step, the total number of steps is proportional to logβ‚‚(n).
Java
// Input halved each iteration β†’ O(log n) int lo = 0, hi = n - 1; while (lo <= hi) { // runs logβ‚‚(n) times int mid = lo + (hi - lo) / 2; if (check(mid)) hi = mid - 1; else lo = mid + 1; } // Tree traversal with height h = log n (balanced tree): TreeNode cur = root; while (cur != null) { // h = log n levels if (target < cur.val) cur = cur.left; else if (target > cur.val) cur = cur.right; else return cur; } // O(log n) β€” each step eliminates half the remaining nodes
Halving is the signature of O(log n):
  • Any time you see a loop where the search space is cut in half β€” whether by lo/hi pointers, a balanced tree, or a divide-and-conquer recursion β€” the loop runs at most logβ‚‚(n) times.
  • Identify the shrinking variable to confirm.
Rule 4 β€” Recursive Calls
A recursive function’s complexity is the work per call multiplied by the total number of calls.
Java
// Each call does O(1) work and makes 1 recursive call β†’ O(n) int sum(int[] arr, int i) { if (i == arr.length) return 0; // base case return arr[i] + sum(arr, i + 1); // 1 call, O(1) work β†’ O(n) } // Each call does O(1) work and makes 2 recursive calls β†’ O(2ⁿ) int fib(int n) { if (n <= 1) return n; return fib(n-1) + fib(n-2); // 2 calls β†’ exponential tree } // Divide and conquer: 2 calls on n/2 input + O(n) merge β†’ O(n log n) void mergeSort(int[] arr, int lo, int hi) { if (lo >= hi) return; int mid = lo + (hi - lo) / 2; mergeSort(arr, lo, mid); // T(n/2) mergeSort(arr, mid+1, hi); // T(n/2) merge(arr, lo, mid, hi); // O(n) } // T(n) = 2T(n/2) + O(n) β†’ O(n log n) by Master Theorem
Draw the recursion tree to count calls:
  • For any recursive function, draw a tree where each node is one call and each branch is a recursive invocation.
  • The total number of nodes is the total number of calls.
  • Multiply by the work per call (excluding recursion) to get the total complexity.
The 3-step derivation process
Identify loops and recursion Find the dominant term drop constants + lower terms State the Big O class O( ) Apply to any algorithm β€” 4 rules cover 95% of cases
═══════════════════════════════════════════════════════════════════════ -->
04
Section Four Β· Cases

Best, Average, and Worst Case

Big O typically describes worst-case β€” the maximum number of steps on any input of size n. But algorithms can behave very differently depending on the input, so three cases matter: best, average, and worst. Understanding all three prevents false confidence from benchmarks on lucky inputs.

Best Case β€” Ξ© (Omega)

The ideal input

The input that makes the algorithm finish as fast as possible. Useful as a lower bound but rarely the number you plan for.

  • Linear search finds target at index 0 β†’ Ξ©(1)
  • QuickSort with perfect median pivot β†’ Ξ©(n log n)
Worst Case β€” O (Big O)

The hardest input

The input that forces the maximum number of steps. This is what Big O describes. Plan for this β€” production systems encounter worst-case inputs regularly.

  • Linear search: target not in array β†’ O(n)
  • QuickSort: always picking smallest pivot β†’ O(nΒ²)
Average Case β€” Θ (Theta)

The expected input

The expected behaviour over all possible inputs of size n, weighted by probability. Harder to derive but most representative of real-world performance.

  • HashMap lookup: O(1) average, O(n) worst (all keys collide)
  • QuickSort: O(n log n) average with random pivot selection
Linear Search β€” three cases on the same array
Best Case β€” Ξ©(1) search(3) β€” found at index 0 3 7 1 9 4 2 8 5 1 comparison Average β€” Θ(n/2) search(9) β€” found at index 3 3 7 1 9 4 2 8 5 4 comparisons Worst Case β€” O(n) search(99) β€” not found 3 7 1 9 4 2 8 5 8 comparisons
Common Mistake: Assuming QuickSort is always O(n log n) because that is what the textbook says. QuickSort degrades to O(nΒ²) when the pivot is consistently the smallest or largest element β€” exactly what happens on sorted or reverse-sorted input with a naive first-element pivot. Java’s Arrays.sort() for primitives uses dual-pivot QuickSort with safeguards; for objects it uses TimSort, which is O(n log n) worst case.
═══════════════════════════════════════════════════════════════════════ -->
05
Section Five Β· Space

Space Complexity

Time complexity measures how runtime grows with input size β€” space complexity measures how memory usage grows. The two are often in tension: using extra memory (a HashMap, a cache, a second array) frequently reduces time complexity, and the trade-off is worth making explicit. When someone says an algorithm runs in O(1) space, they mean it uses a constant amount of extra memory regardless of input size β€” not that it uses zero memory.

Input space (the data itself)
β–Ά
Not counted in space complexity β€” it is already there
Auxiliary space (extra memory the algorithm allocates)
β–Ά
This is what space complexity measures
Recursive call stack
β–Ά
Each frame uses memory β€” O(n) recursion depth = O(n) space
O(1) space (“in-place”)
β–Ά
Algorithm uses only a fixed number of extra variables β€” no arrays, no maps, no stack frames proportional to n
Space Complexity Examples
Algorithm Time Space Notes
Array reversal (in-place) O(n) O(1) Swap with two pointers
Array reversal (new array) O(n) O(n) Allocates n-element copy
Recursive fibonacci O(2ⁿ) O(n) n stack frames at deepest path
Memoized fibonacci O(n) O(n) Cache stores n results
Iterative fibonacci O(n) O(1) Two variables, no cache
BFS on a graph O(V+E) O(V) Queue holds up to V nodes
Merge sort O(n log n) O(n) Merge step needs temp array
QuickSort (in-place) O(n log n) O(log n) Stack frames = tree height
The time-space trade-off is always explicit:
  • When you add a HashMap to reduce O(nΒ²) to O(n), you are trading O(n) space for O(n) time savings.
  • Name the trade-off when you make it β€” in code comments, in interviews, and in design discussions.
═══════════════════════════════════════════════════════════════════════ -->
06
Section Six Β· Amortized

Amortized Analysis

Amortized analysis describes the average cost of an operation over a sequence of operations β€” not the worst single case, but the cost spread across many calls. It matters because some operations are occasionally expensive but cheaply distributed over time. ArrayList’s add() is the canonical example: most appends are O(1), but one in every n appends triggers a resize that costs O(n) β€” yet the average cost per append over n operations is still O(1).

Start with capacity 1. Each time the array fills, double it and copy all elements. Track the total copy operations across 16 appends.

ArrayList Resize Walkthrough
Append # Capacity before Resize? Copy cost Running total
1 1 No 0 0
2 1 Yes β†’ 2 1 1
3 2 Yes β†’ 4 2 3
4 4 No 0 3
5 4 Yes β†’ 8 4 7
6–8 8 No 0 7
9 8 Yes β†’ 16 8 15
10–16 16 No 0 15
Average copy cost per append: 15/16 β‰ˆ 1 β†’ O(1) amortized
Geometric series β€” resize cost halves each time we look back
1 copy 2 copies 4 copies 8 copies 16 copies Total copies = 1+2+4+8+... < 2n Geometric series: resize cost halves each time we look back β†’ total copies bounded by 2n β†’ amortized O(1) per append
Amortized β‰  average case:
  • Amortized O(1) means the total cost of n operations is O(n), so each individual operation costs O(1) on average β€” but some individual calls may be O(n).
  • Average case is about input distribution; amortized is about spreading one expensive operation across many cheap ones.
═══════════════════════════════════════════════════════════════════════ -->
07
Section Seven Β· Reference

Java Collections Cheatsheet

Every Java collection has a defined complexity for its core operations β€” knowing these cold is the difference between a confident interview answer and an uncertain guess. This is the reference to bookmark: the complete complexity profile of every collection you will reach for.

List Implementations
Operation ArrayList LinkedList Notes
get(index) O(1) O(n) LinkedList must traverse from head/tail
add() at end O(1)* O(1) *ArrayList amortized; LinkedList has tail ref
add(i, val) O(n) O(n) ArrayList: shift; LinkedList: traverse to i
remove(index) O(n) O(n) ArrayList: shift; LinkedList: traverse to i
contains() O(n) O(n) Linear scan in both
size() O(1) O(1) Both store as field
iterator.next() O(1) O(1) ArrayList: index++; LinkedList: pointer follow

* LinkedList.get(i) is O(n) β€” never call it in a loop

Queue / Deque / Stack
Operation ArrayDeque LinkedList PriorityQueue Notes
addFirst / push O(1) O(1) β€” ArrayDeque: array index
addLast / offer O(1) O(1) O(log n) PQ: heap sift-up
removeFirst / pop O(1) O(1) O(log n) PQ: heap sift-down
peekFirst O(1) O(1) O(1) PQ: root is always min/max
size O(1) O(1) O(1) All store as field
Map Implementations
Operation HashMap LinkedHashMap TreeMap Notes
get(key) O(1)* O(1)* O(log n) *avg β€” O(n) worst (all collide)
put(key, val) O(1)* O(1)* O(log n) TreeMap maintains sorted order
remove(key) O(1)* O(1)* O(log n) β€”
containsKey O(1)* O(1)* O(log n) β€”
iteration O(n) O(n) O(n) LinkedHashMap: insertion order
firstKey / lastKey β€” β€” O(log n) TreeMap only β€” BST min/max

* HashMap O(1) average assumes a good hash function

Set Implementations
Operation HashSet LinkedHashSet TreeSet Notes
add(value) O(1)* O(1)* O(log n) β€”
contains() O(1)* O(1)* O(log n) β€”
remove() O(1)* O(1)* O(log n) β€”
first() / last() β€” β€” O(log n) TreeSet only
iteration O(n) O(n) O(n) TreeSet: sorted order
When in doubt: HashMap for lookup, ArrayDeque for LIFO/FIFO.:
  • HashMap gives O(1) get/put for the most common need β€” checking membership or counting frequency.
  • ArrayDeque gives O(1) push/pop/enqueue/dequeue for the second most common need β€” processing items in order.